From 3d6916c4368680ce1c7823ff5a820bc9be89b861 Mon Sep 17 00:00:00 2001 From: jmaguire1 Date: Tue, 6 Aug 2024 14:15:42 -0600 Subject: [PATCH 01/12] Initial creation of 120V HPWH model --- ochre/Equipment/WaterHeater.py | 51 ++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/ochre/Equipment/WaterHeater.py b/ochre/Equipment/WaterHeater.py index 1474aca..9152c74 100644 --- a/ochre/Equipment/WaterHeater.py +++ b/ochre/Equipment/WaterHeater.py @@ -413,8 +413,12 @@ class HeatPumpWaterHeater(ElectricResistanceWaterHeater): def __init__(self, hp_only_mode=False, water_nodes=12, **kwargs): super().__init__(water_nodes=water_nodes, **kwargs) + self.low_power_hpwh = True + # Control parameters self.hp_only_mode = hp_only_mode + if self.low_power_hpwh: + self.hp_only_mode = True self.er_only_mode = False # True when ambient temp is very hot or cold, forces HP off hp_on_time = kwargs.get('HPWH Minimum On Time (min)', 10) hp_off_time = kwargs.get('HPWH Minimum Off Time (min)', 0) @@ -424,25 +428,38 @@ def __init__(self, hp_only_mode=False, water_nodes=12, **kwargs): self.deadband_temp = kwargs.get('Deadband Temperature (C)', 8.17) # different default than ERWH # Nominal COP based on simulation of the UEF test procedure at varying COPs - self.cop_nominal = kwargs['HPWH COP (-)'] - self.hp_cop = self.cop_nominal - if self.cop_nominal < 2: - self.warn("Low Nominal COP:", self.cop_nominal) - - # Heat pump capacity and power parameters - hardcoded for now - if 'HPWH Capacity (W)' in kwargs: - self.hp_capacity_nominal = kwargs['HPWH Capacity (W)'] # max heating capacity, in W - else: - hp_power_nominal = kwargs.get('HPWH Power (W)', 500) # in W + if self.low_power_hpwh: #TODO: can read a lot of these directly when properly integrated into HPXML + self.cop_nominal = 4.2 + self.hp_cop = self.cop_nominal + self.hp_capacity_nominal = 1499.4 + hp_power_nominal = self.hp_capacity_nominal / self.cop_nominal # in W self.hp_capacity_nominal = hp_power_nominal * self.hp_cop # in W - self.hp_capacity = self.hp_capacity_nominal # in W + self.hp_capacity = self.hp_capacity_nominal # in W + else: + self.cop_nominal = kwargs['HPWH COP (-)'] + self.hp_cop = self.cop_nominal + if self.cop_nominal < 2: + self.warn("Low Nominal COP:", self.cop_nominal) + + # Heat pump capacity and power parameters - hardcoded for now + if 'HPWH Capacity (W)' in kwargs: + self.hp_capacity_nominal = kwargs['HPWH Capacity (W)'] # max heating capacity, in W + else: + hp_power_nominal = kwargs.get('HPWH Power (W)', 500) # in W + self.hp_capacity_nominal = hp_power_nominal * self.hp_cop # in W + self.hp_capacity = self.hp_capacity_nominal # in W self.parasitic_power = kwargs.get('HPWH Parasitics (W)', 1) # Standby power in W self.fan_power = kwargs.get('HPWH Fan Power (W)', 35) # in W # Dynamic capacity coefficients # curve format: [1, t_in_wet, t_in_wet ** 2, t_lower, t_lower ** 2, t_lower * t_in_wet] - self.hp_capacity_coeff = np.array([0.563, 0.0437, 0.000039, 0.0055, -0.000148, -0.000145]) - self.cop_coeff = np.array([1.1332, 0.063, -0.0000979, -0.00972, -0.0000214, -0.000686]) + if self.low_power_hpwh: + self.hp_capacity_coeff = np.array([0.813, 0.0160, 0.000537, 0.0020319, -0.0000860, -0.0000686]) + self.cop_coeff = np.array([1.1332, 0.063, -0.0000979, -0.00972, -0.0000214, -0.000686]) + + else: + self.hp_capacity_coeff = np.array([0.563, 0.0437, 0.000039, 0.0055, -0.000148, -0.000145]) + self.cop_coeff = np.array([1.0132, .0436, 0.0000117, -0.01113, 0.00003688, -0.000498]) # Sensible and latent heat parameters self.shr_nominal = kwargs.get('HPWH SHR (-)', 0.88) # unitless @@ -562,7 +579,13 @@ def run_thermostat_control(self, use_future_states=False): def update_internal_control(self): # operate as ERWH when ambient temperatures are out of bounds t_amb = self.current_schedule['Zone Temperature (C)'] - if t_amb < 7.222 or t_amb > 43.333: + if self.low_power_hpwh: + t_low = 2.778 + t_high = 62.778 + else: + t_low = 7.222 + t_high = 43.333 + if t_amb < t_low or t_amb > t_high: self.er_only_mode = True else: self.er_only_mode = False From a5cac45c1eb20b800d9e835266cfb531a2890697 Mon Sep 17 00:00:00 2001 From: jmaguire1 Date: Tue, 6 Aug 2024 14:50:13 -0600 Subject: [PATCH 02/12] parse 120V HPWH based on temp logic in HPXML for panels branch --- ochre/utils/hpxml.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ochre/utils/hpxml.py b/ochre/utils/hpxml.py index 2a48d08..6060cf1 100644 --- a/ochre/utils/hpxml.py +++ b/ochre/utils/hpxml.py @@ -925,6 +925,7 @@ def parse_water_heater(water_heater, water, construction, solar_fraction=0): first_hour_rating = water_heater.get('FirstHourRating') recovery_efficiency = water_heater.get('RecoveryEfficiency') tank_jacket_r = water_heater.get('WaterHeaterInsulation', {}).get('Jacket', {}).get('JacketRValue', 0) + low_power_hpwh = False # calculate actual volume from rated volume if volume_gal is not None: @@ -952,6 +953,9 @@ def parse_water_heater(water_heater, water, construction, solar_fraction=0): elif water_heater_type == 'heat pump water heater': assert is_electric eta_c = 1 + if uniform_energy_factor == 4.9: #FIXME: temporary flag for designating 120V HPWHs in panels branch of ResStock + low_power_hpwh = True + # HPWH UA calculation taken from ResStock: # https://github.com/NREL/resstock/blob/run/restructure-v3/resources/hpxml-measures/HPXMLtoOpenStudio/resources/waterheater.rb#L765 if volume_gal <= 58.0: @@ -1036,6 +1040,7 @@ def parse_water_heater(water_heater, water, construction, solar_fraction=0): 'Energy Factor (-)': energy_factor, 'Tank Volume (L)': volume, 'Tank Height (m)': height, + 'Low Power HPWH': low_power_hpwh, } if heating_capacity is not None: wh['Capacity (W)'] = convert(heating_capacity, 'Btu/hour', 'W') From 212c4df2bc4d0b87dafcc39b5cf2f16959013bf4 Mon Sep 17 00:00:00 2001 From: jmaguire1 Date: Tue, 6 Aug 2024 15:07:08 -0600 Subject: [PATCH 03/12] Trying to add a tempering valve, still some hard coded things for panels --- ochre/Models/Water.py | 12 +++++++++++- ochre/utils/hpxml.py | 12 ++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/ochre/Models/Water.py b/ochre/Models/Water.py index 5469e77..c96743c 100644 --- a/ochre/Models/Water.py +++ b/ochre/Models/Water.py @@ -73,6 +73,7 @@ def __init__(self, water_nodes=12, water_vol_fractions=None, **kwargs): # mixed temperature (i.e. target temperature) setpoint for fixtures - Sink/Shower/Bath (SSB) self.tempered_draw_temp = kwargs.get('Mixed Delivery Temperature (C)', convert(105, 'degF', 'degC')) + self.hot_draw_temp = kwargs.get('Tempering Valve Setpoint (C)', convert(125, 'degF', 'degC')) # Removing target temperature for clothes washers # self.washer_draw_temp = kwargs.get('Clothes Washer Delivery Temperature (C)', convert(92.5, 'degF', 'degC')) @@ -148,7 +149,16 @@ def update_water_draw(self): # calculate total draw volume from tempered draw volume(s) # for tempered draw, assume outlet temperature == T1, slightly off if the water draw is very large - self.draw_total = draw_hot + t_set = kwargs.get('Setpoint Temperature (C)', convert(125, 'degF', 'degC')) #FIXME: can pass this in from elesewhere + if self.tempered_draw_temp < t_set: + if self.outlet_temp <= self.hot_draw_temp: + self.draw_total += draw_hot + else: + vol_ratio_hot = (self.hot_draw_temp - self.mains_temp) / (self.outlet_temp - self.mains_temp) + self.draw_total += draw_hot * vol_ratio_hot + else: + self.draw_total = draw_hot + if draw_tempered: if self.outlet_temp <= self.tempered_draw_temp: self.draw_total += draw_tempered diff --git a/ochre/utils/hpxml.py b/ochre/utils/hpxml.py index 6060cf1..58dbb85 100644 --- a/ochre/utils/hpxml.py +++ b/ochre/utils/hpxml.py @@ -1011,6 +1011,7 @@ def parse_water_heater(water_heater, water, construction, solar_fraction=0): else: raise OCHREException(f'Unknown water heater type: {water_heater_type}') + # Increase insulation from tank jacket (reduces UA) if tank_jacket_r: @@ -1028,12 +1029,19 @@ def parse_water_heater(water_heater, water, construction, solar_fraction=0): if eta_c > 1.0: raise OCHREException('A water heater heat source (either burner or element) efficiency of > 1 has been calculated.' ' Double check water heater inputs.') - + + if low_power_hpwh: + t_set = 140 #F + t_temper = 125 #F + else: + t_set = convert(water_heater.get('HotWaterTemperature', 125), 'degF', 'degC') + t_temper = t_set wh = { 'Equipment Name': water_heater_type, 'Fuel': water_heater['FuelType'].capitalize(), 'Zone': parse_zone_name(water_heater['Location']), - 'Setpoint Temperature (C)': convert(water_heater.get('HotWaterTemperature', 125), 'degF', 'degC'), + 'Setpoint Temperature (C)': t_set, + 'Temepring Valve Setpoint (C)': t_temper, # 'Heat Transfer Coefficient (W/m^2/K)': u, 'UA (W/K)': convert(ua, 'Btu/hour/degR', 'W/K'), 'Efficiency (-)': eta_c, From 0ba3e017e0be8992b0f4198eee6c1bcb92f38861 Mon Sep 17 00:00:00 2001 From: jmaguire1 Date: Tue, 6 Aug 2024 15:24:19 -0600 Subject: [PATCH 04/12] More 120V HPWH changes, update test run --- bin/run_dwelling.py | 6 ++++-- ochre/Equipment/WaterHeater.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/bin/run_dwelling.py b/bin/run_dwelling.py index d2f1c83..f02cf67 100644 --- a/bin/run_dwelling.py +++ b/bin/run_dwelling.py @@ -23,8 +23,10 @@ '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', 'sample_resstock_properties.xml'), + #'schedule_input_file': os.path.join(default_input_path, 'Input Files', 'sample_resstock_schedule.csv'), + 'hpxml_file': 'C:/OCHRE-120V-hpwh/in.xml', + 'schedule_input_file': 'C:/OCHRE-120V-hpwh/schedules.csv', # Input parameters - weather (note weather_path can be used when Weather Station is specified in HPXML file) # 'weather_path': weather_path, diff --git a/ochre/Equipment/WaterHeater.py b/ochre/Equipment/WaterHeater.py index 9152c74..af3ccff 100644 --- a/ochre/Equipment/WaterHeater.py +++ b/ochre/Equipment/WaterHeater.py @@ -413,7 +413,7 @@ class HeatPumpWaterHeater(ElectricResistanceWaterHeater): def __init__(self, hp_only_mode=False, water_nodes=12, **kwargs): super().__init__(water_nodes=water_nodes, **kwargs) - self.low_power_hpwh = True + self.low_power_hpwh = kwargs.get('Low Power HPWH', false) # Control parameters self.hp_only_mode = hp_only_mode From 1411e40f12280e35d2cfee85c0a4043a9eabcd62 Mon Sep 17 00:00:00 2001 From: jmaguire1 Date: Tue, 6 Aug 2024 15:39:49 -0600 Subject: [PATCH 05/12] A couple of bug fixes on the initial attempt --- ochre/Equipment/WaterHeater.py | 2 +- ochre/Models/Water.py | 4 ++-- ochre/utils/hpxml.py | 4 ++-- ochre/utils/schedule.py | 1 + 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ochre/Equipment/WaterHeater.py b/ochre/Equipment/WaterHeater.py index af3ccff..d90fbab 100644 --- a/ochre/Equipment/WaterHeater.py +++ b/ochre/Equipment/WaterHeater.py @@ -413,7 +413,7 @@ class HeatPumpWaterHeater(ElectricResistanceWaterHeater): def __init__(self, hp_only_mode=False, water_nodes=12, **kwargs): super().__init__(water_nodes=water_nodes, **kwargs) - self.low_power_hpwh = kwargs.get('Low Power HPWH', false) + self.low_power_hpwh = kwargs.get('Low Power HPWH', False) # Control parameters self.hp_only_mode = hp_only_mode diff --git a/ochre/Models/Water.py b/ochre/Models/Water.py index c96743c..3587b65 100644 --- a/ochre/Models/Water.py +++ b/ochre/Models/Water.py @@ -74,6 +74,7 @@ def __init__(self, water_nodes=12, water_vol_fractions=None, **kwargs): # mixed temperature (i.e. target temperature) setpoint for fixtures - Sink/Shower/Bath (SSB) self.tempered_draw_temp = kwargs.get('Mixed Delivery Temperature (C)', convert(105, 'degF', 'degC')) self.hot_draw_temp = kwargs.get('Tempering Valve Setpoint (C)', convert(125, 'degF', 'degC')) + self.setpoint_temp = kwargs.get('Setpoint Temperature (C)', convert(125, 'degF', 'degC')) # Removing target temperature for clothes washers # self.washer_draw_temp = kwargs.get('Clothes Washer Delivery Temperature (C)', convert(92.5, 'degF', 'degC')) @@ -149,8 +150,7 @@ def update_water_draw(self): # calculate total draw volume from tempered draw volume(s) # for tempered draw, assume outlet temperature == T1, slightly off if the water draw is very large - t_set = kwargs.get('Setpoint Temperature (C)', convert(125, 'degF', 'degC')) #FIXME: can pass this in from elesewhere - if self.tempered_draw_temp < t_set: + if self.tempered_draw_temp < self.setpoint_temp: if self.outlet_temp <= self.hot_draw_temp: self.draw_total += draw_hot else: diff --git a/ochre/utils/hpxml.py b/ochre/utils/hpxml.py index f7bc079..5e3dad3 100644 --- a/ochre/utils/hpxml.py +++ b/ochre/utils/hpxml.py @@ -1038,8 +1038,8 @@ def parse_water_heater(water_heater, water, construction, solar_fraction=0): ' Double check water heater inputs.') if low_power_hpwh: - t_set = 140 #F - t_temper = 125 #F + t_set = convert(140, 'degF', 'degC') + t_temper = convert(125, 'degF', 'degC') else: t_set = convert(water_heater.get('HotWaterTemperature', 125), 'degF', 'degC') t_temper = t_set diff --git a/ochre/utils/schedule.py b/ochre/utils/schedule.py index 1c816cd..33376be 100644 --- a/ochre/utils/schedule.py +++ b/ochre/utils/schedule.py @@ -67,6 +67,7 @@ "water_heater_operating_mode": None, "Vacancy": None, "Power Outage": None, + "hot_water_showers": None, #TODO: use this for unmet loads }, } From 1174c46d88b436e944e27ce5a562b14a44f710e1 Mon Sep 17 00:00:00 2001 From: jmaguire1 Date: Thu, 15 Aug 2024 14:44:20 -0600 Subject: [PATCH 06/12] Issue with the tempering valve that accumulated hot draws. Thanks @JingWang-CUB for helping me find this! --- ochre/Models/Water.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ochre/Models/Water.py b/ochre/Models/Water.py index 3587b65..07d1bad 100644 --- a/ochre/Models/Water.py +++ b/ochre/Models/Water.py @@ -152,7 +152,7 @@ def update_water_draw(self): # for tempered draw, assume outlet temperature == T1, slightly off if the water draw is very large if self.tempered_draw_temp < self.setpoint_temp: if self.outlet_temp <= self.hot_draw_temp: - self.draw_total += draw_hot + self.draw_total = draw_hot else: vol_ratio_hot = (self.hot_draw_temp - self.mains_temp) / (self.outlet_temp - self.mains_temp) self.draw_total += draw_hot * vol_ratio_hot From b781132a77e95ccfcd355dab608ec63cb5bf9c6a Mon Sep 17 00:00:00 2001 From: Michael Blonsky Date: Wed, 28 Aug 2024 16:43:56 -0600 Subject: [PATCH 07/12] adding shower draw for unmet loads --- ochre/Models/Water.py | 12 ++-- ochre/utils/hpxml.py | 3 +- ochre/utils/schedule.py | 124 ++++++++++++++++++++++------------------ 3 files changed, 76 insertions(+), 63 deletions(-) diff --git a/ochre/Models/Water.py b/ochre/Models/Water.py index 07d1bad..dc7098d 100644 --- a/ochre/Models/Water.py +++ b/ochre/Models/Water.py @@ -37,7 +37,8 @@ class StratifiedWaterModel(RCModel): """ name = 'Water Tank' optional_inputs = [ - 'Water Heating (L/min)', + 'Water Fixtures (L/min)', + 'Showers (L/min)', 'Clothes Washer (L/min)', 'Dishwasher (L/min)', 'Mains Temperature (C)', @@ -133,7 +134,8 @@ def update_water_draw(self): self.outlet_temp = self.states[self.t_1_idx] # initial outlet temp, for estimating draw volume # Note: removing target draw temperature for clothes washers, not implemented in ResStock - draw_tempered = self.current_schedule.get('Water Heating (L/min)', 0) + draw_tempered = self.current_schedule.get('Water Fixtures (L/min)', 0) + draw_showers = self.current_schedule.get('Showers (L/min)', 0) draw_hot = (self.current_schedule.get('Clothes Washer (L/min)', 0) + self.current_schedule.get('Dishwasher (L/min)', 0)) # draw_cw = self.current_schedule.get('Clothes Washer (L/min)', 0) @@ -224,8 +226,10 @@ def update_water_draw(self): self.h_delivered = q_delivered / t_s heats_to_model += q_nodes / t_s - # calculate unmet loads, fixtures only, in W - self.h_unmet_load = max(draw_tempered / 60 * water_c * (self.tempered_draw_temp - self.outlet_temp), 0) # in W + # calculate unmet loads, showers only, in W + self.h_unmet_load = max( + draw_showers / 60 * water_c * (self.tempered_draw_temp - self.outlet_temp), 0 + ) # in W return heats_to_model diff --git a/ochre/utils/hpxml.py b/ochre/utils/hpxml.py index 5e3dad3..c1997ec 100644 --- a/ochre/utils/hpxml.py +++ b/ochre/utils/hpxml.py @@ -1116,8 +1116,7 @@ def parse_water_heater(water_heater, water, construction, solar_fraction=0): distribution_gal_per_day = mw_gpd * fixture_multiplier # Combine fixture and distribution water draws in schedule - wh['Fixture Average Water Draw (L/day)'] = convert(fixture_gal_per_day + distribution_gal_per_day, 'gallon/day', - 'L/day') + wh['Average Water Draw (L/day)'] = convert(fixture_gal_per_day + distribution_gal_per_day, 'gallon/day', 'L/day') return wh diff --git a/ochre/utils/schedule.py b/ochre/utils/schedule.py index 33376be..710672c 100644 --- a/ochre/utils/schedule.py +++ b/ochre/utils/schedule.py @@ -47,7 +47,8 @@ # 'basement_mels': 'Basement MELs', # not modeled }, "Water": { - "hot_water_fixtures": "Water Heating", + "hot_water_fixtures": "Water Fixtures", + # "hot_water_showers": "Showers", # for unmet loads only "hot_water_clothes_washer": "Clothes Washer", "hot_water_dishwasher": "Dishwasher", }, @@ -67,7 +68,6 @@ "water_heater_operating_mode": None, "Vacancy": None, "Power Outage": None, - "hot_water_showers": None, #TODO: use this for unmet loads }, } @@ -285,59 +285,67 @@ def create_simple_schedule(weekday_fractions, weekend_fractions=None, month_mult return df['w_fracs'] * df['m_fracs'] -def convert_schedule_column(s_hpxml, ochre_name, properties, category='Power'): - if category == 'Power': - # try getting from max power or from annual energy, priority goes to max power - if 'Max Electric Power (W)' in properties: - max_value = properties['Max Electric Power (W)'] / 1000 # W to kW - elif 'Annual Electric Energy (kWh)' in properties: - annual_mean = properties['Annual Electric Energy (kWh)'] / 8760 - schedule_mean = s_hpxml.mean() - max_value = annual_mean / schedule_mean if schedule_mean != 0 else 0 - else: - max_value = None - if max_value is not None: - out = s_hpxml * max_value - out.name = f'{ochre_name} (kW)' - else: - out = None - - # check for gas (max power and annual energy), and copy schedule - if 'Max Gas Power (therms/hour)' in properties: - max_value = properties['Max Gas Power (therms/hour)'] # in therms/hour - elif 'Annual Gas Energy (therms)' in properties: - annual_mean = properties['Annual Gas Energy (therms)'] / 8760 # in therms/hour - schedule_mean = s_hpxml.mean() - max_value = annual_mean / schedule_mean if schedule_mean != 0 else 0 - else: - max_value = None - if max_value is None: - pass - elif out is None: - out = s_hpxml * max_value - out.name = f'{ochre_name} (therms/hour)' - else: - # combine 2 series into data frame - s_gas = s_hpxml * max_value - s_gas.name = f'{ochre_name} (therms/hour)' - out = pd.concat([out, s_gas], axis=1) - - if out is None: - raise OCHREException(f'Could not determine max value for {s_hpxml.name} schedule ({ochre_name}).') - - elif category == 'Water': - if ochre_name == 'Water Heating': - # Fixtures include sinks, showers, and baths (SSB), all combined - avg_water_draw = properties.get('Fixture Average Water Draw (L/day)', 0) - annual_mean = avg_water_draw / 1440 # in L/min - else: - # For dishwasher and clothes washer, get average draw value from their properties dict - annual_mean = properties['Average Water Draw (L/day)'] / 1440 # in L/min - schedule_mean = s_hpxml.mean() +def convert_power_column(s_hpxml, ochre_name, properties): + # try getting from max power or from annual energy, priority goes to max power + if 'Max Electric Power (W)' in properties: + max_value = properties['Max Electric Power (W)'] / 1000 # W to kW + elif 'Annual Electric Energy (kWh)' in properties: + annual_mean = properties['Annual Electric Energy (kWh)'] / 8760 schedule_mean = s_hpxml.mean() max_value = annual_mean / schedule_mean if schedule_mean != 0 else 0 + else: + max_value = None + if max_value is not None: out = s_hpxml * max_value - out.name = f'{ochre_name} (L/min)' + out.name = f'{ochre_name} (kW)' + else: + out = None + + # check for gas (max power and annual energy), and copy schedule + if 'Max Gas Power (therms/hour)' in properties: + max_value = properties['Max Gas Power (therms/hour)'] # in therms/hour + elif 'Annual Gas Energy (therms)' in properties: + annual_mean = properties['Annual Gas Energy (therms)'] / 8760 # in therms/hour + schedule_mean = s_hpxml.mean() + max_value = annual_mean / schedule_mean if schedule_mean != 0 else 0 + else: + max_value = None + if max_value is None: + pass + elif out is None: + out = s_hpxml * max_value + out.name = f'{ochre_name} (therms/hour)' + else: + # combine 2 series into data frame + s_gas = s_hpxml * max_value + s_gas.name = f'{ochre_name} (therms/hour)' + out = pd.concat([out, s_gas], axis=1) + + if out is None: + raise OCHREException(f'Could not determine max value for {s_hpxml.name} schedule ({ochre_name}).') + + return out + + +def convert_water_column(s_hpxml, ochre_name, equipment): + if ochre_name in ["Water Fixtures", "Showers"]: + # Fixtures include sinks, showers, and baths (SSB), all combined + # Showers are only included for unmet loads calculation + equipment_name = "Water Heating" + else: + equipment_name = ochre_name + + if equipment_name not in equipment: + return None + + properties = equipment[equipment_name] + avg_water_draw = properties.get('Average Water Draw (L/day)', 0) + annual_mean = avg_water_draw / 1440 # in L/min + + schedule_mean = s_hpxml.mean() + max_value = annual_mean / schedule_mean if schedule_mean != 0 else 0 + out = s_hpxml * max_value + out.name = f'{ochre_name} (L/min)' return out @@ -409,11 +417,13 @@ def import_occupancy_schedule(occupancy, equipment, start_time, schedule_input_f s_ochre = s_hpxml * occupancy['Number of Occupants (-)'] s_ochre.name = f'{ochre_name} (Persons)' schedule_data.append(s_ochre) - elif category in ['Power', 'Water']: - if ochre_name not in equipment: - continue - else: - schedule_data.append(convert_schedule_column(s_hpxml, ochre_name, equipment[ochre_name], category)) + elif category == "Power": + if ochre_name in equipment: + schedule_data.append(convert_power_column(s_hpxml, ochre_name, equipment[ochre_name])) + elif category == "Water": + s_ochre = convert_water_column(s_hpxml, ochre_name, equipment) + if s_ochre is not None: + schedule_data.append(s_ochre) elif category == 'Setpoint': # Already in the correct units s_ochre = s_hpxml From 49350c67ce3076aedf4cf948d3cc914f718a7120 Mon Sep 17 00:00:00 2001 From: Michael Blonsky Date: Thu, 29 Aug 2024 09:13:59 -0600 Subject: [PATCH 08/12] moved some low power HPWH inputs to hpxml.py --- ochre/Equipment/WaterHeater.py | 35 ++++++++-------------- ochre/utils/hpxml.py | 53 ++++++++++++++++++---------------- 2 files changed, 40 insertions(+), 48 deletions(-) diff --git a/ochre/Equipment/WaterHeater.py b/ochre/Equipment/WaterHeater.py index d90fbab..755e97e 100644 --- a/ochre/Equipment/WaterHeater.py +++ b/ochre/Equipment/WaterHeater.py @@ -413,12 +413,8 @@ class HeatPumpWaterHeater(ElectricResistanceWaterHeater): def __init__(self, hp_only_mode=False, water_nodes=12, **kwargs): super().__init__(water_nodes=water_nodes, **kwargs) - self.low_power_hpwh = kwargs.get('Low Power HPWH', False) - # Control parameters self.hp_only_mode = hp_only_mode - if self.low_power_hpwh: - self.hp_only_mode = True self.er_only_mode = False # True when ambient temp is very hot or cold, forces HP off hp_on_time = kwargs.get('HPWH Minimum On Time (min)', 10) hp_off_time = kwargs.get('HPWH Minimum Off Time (min)', 0) @@ -428,26 +424,19 @@ def __init__(self, hp_only_mode=False, water_nodes=12, **kwargs): self.deadband_temp = kwargs.get('Deadband Temperature (C)', 8.17) # different default than ERWH # Nominal COP based on simulation of the UEF test procedure at varying COPs - if self.low_power_hpwh: #TODO: can read a lot of these directly when properly integrated into HPXML - self.cop_nominal = 4.2 - self.hp_cop = self.cop_nominal - self.hp_capacity_nominal = 1499.4 - hp_power_nominal = self.hp_capacity_nominal / self.cop_nominal # in W - self.hp_capacity_nominal = hp_power_nominal * self.hp_cop # in W - self.hp_capacity = self.hp_capacity_nominal # in W + self.low_power_hpwh = kwargs.get('Low Power HPWH', False) + self.cop_nominal = kwargs['HPWH COP (-)'] + self.hp_cop = self.cop_nominal + if self.cop_nominal < 2: + self.warn("Low Nominal COP:", self.cop_nominal) + + # Heat pump capacity and power parameters - hardcoded for now + if 'HPWH Capacity (W)' in kwargs: + self.hp_capacity_nominal = kwargs['HPWH Capacity (W)'] # max heating capacity, in W else: - self.cop_nominal = kwargs['HPWH COP (-)'] - self.hp_cop = self.cop_nominal - if self.cop_nominal < 2: - self.warn("Low Nominal COP:", self.cop_nominal) - - # Heat pump capacity and power parameters - hardcoded for now - if 'HPWH Capacity (W)' in kwargs: - self.hp_capacity_nominal = kwargs['HPWH Capacity (W)'] # max heating capacity, in W - else: - hp_power_nominal = kwargs.get('HPWH Power (W)', 500) # in W - self.hp_capacity_nominal = hp_power_nominal * self.hp_cop # in W - self.hp_capacity = self.hp_capacity_nominal # in W + hp_power_nominal = kwargs.get('HPWH Power (W)', 500) # in W + self.hp_capacity_nominal = hp_power_nominal * self.hp_cop # in W + self.hp_capacity = self.hp_capacity_nominal # in W self.parasitic_power = kwargs.get('HPWH Parasitics (W)', 1) # Standby power in W self.fan_power = kwargs.get('HPWH Fan Power (W)', 35) # in W diff --git a/ochre/utils/hpxml.py b/ochre/utils/hpxml.py index c1997ec..41d710a 100644 --- a/ochre/utils/hpxml.py +++ b/ochre/utils/hpxml.py @@ -920,6 +920,7 @@ def parse_water_heater(water_heater, water, construction, solar_fraction=0): # Inputs from HPXML water_heater_type = water_heater['WaterHeaterType'] is_electric = water_heater['FuelType'] == 'electricity' + t_set = convert(water_heater.get('HotWaterTemperature', 125), 'degF', 'degC') energy_factor = water_heater.get('EnergyFactor') uniform_energy_factor = water_heater.get('UniformEnergyFactor') n_beds = construction['Number of Bedrooms (-)'] @@ -932,7 +933,6 @@ def parse_water_heater(water_heater, water, construction, solar_fraction=0): first_hour_rating = water_heater.get('FirstHourRating') recovery_efficiency = water_heater.get('RecoveryEfficiency') tank_jacket_r = water_heater.get('WaterHeaterInsulation', {}).get('Jacket', {}).get('JacketRValue', 0) - low_power_hpwh = False # calculate actual volume from rated volume if volume_gal is not None: @@ -960,9 +960,6 @@ def parse_water_heater(water_heater, water, construction, solar_fraction=0): elif water_heater_type == 'heat pump water heater': assert is_electric eta_c = 1 - if uniform_energy_factor == 4.9: #FIXME: temporary flag for designating 120V HPWHs in panels branch of ResStock - low_power_hpwh = True - # HPWH UA calculation taken from ResStock: # https://github.com/NREL/resstock/blob/run/restructure-v3/resources/hpxml-measures/HPXMLtoOpenStudio/resources/waterheater.rb#L765 if volume_gal <= 58.0: @@ -1018,7 +1015,6 @@ def parse_water_heater(water_heater, water, construction, solar_fraction=0): else: raise OCHREException(f'Unknown water heater type: {water_heater_type}') - # Increase insulation from tank jacket (reduces UA) if tank_jacket_r: @@ -1036,26 +1032,19 @@ def parse_water_heater(water_heater, water, construction, solar_fraction=0): if eta_c > 1.0: raise OCHREException('A water heater heat source (either burner or element) efficiency of > 1 has been calculated.' ' Double check water heater inputs.') - - if low_power_hpwh: - t_set = convert(140, 'degF', 'degC') - t_temper = convert(125, 'degF', 'degC') - else: - t_set = convert(water_heater.get('HotWaterTemperature', 125), 'degF', 'degC') - t_temper = t_set + wh = { - 'Equipment Name': water_heater_type, - 'Fuel': water_heater['FuelType'].capitalize(), - 'Zone': parse_zone_name(water_heater['Location']), - 'Setpoint Temperature (C)': t_set, - 'Temepring Valve Setpoint (C)': t_temper, + "Equipment Name": water_heater_type, + "Fuel": water_heater["FuelType"].capitalize(), + "Zone": parse_zone_name(water_heater["Location"]), + "Setpoint Temperature (C)": t_set, + "Tempering Valve Setpoint (C)": None, # 'Heat Transfer Coefficient (W/m^2/K)': u, - 'UA (W/K)': convert(ua, 'Btu/hour/degR', 'W/K'), - 'Efficiency (-)': eta_c, - 'Energy Factor (-)': energy_factor, - 'Tank Volume (L)': volume, - 'Tank Height (m)': height, - 'Low Power HPWH': low_power_hpwh, + "UA (W/K)": convert(ua, "Btu/hour/degR", "W/K"), + "Efficiency (-)": eta_c, + "Energy Factor (-)": energy_factor, + "Tank Volume (L)": volume, + "Tank Height (m)": height, } if heating_capacity is not None: wh['Capacity (W)'] = convert(heating_capacity, 'Btu/hour', 'W') @@ -1064,8 +1053,22 @@ def parse_water_heater(water_heater, water, construction, solar_fraction=0): # add HPWH COP, from ResStock, defaults to using UEF if uniform_energy_factor is None: uniform_energy_factor = (0.60522 + energy_factor) / 1.2101 - cop = 1.174536058 * uniform_energy_factor # Based on simulation of the UEF test procedure at varying COPs - wh['HPWH COP (-)'] = cop + + # Add/update parameters for low power HPWH + # FIXME: temporary flag for designating 120V HPWHs in panels branch of ResStock + if uniform_energy_factor == 4.9: + wh.update({ + "Low Power HPWH": True, + "HPWH COP (-)": 4.2, + "HPWH Capacity (W)": 1499.4, + "Setpoint Temperature (C)": convert(140, "degF", "degC"), + "Tempering Valve Setpoint (C)": convert(125, "degF", "degC"), + "hp_only_mode": True, + }) + else: + # Based on simulation of the UEF test procedure at varying COPs + wh["HPWH COP (-)"] = 1.174536058 * uniform_energy_factor + if water_heater_type == 'instantaneous water heater' and wh['Fuel'] != 'Electricity': on_time_frac = [0.0269, 0.0333, 0.0397, 0.0462, 0.0529][n_beds - 1] wh['Parasitic Power (W)'] = 5 + 60 * on_time_frac From f099d2a5c3f99ccf8e97039fec2ffb18504df43f Mon Sep 17 00:00:00 2001 From: Michael Blonsky Date: Thu, 29 Aug 2024 09:45:56 -0600 Subject: [PATCH 09/12] set default tempering valve to off --- ochre/Models/Water.py | 47 ++++++++++++++-------------------- test/test_models/test_water.py | 4 +-- 2 files changed, 21 insertions(+), 30 deletions(-) diff --git a/ochre/Models/Water.py b/ochre/Models/Water.py index dc7098d..5325369 100644 --- a/ochre/Models/Water.py +++ b/ochre/Models/Water.py @@ -72,10 +72,12 @@ def __init__(self, water_nodes=12, water_vol_fractions=None, **kwargs): self.mains_temp = 0 # water mains temperature, in C self.outlet_temp = 0 # temperature of outlet water, in C - # mixed temperature (i.e. target temperature) setpoint for fixtures - Sink/Shower/Bath (SSB) - self.tempered_draw_temp = kwargs.get('Mixed Delivery Temperature (C)', convert(105, 'degF', 'degC')) - self.hot_draw_temp = kwargs.get('Tempering Valve Setpoint (C)', convert(125, 'degF', 'degC')) - self.setpoint_temp = kwargs.get('Setpoint Temperature (C)', convert(125, 'degF', 'degC')) + # target draw temperature setpoint for fixtures - Sink/Shower/Bath (SSB) + self.fixture_draw_temp = kwargs.get('Mixed Delivery Temperature (C)', convert(105, 'degF', 'degC')) + + # target draw temperature for CW and DW + # use WH tempering valve setpoint or set to None + self.hot_draw_temp = kwargs.get('Tempering Valve Setpoint (C)') # Removing target temperature for clothes washers # self.washer_draw_temp = kwargs.get('Clothes Washer Delivery Temperature (C)', convert(92.5, 'degF', 'degC')) @@ -150,29 +152,18 @@ def update_water_draw(self): if self.mains_temp is None: raise ModelException('Mains temperature required when water draw exists') - # calculate total draw volume from tempered draw volume(s) - # for tempered draw, assume outlet temperature == T1, slightly off if the water draw is very large - if self.tempered_draw_temp < self.setpoint_temp: - if self.outlet_temp <= self.hot_draw_temp: - self.draw_total = draw_hot - else: - vol_ratio_hot = (self.hot_draw_temp - self.mains_temp) / (self.outlet_temp - self.mains_temp) - self.draw_total += draw_hot * vol_ratio_hot - else: - self.draw_total = draw_hot - - if draw_tempered: - if self.outlet_temp <= self.tempered_draw_temp: - self.draw_total += draw_tempered - else: - vol_ratio = (self.tempered_draw_temp - self.mains_temp) / (self.outlet_temp - self.mains_temp) - self.draw_total += draw_tempered * vol_ratio - # if draw_cw: - # if self.outlet_temp <= self.washer_draw_temp: - # self.draw_total += draw_cw - # else: - # vol_ratio = (self.washer_draw_temp - self.mains_temp) / (self.outlet_temp - self.mains_temp) - # self.draw_total += draw_cw * vol_ratio + # calculate total draw volume from tempered and hot draw volume(s) + # for tempered draws, assume outlet temperature == T_WH1, slightly off if the water draw is very large + if self.outlet_temp > self.fixture_draw_temp: + vol_ratio = (self.fixture_draw_temp - self.mains_temp) / (self.outlet_temp - self.mains_temp) + draw_tempered *= vol_ratio + # draw_showers *= vol_ratio # no impact on model or results + + if self.hot_draw_temp is not None and self.outlet_temp > self.hot_draw_temp: + vol_ratio = (self.hot_draw_temp - self.mains_temp) / (self.outlet_temp - self.mains_temp) + draw_hot *= vol_ratio + + self.draw_total = draw_tempered + draw_hot t_s = self.time_res.total_seconds() draw_liters = self.draw_total * t_s / 60 # in liters @@ -228,7 +219,7 @@ def update_water_draw(self): # calculate unmet loads, showers only, in W self.h_unmet_load = max( - draw_showers / 60 * water_c * (self.tempered_draw_temp - self.outlet_temp), 0 + draw_showers / 60 * water_c * (self.fixture_draw_temp - self.outlet_temp), 0 ) # in W return heats_to_model diff --git a/test/test_models/test_water.py b/test/test_models/test_water.py index 34fa45b..9d32001 100644 --- a/test/test_models/test_water.py +++ b/test/test_models/test_water.py @@ -97,7 +97,7 @@ def test_update_water_draw(self): self.assertLess(result[0], 0) # Tempered water draw - low setpoint - self.model.tempered_draw_temp = 40 + self.model.fixture_draw_temp = 40 result = self.model.update_water_draw(update_args_tempered_draw) self.assertLess(self.model.draw_total, 1) self.assertAlmostEqual(self.model.h_delivered, 2091.5, places=0) @@ -106,7 +106,7 @@ def test_update_water_draw(self): # Tempered water draw - high setpoint self.model.states[0] = 55 # reset state - self.model.tempered_draw_temp = 60 + self.model.fixture_draw_temp = 60 self.model.update_water_draw(update_args_tempered_draw) self.assertEqual(self.model.draw_total, 1) self.assertAlmostEqual(self.model.h_unmet_load, 348.6, places=1) From deeec2f2be61a90c5d1ca8d3bd22fdbf30dab58a Mon Sep 17 00:00:00 2001 From: jmaguire1 Date: Fri, 6 Sep 2024 16:08:12 -0600 Subject: [PATCH 10/12] Revert "set default tempering valve to off" This reverts commit f099d2a5c3f99ccf8e97039fec2ffb18504df43f. We need the tempering valve for the time being. We can more formally integrate this later, but since in our test cases WH setpoint is 140F with a tempering valve set to 125 F this is necessary for the current runs. --- ochre/Models/Water.py | 47 ++++++++++++++++++++-------------- test/test_models/test_water.py | 4 +-- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/ochre/Models/Water.py b/ochre/Models/Water.py index 5325369..dc7098d 100644 --- a/ochre/Models/Water.py +++ b/ochre/Models/Water.py @@ -72,12 +72,10 @@ def __init__(self, water_nodes=12, water_vol_fractions=None, **kwargs): self.mains_temp = 0 # water mains temperature, in C self.outlet_temp = 0 # temperature of outlet water, in C - # target draw temperature setpoint for fixtures - Sink/Shower/Bath (SSB) - self.fixture_draw_temp = kwargs.get('Mixed Delivery Temperature (C)', convert(105, 'degF', 'degC')) - - # target draw temperature for CW and DW - # use WH tempering valve setpoint or set to None - self.hot_draw_temp = kwargs.get('Tempering Valve Setpoint (C)') + # mixed temperature (i.e. target temperature) setpoint for fixtures - Sink/Shower/Bath (SSB) + self.tempered_draw_temp = kwargs.get('Mixed Delivery Temperature (C)', convert(105, 'degF', 'degC')) + self.hot_draw_temp = kwargs.get('Tempering Valve Setpoint (C)', convert(125, 'degF', 'degC')) + self.setpoint_temp = kwargs.get('Setpoint Temperature (C)', convert(125, 'degF', 'degC')) # Removing target temperature for clothes washers # self.washer_draw_temp = kwargs.get('Clothes Washer Delivery Temperature (C)', convert(92.5, 'degF', 'degC')) @@ -152,18 +150,29 @@ def update_water_draw(self): if self.mains_temp is None: raise ModelException('Mains temperature required when water draw exists') - # calculate total draw volume from tempered and hot draw volume(s) - # for tempered draws, assume outlet temperature == T_WH1, slightly off if the water draw is very large - if self.outlet_temp > self.fixture_draw_temp: - vol_ratio = (self.fixture_draw_temp - self.mains_temp) / (self.outlet_temp - self.mains_temp) - draw_tempered *= vol_ratio - # draw_showers *= vol_ratio # no impact on model or results - - if self.hot_draw_temp is not None and self.outlet_temp > self.hot_draw_temp: - vol_ratio = (self.hot_draw_temp - self.mains_temp) / (self.outlet_temp - self.mains_temp) - draw_hot *= vol_ratio - - self.draw_total = draw_tempered + draw_hot + # calculate total draw volume from tempered draw volume(s) + # for tempered draw, assume outlet temperature == T1, slightly off if the water draw is very large + if self.tempered_draw_temp < self.setpoint_temp: + if self.outlet_temp <= self.hot_draw_temp: + self.draw_total = draw_hot + else: + vol_ratio_hot = (self.hot_draw_temp - self.mains_temp) / (self.outlet_temp - self.mains_temp) + self.draw_total += draw_hot * vol_ratio_hot + else: + self.draw_total = draw_hot + + if draw_tempered: + if self.outlet_temp <= self.tempered_draw_temp: + self.draw_total += draw_tempered + else: + vol_ratio = (self.tempered_draw_temp - self.mains_temp) / (self.outlet_temp - self.mains_temp) + self.draw_total += draw_tempered * vol_ratio + # if draw_cw: + # if self.outlet_temp <= self.washer_draw_temp: + # self.draw_total += draw_cw + # else: + # vol_ratio = (self.washer_draw_temp - self.mains_temp) / (self.outlet_temp - self.mains_temp) + # self.draw_total += draw_cw * vol_ratio t_s = self.time_res.total_seconds() draw_liters = self.draw_total * t_s / 60 # in liters @@ -219,7 +228,7 @@ def update_water_draw(self): # calculate unmet loads, showers only, in W self.h_unmet_load = max( - draw_showers / 60 * water_c * (self.fixture_draw_temp - self.outlet_temp), 0 + draw_showers / 60 * water_c * (self.tempered_draw_temp - self.outlet_temp), 0 ) # in W return heats_to_model diff --git a/test/test_models/test_water.py b/test/test_models/test_water.py index 9d32001..34fa45b 100644 --- a/test/test_models/test_water.py +++ b/test/test_models/test_water.py @@ -97,7 +97,7 @@ def test_update_water_draw(self): self.assertLess(result[0], 0) # Tempered water draw - low setpoint - self.model.fixture_draw_temp = 40 + self.model.tempered_draw_temp = 40 result = self.model.update_water_draw(update_args_tempered_draw) self.assertLess(self.model.draw_total, 1) self.assertAlmostEqual(self.model.h_delivered, 2091.5, places=0) @@ -106,7 +106,7 @@ def test_update_water_draw(self): # Tempered water draw - high setpoint self.model.states[0] = 55 # reset state - self.model.fixture_draw_temp = 60 + self.model.tempered_draw_temp = 60 self.model.update_water_draw(update_args_tempered_draw) self.assertEqual(self.model.draw_total, 1) self.assertAlmostEqual(self.model.h_unmet_load, 348.6, places=1) From aeb1c721c0d539e5319ea07b1b5be124b49b5037 Mon Sep 17 00:00:00 2001 From: jmaguire1 Date: Fri, 6 Sep 2024 17:13:11 -0600 Subject: [PATCH 11/12] One more tempering valve fix --- ochre/Models/Water.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ochre/Models/Water.py b/ochre/Models/Water.py index dc7098d..d7c79db 100644 --- a/ochre/Models/Water.py +++ b/ochre/Models/Water.py @@ -157,7 +157,7 @@ def update_water_draw(self): self.draw_total = draw_hot else: vol_ratio_hot = (self.hot_draw_temp - self.mains_temp) / (self.outlet_temp - self.mains_temp) - self.draw_total += draw_hot * vol_ratio_hot + self.draw_total = draw_hot * vol_ratio_hot else: self.draw_total = draw_hot From 41cfdc3d9ee9ccec4539b4c4bc0a78f82db4056a Mon Sep 17 00:00:00 2001 From: jmaguire1 Date: Fri, 6 Sep 2024 17:32:36 -0600 Subject: [PATCH 12/12] default the tempering valve to t_set --- ochre/utils/hpxml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ochre/utils/hpxml.py b/ochre/utils/hpxml.py index 41d710a..f9a0b55 100644 --- a/ochre/utils/hpxml.py +++ b/ochre/utils/hpxml.py @@ -1038,7 +1038,7 @@ def parse_water_heater(water_heater, water, construction, solar_fraction=0): "Fuel": water_heater["FuelType"].capitalize(), "Zone": parse_zone_name(water_heater["Location"]), "Setpoint Temperature (C)": t_set, - "Tempering Valve Setpoint (C)": None, + "Tempering Valve Setpoint (C)": t_set, # 'Heat Transfer Coefficient (W/m^2/K)': u, "UA (W/K)": convert(ua, "Btu/hour/degR", "W/K"), "Efficiency (-)": eta_c,