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

120V HPWH #134

Draft
wants to merge 13 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 8 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
6 changes: 4 additions & 2 deletions bin/run_dwelling.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
51 changes: 37 additions & 14 deletions ochre/Equipment/WaterHeater.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = kwargs.get('Low Power HPWH', False)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think it'd be better to move these parameters into the hpxml.py file. I can
start to work on that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agreed, when we have HPXML support for 120V HPWHs, we'll get rid of this and a few other things that are just for testing purposes.


# 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)
Expand All @@ -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])
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you double check this? Looks like you copied the existing capacity values
to the normal HPWH, but the existing COP values to the low power HPWH.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good call, but this is actually up to date (ie different) in the latest version?

self.cop_coeff = np.array([1.0132, .0436, 0.0000117, -0.01113, 0.00003688, -0.000498])


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
Expand Down Expand Up @@ -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
Expand Down
24 changes: 19 additions & 5 deletions ochre/Models/Water.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
Expand Down Expand Up @@ -73,6 +74,8 @@ 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'))

Expand Down Expand Up @@ -131,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)
Expand All @@ -148,7 +152,15 @@ 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
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
Expand Down Expand Up @@ -214,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

Expand Down
20 changes: 16 additions & 4 deletions ochre/utils/hpxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -932,6 +932,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:
Expand Down Expand Up @@ -959,6 +960,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:
Expand Down Expand Up @@ -1014,6 +1018,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:
Expand All @@ -1031,18 +1036,26 @@ 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)': 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,
'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')
Expand Down Expand Up @@ -1103,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

Expand Down
123 changes: 67 additions & 56 deletions ochre/utils/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand Down Expand Up @@ -284,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

Expand Down Expand Up @@ -408,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
Expand Down