Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

backup control to dos #146

Draft
wants to merge 11 commits into
base: dev
Choose a base branch
from
14 changes: 8 additions & 6 deletions bin/run_dwelling.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,16 @@

# Timing parameters
'start_time': dt.datetime(2018, 1, 1, 0, 0), # year, month, day, hour, minute
'time_res': dt.timedelta(minutes=10), # time resolution of the simulation
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kendallbaertlein: We'll want to revert these changes, they were just for testing on your local branch. I don't think we actually want anything in run_dwelling.py to change here.

'duration': dt.timedelta(days=3), # duration of the simulation
'initialization_time': dt.timedelta(days=1), # used to create realistic starting temperature
# 'start_time': dt.datetime(2018, 4, 26, 0, 0), # year, month, day, hour, minute
'time_res': dt.timedelta(minutes=1), # time resolution of the simulation
'duration': dt.timedelta(days=365), # duration of the simulation
'initialization_time': dt.timedelta(days=2), # used to create realistic starting temperature
'time_zone': None, # option to specify daylight savings, in development
'output_path': default_input_path,

# Input parameters - Sample building (uses HPXML file and time series schedule file)
'hpxml_file': os.path.join(default_input_path, 'Input Files', 'sample_resstock_properties.xml'),
'schedule_input_file': os.path.join(default_input_path, 'Input Files', 'sample_resstock_schedule.csv'),
'hpxml_file': os.path.join(default_input_path, 'Input Files', 'Denver_example_more_heating.xml'),
'schedule_input_file': os.path.join(default_input_path, 'Input Files', 'Denver_example.csv'),

# Input parameters - weather (note weather_path can be used when Weather Station is specified in HPXML file)
# 'weather_path': weather_path,
Expand Down Expand Up @@ -126,7 +128,7 @@
df, metrics, hourly = dwelling.simulate()

# Load results from previous run
# output_path = dwelling_args.get('output_path', os.path.dirname(dwelling_args['hpxml_file']))
output_path = dwelling_args.get('output_path', os.path.dirname(dwelling_args['hpxml_file']))
# df, metrics, hourly = Analysis.load_ochre(output_path, simulation_name)

# Plot results
Expand Down
98 changes: 98 additions & 0 deletions calculate electricity price.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import datetime as dt
import pandas as pd
import os

df = pd.read_csv("case 8 annual/OCHRE_hourly.csv")
df["Time"] = pd.to_datetime(df["Time"])

cost = []
unmet_load = []

df.dropna(inplace=True)

for n in range(len(df)):
date = df["Time"].iloc[n]
TOU = True

winter = False
summer = False

winter_1_start = dt.datetime(2018, 1, 1, 0, 0) #.timestamp() # year, month, day, hour, minute
winter_1_end = dt.datetime(2018, 6, 1, 0, 0) #.timestamp() # year, month, day, hour, minute

winter_2_start = dt.datetime(2018, 10, 1, 0, 0)#.timestamp() # year, month, day, hour, minute
winter_2_end = dt.datetime(2019, 1, 1, 0, 0)#.timestamp() # year, month, day, hour, minute

summer_start = dt.datetime(2018, 6, 1, 0, 0)#.timestamp() # year, month, day, hour, minute
summer_end = dt.datetime(2018, 10, 1, 0, 0)#.timestamp() # year, month, day, hour, minute

# figure out whether it is summer or winter rates
if date >= winter_1_start and date < winter_1_end:
winter = True
elif date >= summer_start and date < summer_end:
summer = True
elif date >= winter_2_start and date < winter_2_end:
winter = True
else:
raise Exception("invalid date input", date)

off_peak_start_1 = dt.time(19, 0, 0)
off_peak_end_1 = dt.time(23, 59, 59)
off_peak_start_2 = dt.time(0, 0, 0)
off_peak_end_2 = dt.time(13, 0, 0)

