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

merging in backup control changes #155 #160

Open
wants to merge 15 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bin/run_dwelling.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
'time_zone': None, # option to specify daylight savings, in development

# 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', 'bldg0112631-up00.xml'),
'schedule_input_file': os.path.join(default_input_path, 'Input Files', 'bldg0112631_schedule.csv'),

# Input parameters - weather (note weather_path can be used when Weather Station is specified in HPXML file)
# 'weather_path': weather_path,
Expand Down
2 changes: 2 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
- Fixed issue with ducts in multiple locations [#148](https://github.com/NREL/OCHRE/issues/148)
- Allowed "Occupancy" adjustments in input arguments
- Allow superinsulated slabs compatible with BEopt 3 for test purposes
- Updated backup controls for HVAC to allow additional flexibility, defaults to
representing a major manufacturer [#155](https://github.com/NREL/OCHRE/issues/155)

### OCHRE v0.8.5-beta

Expand Down
2 changes: 0 additions & 2 deletions docs/source/InputsAndArguments.rst
Original file line number Diff line number Diff line change
Expand Up @@ -380,8 +380,6 @@ arguments for HVAC equipment.
+------------------------------------------------+---------------------------+------------------------------+--------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------+
| ``Deadband Temperature (C)`` | number | No | Taken from HPXML file, or 1 | Size of temperature deadband in degC. Can also be specified in the schedule |
+------------------------------------------------+---------------------------+------------------------------+--------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------+
| ``setpoint_ramp_rate`` | number | No | 0.2 for ASHP Heater, otherwise None | Maximum ramp rate of thermostat setpoint, in degC/min |
+------------------------------------------------+---------------------------+------------------------------+--------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------+
| ``show_eir_shr`` | boolean | No | FALSE | If True, show EIR and SHR in results for all time steps. If False, they will be set to 0 when the equipment is off |
+------------------------------------------------+---------------------------+------------------------------+--------------------------------------------------------------------+--------------------------------------------------------------------------------------------------------------------+
| ``Number of Speeds (-)`` | int | No | Taken from HPXML file, or 1 | Number of speeds. Options are 1 (single speed), 2 (double speed), 4 (variable speed), or 10 (mini-split HP only) |
Expand Down
143 changes: 122 additions & 21 deletions ochre/Equipment/HVAC.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,11 @@ def __init__(self, envelope_model=None, use_ideal_capacity=None, **kwargs):
# Thermostat Control Parameters
self.temp_setpoint = initial_setpoint
self.temp_deadband = kwargs.get('Deadband Temperature (C)', 1)
# Offset defines much over deadband is overshooting setpoint, (1-offset) is undershooting
# Offset defaults to reflect lab results
self.deadband_offset = kwargs.get("Deadband Offset (C)", 0.2)
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.setpoint_ramp_rate = kwargs.get('setpoint_ramp_rate') # max setpoint ramp rate, in C/min
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
Expand Down Expand Up @@ -326,13 +329,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
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)
else:
self.temp_setpoint = t_set

self.temp_setpoint = t_set

# set envelope comfort limits
# TODO: update using deadband_offset
if self.envelope_model is not None:
if self.is_heater:
self.envelope_model.heating_setpoint = self.temp_setpoint
Expand All @@ -346,8 +347,8 @@ def run_thermostat_control(self, setpoint=None):
setpoint = self.temp_setpoint

# On and off limits depend on heating vs. cooling
temp_turn_on = setpoint - self.hvac_mult * self.temp_deadband / 2
temp_turn_off = setpoint + self.hvac_mult * self.temp_deadband / 2
temp_turn_on = setpoint - self.hvac_mult * self.temp_deadband * (1 - self.deadband_offset)
temp_turn_off = setpoint + self.hvac_mult * self.temp_deadband * (self.deadband_offset)

# Determine mode
if self.hvac_mult * (self.zone.temperature - temp_turn_on) < 0:
Expand Down Expand Up @@ -560,8 +561,8 @@ def make_equivalent_battery_model(self):
# TODO: update capacitance using 1R1C model
ref_temp = 10 if self.is_heater else 30 # temperature at Energy=0, in C
total_capacitance = convert(self.zone.capacitance, 'kJ', 'kWh') # in kWh/K
max_temp = self.temp_setpoint + self.hvac_mult * self.temp_deadband / 2 # "turn off" temperature
min_temp = self.temp_setpoint - self.hvac_mult * self.temp_deadband / 2 # "turn on" temperature
max_temp = self.temp_setpoint + self.hvac_mult * self.temp_deadband * (1 - self.deadband_offset) # "turn off" temperature
min_temp = self.temp_setpoint - self.hvac_mult * self.temp_deadband * self.deadband_offset # "turn on" temperature
return {
f'{self.end_use} EBM Energy (kWh)': total_capacitance * (self.zone.temperature - ref_temp) * self.hvac_mult,
f'{self.end_use} EBM Min Energy (kWh)': total_capacitance * (min_temp - ref_temp) * self.hvac_mult,
Expand Down Expand Up @@ -782,8 +783,8 @@ def run_two_speed_control(self):
# else:
# speed_idx = 0
elif self.control_type == 'Setpoint':
# Setpoint-based 2-speed HVAC control: High speed uses setpoint difference of deadband / 2 (overlapping)
high_mode = super().run_thermostat_control(self.temp_setpoint - self.hvac_mult * self.temp_deadband / 2)
# Setpoint-based 2-speed HVAC control: High speed uses setpoint difference of deadband * deadband_offset (overlapping)
high_mode = super().run_thermostat_control(self.temp_setpoint - self.hvac_mult * self.deadband_offset)
if high_mode == 'On':
speed = 2
elif high_mode == 'Off':
Expand Down Expand Up @@ -1067,14 +1068,22 @@ def __init__(self, **kwargs):

super().__init__(**kwargs)

# backup element parameters
self.outdoor_temp_limit = kwargs.get('Supplemental Heater Cut-in Temperature (C)') # temp to shut off HP
# backup element capacity and efficiency parameters
self.er_capacity_rated = kwargs['Supplemental Heater Capacity (W)']
self.er_eir_rated = kwargs.get('Supplemental Heater EIR (-)', 1)
self.er_capacity = 0
self.er_ext_capacity = None # Option to set ER capacity directly, ideal capacity only
self.er_ext_capacity_frac = 1 # Option to limit max capacity, ideal capacity only

# backup element control parameters
# TODO: add options for ER temperature_offset,
# min_setpoint_change_duration, hard_lockout
# outdoor_temp_limit shuts off HP
self.outdoor_temp_limit = kwargs.get('Supplemental Heater Cut-in Temperature (C)')
self.timestep_count = 1
self.prev_setpoint = self.temp_setpoint
# self.existing_stages = 0 # staged backup, number of stages on

# Update minimum time for ER element
er_on_time = kwargs.get(self.end_use + ' Minimum ER On Time', 0)
self.min_time_in_mode['HP and ER On'] = dt.timedelta(minutes=er_on_time)
Expand Down Expand Up @@ -1165,22 +1174,114 @@ 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
# 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
def run_er_thermostat_control(
self,
temperature_offset = 1.6,
min_setpoint_change_duration = 30,
hard_lockout = 10,
staged = False,
max_outdoor_temp = 1.67
):
# get indoor temperature
temp_indoor = self.zone.temperature

Copy link
Collaborator

Choose a reason for hiding this comment

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

Should these be options that can be changed or switched off?

# if the outdoor temp is greater than input value, turn er off
if self.outdoor_temp_limit is not None:
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'
else: # in case there is no outdoor temp limit provided, use a default
Copy link
Collaborator

Choose a reason for hiding this comment

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

If we want a default, it should be specified in init. I think we didn't have a
default before, maybe we should remove this?

if self.current_schedule['Ambient Dry Bulb (C)'] >= max_outdoor_temp:
self.timestep_count = 1
self.prev_setpoint = self.temp_setpoint
# self.existing_stages = 0 # no staged
return 'Off'

# Determine if setpoint has changed recently
if min_setpoint_change_duration is not None and 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
self.warn(f"minimum setpoint change duration ({min_setpoint_change_duration} minutes) updated to comply with hard lockout interval ({hard_lockout} minutes)")
if self.temp_setpoint > self.prev_setpoint: # turned up the heat
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
elif (self.timestep_count * self.time_res) > hard_lockout_interval: # hard lockout duration met
if self.temp_indoor_prev is not None:
if temp_indoor < self.temp_indoor_prev: # temp is decreasing
self.timestep_count == 1 # if it turns on, will reset this
# 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'
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'

# run thermostat control for ER element - lower the setpoint by the deadband or user input
# 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:
er_setpoint = self.temp_setpoint
temp_turn_on = er_setpoint - self.hvac_mult * temperature_offset
mnblonsky marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't understand this. Are we changing the deadband size of the ER element?
Should this be default behavior, or should there be a flag for it?

temp_turn_off = er_setpoint + self.hvac_mult * (1 - self.deadband_offset)
else:
er_setpoint = self.temp_setpoint - self.temp_deadband
temp_turn_on = er_setpoint - self.hvac_mult * self.deadband_offset
temp_turn_off = er_setpoint + self.hvac_mult * (1 - self.deadband_offset)

# Determine mode
if self.hvac_mult * (temp_indoor - temp_turn_on) < 0:
self.prev_setpoint = self.temp_setpoint
self.timestep_count = 1
# if staged==True: # TODO: need to edit downstream to make use of staged backup
# operating_capacity = self.staged_backup()
return 'On'
if self.hvac_mult * (temp_indoor - temp_turn_off) > 0:
self.timestep_count = 1
self.prev_setpoint = self.temp_setpoint
# self.existing_stages = 0 # no staged
return 'Off'

# TODO: staged backup (gradually increasing amount of capacity available) (lowest priority)
# 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
#
# # rounding to lowest integer #TODO: is the correct variable for er capacity?
# number_stages = max(1, self.er_capacity_rated//capacity_per_stage)
# 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:
if self.er_ext_capacity is not None:
Expand Down
3 changes: 0 additions & 3 deletions test/test_equipment/test_hvac.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
'cooling fan power (W/cfm)': 0.3,
'supplemental heater cut in temp (C)': -17,
'supplemental heating capacity (W)': 1000,
'setpoint_ramp_rate': None,
'envelope_model': envelope,
})

Expand Down Expand Up @@ -132,8 +131,6 @@ def test_update_setpoint(self):
self.hvac.update_setpoint(update_args_heat)
self.assertEqual(self.hvac.temp_setpoint, 21)

# test with ramp rate
self.hvac.setpoint_ramp_rate = 0.5
self.hvac.update_setpoint(update_args_cool)
self.assertEqual(self.hvac.temp_setpoint, 20.5)

Expand Down