mid_peak_start = dt.time(13, 0, 0)
mid_peak_end = dt.time(15, 0, 0)

on_peak_start = dt.time(15, 0, 0)
on_peak_end = dt.time(19, 0, 0)

# figure out prices based on time of day, season, and rate
if TOU == True: # prices based on https://co.my.xcelenergy.com/s/billing-payment/residential-rates/time-of-use-pricing
if date.time() >= off_peak_start_1 and date.time() <= off_peak_end_1:
if winter == True:
rate = 0.12
elif summer == True:
rate = 0.12
else:
raise Exception("invalid date input", date)
elif date.time() >= off_peak_start_2 and date.time() < off_peak_end_2:
if winter == True:
rate = 0.12
elif summer == True:
rate = 0.12
else:
raise Exception("invalid date input", date)
elif date.time() >= mid_peak_start and date.time() < mid_peak_end:
if winter == True:
rate = 0.16
elif summer == True:
coratest = 0.22
else:
raise Exception("invalid date input", date)
elif date.time() >= on_peak_start and date.time() < on_peak_end:
if winter == True:
rate = 0.21
elif summer == True:
rate = 0.33
else:
raise Exception("invalid date input", date)
else:
raise Exception("invalid date input", date)
else:
if winter == True:
rate = 0.13
elif summer == True:
rate = 0.16
else:
raise Exception("invalid input")

cost += [rate*df['Total Electric Energy (kWh)'].iloc[n]]
unmet_load += [abs(df['Unmet HVAC Load (C)'].iloc[n])]

df["Electricity Cost [$]"] = cost
print("Total Electricity Cost ($): ", sum(cost))
print("HVAC Energy Consumption (kWh)", sum(df['HVAC Heating Main Power (kW)']))
print("Backup Energy Consumption (kWh)", sum(df['HVAC Heating ER Power (kW)']))
print("Combined HVAC and Backup Energy Consumption (kWh)", sum(df['HVAC Heating Main Power (kW)']))
print("Unmet Load (hr*C)", sum(unmet_load))
171 changes: 163 additions & 8 deletions ochre/Equipment/HVAC.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,18 @@ def __init__(self, envelope_model=None, use_ideal_capacity=None, **kwargs):
self.temp_deadband = kwargs.get('Deadband Temperature (C)', 1)
self.ext_ignore_thermostat = kwargs.get('ext_ignore_thermostat', False)
self.setpoint_ramp_rate = kwargs.get('setpoint_ramp_rate') # max setpoint ramp rate, in C/min
self.kendall_test = 8
self.temp_indoor_prev = self.temp_setpoint
self.ext_capacity = None # Option to set capacity directly, ideal capacity only
self.ext_capacity_frac = 1 # Option to limit max capacity, ideal capacity only

# Electric Resistance Control
self.timestep_count = 1
self.prev_setpoint = None
self.prev_temp_indoor = None
self.existing_stages = 0 # staged backup, number of stages on
self.temp_indoor = None

# Results options
self.show_eir_shr = kwargs.get('show_eir_shr', False)

Expand Down Expand Up @@ -326,6 +334,11 @@ def update_setpoint(self):
# updates setpoint with ramp rate constraints
# TODO: create temp_setpoint_old and update in update_results.
# Could get run multiple times per time step in update_model

# FIXME: do we need a ramp rate with lockout for time after setpoint change
if self.kendall_test in {2, 3, 4, 5, 6, 7, 8}:
self.setpoint_ramp_rate = None

if self.setpoint_ramp_rate is not None:
delta_t = self.setpoint_ramp_rate * self.time_res.total_seconds() / 60 # in C
self.temp_setpoint = min(max(t_set, self.temp_setpoint - delta_t), self.temp_setpoint + delta_t)
Expand Down Expand Up @@ -1165,21 +1178,163 @@ def update_internal_control(self):
else:
return 'Off'

def run_er_thermostat_control(self):
# run thermostat control for ER element - lower the setpoint by the deadband
def run_er_thermostat_control(self, temperature_offset = None, min_setpoint_change_duration = None, hard_lockout = 10, staged = False): #temp offset = 2, min setpoint change duration = 30
# TODO: add option to keep setpoint as is, e.g. when using external control
er_setpoint = self.temp_setpoint - self.temp_deadband
temp_indoor = self.zone.temperature
# TODO: input for how far off of setpoint (setpoint - user input)
# TODO: lockout after setpoint changes # self.temp_setpoint ?
# TODO: checking indoor temp (update_internal_control ?)
# TODO: staged backup (gradually increasing amount of capacity available) (lowest priority)

# cases for testing purposes
# case 1: baseline (nothing), case 2: only staged backup, case 3: only lockout after setpoint change, case 4: lockout and staged,
# case 5: input how far off setpoint, case 6: staged and offset temp, case 7: all (staged, offset, lockout), case 8: offset temp and lockout
# offset temp
if self.kendall_test in {1, 2, 3, 4}:
temperature_offset = None # no offset
elif self.kendall_test in {5, 6, 7, 8}:
temperature_offset = 1.3 # degree offset: ecobee options: 1.1, 1.3, 1.4, 1.6, 1.8
else:
print("self.kendall_test input error", self.kendall_test)
# lockout after setpoint change
if self.kendall_test in {1, 2, 5, 6}:
min_setpoint_change_duration = None # no lockout
elif self.kendall_test in {3, 4, 7, 8}:
min_setpoint_change_duration = 30 # 30 min lockout
else:
print("self.kendall_test input error", self.kendall_test)
# staged backup
if self.kendall_test in {1, 3, 5, 8}:
staged = False # not staged
elif self.kendall_test in {2, 4, 6, 7}:
staged = True # staged
else:
print("self.kendall_test input error", self.kendall_test)

# indoor and previous temp
try:
self.prev_temp_indoor = self.temp_indoor
except: #this is for the first run
self.prev_temp_indoor = None

self.temp_indoor = self.zone.temperature

# run thermostat control for ER element - lower the setpoint by the deadband or user input
if temperature_offset is not None:
er_setpoint = self.temp_setpoint # - temperature_offset/2
else:
er_setpoint = self.temp_setpoint - self.temp_deadband

# if the outdoor temp is greater than input value, turn er off
if self.current_schedule['Ambient Dry Bulb (C)'] >= self.outdoor_temp_limit:
self.timestep_count = 1
self.prev_setpoint = self.temp_setpoint
self.existing_stages = 0 # no staged
return 'Off'

# if staged==True: # TODO: need to edit downstream to make use of staged backup
# operating_capacity = self.staged_backup()
# print(operating_capacity)

hl = False # hl : hard lockout

# Determine if setpoint has changed recently
if min_setpoint_change_duration is not None:
if self.prev_setpoint is not None:
min_interval = dt.timedelta(minutes=min_setpoint_change_duration) # minimum amount of time after a setpoint change that er stays off (user input)
hard_lockout_interval = dt.timedelta(minutes=hard_lockout) # minimum amount of time after a setpoint change that er stays off (strictly)

if hard_lockout_interval > min_interval:
min_interval = hard_lockout_interval # increase the minimum interval
print(f"minimum setpoint change duration ({min_setpoint_change_duration} minutes) updated to comply with hard lockout interval ({hard_lockout} minutes)") #TODO: raise warning ?
# hard_lockout_interval = min_interval # another option but i think it is a worse option?
if self.end_use == 'HVAC Heating':
if self.temp_setpoint > self.prev_setpoint: # turned up the heat
if (self.time_res > min_interval) or (self.timestep_count * self.time_res > min_interval): # enough time has passed
self.timestep_count = 1 # reset timestep count
# control by temp_turn_on/temp_turn_off
elif (self.time_res > hard_lockout_interval) or (self.timestep_count * self.time_res > hard_lockout_interval): # hard lockout duration met
if self.prev_temp_indoor is not None:
if self.temp_indoor < self.prev_temp_indoor: # temp is decreasing
self.timestep_count += 1 # if it turns on, will reset this
hl = True
# control by temp_turn_on/temp_turn_off
else:
self.existing_stages = 0 # no staged
self.timestep_count += 1 # continue iterating
return 'Off'
else:
self.existing_stages = 0 # no staged
self.timestep_count += 1 # continue iterating
return 'Off'
elif (self.time_res > min_interval) or (self.timestep_count * self.time_res > min_interval): # enough time has passed
self.timestep_count = 1 # reset timestep count
# control by temp_turn_on/temp_turn_off
else:
self.timestep_count += 1 # wait longer
self.existing_stages = 0 # no staged
return 'Off'
elif self.temp_setpoint < self.prev_setpoint: # turned down the heat
self.prev_setpoint = self.temp_setpoint
self.timestep_count = 1
self.existing_stages = 0 # no staged
return 'Off'
# elif self.end_use == 'HVAC Cooling':
# if self.temp_setpoint < self.prev_setpoint: # turned up the ac
# if self.timestep_count*self.time_res > min_interval: # enough time has passed
# self.timestep_count = 1 # reset timestep count
# # control by temp_turn_on/temp_turn_off
# else:
# self.timestep_count += 1 # wait longer
# self.existing_stages = 0 # no staged
# return 'Off'
# elif self.temp_setpoint > self.prev_setpoint: # turned down the ac
# self.prev_setpoint = self.temp_setpoint
# self.timestep_count = 1
# self.existing_stages = 0 # no staged
# return 'Off'

# On and off limits depend on heating vs. cooling
temp_turn_on = er_setpoint - self.hvac_mult * self.temp_deadband / 2
temp_turn_off = er_setpoint + self.hvac_mult * self.temp_deadband / 2
if temperature_offset is not None:
temp_turn_on = er_setpoint - self.hvac_mult * temperature_offset
temp_turn_off = er_setpoint
else:
temp_turn_on = er_setpoint - self.hvac_mult * self.temp_deadband / 2
temp_turn_off = er_setpoint + self.hvac_mult * self.temp_deadband / 2

# Determine mode
if self.hvac_mult * (temp_indoor - temp_turn_on) < 0:
if self.hvac_mult * (self.temp_indoor - temp_turn_on) < 0:
self.prev_setpoint = self.temp_setpoint
# print("modeon")
self.timestep_count = 1
return 'On'
if self.hvac_mult * (temp_indoor - temp_turn_off) > 0:
if self.hvac_mult * (self.temp_indoor - temp_turn_off) > 0:
# print("modeoff")
if hl == False: # have a separate option for hard lockout so it can keep iterating for whole lockout period.
self.timestep_count = 1
self.prev_setpoint = self.temp_setpoint
self.existing_stages = 0 # no staged
return 'Off'

def staged_backup(self, capacity_per_stage=5): # Returns partial capacity based on amount of stages currently on/total amount of stages
# TODO: make a time interval between adding stages (5 min default), update with ecobee/other controls: https://support.ecobee.com/s/articles/Threshold-settings-for-ecobee-thermostats
number_stages = max(1, self.er_capacity_rated//capacity_per_stage) #rounding to lowest integer #TODO: is the correct variable for er capacity?

if number_stages==1:
return self.total_capacity
else:
if self.existing_stages == number_stages: # fully on
return self.total_capacity
elif self.existing_stages > 0: #already partially on
self.existing_stages += 1
multiplier = self.existing_stages/number_stages
if multiplier >= 1:
self.existing_stages = number_stages
return self.total_capacity
else:
return multiplier*capacity_per_stage
else: # turning on, previously off
self.existing_stages += 1
return capacity_per_stage

def update_er_capacity(self, hp_capacity):
if self.use_ideal_capacity:
Expand Down
Loading