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 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
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
18 changes: 15 additions & 3 deletions ochre/Equipment/WaterHeater.py
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ 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.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:
Expand All @@ -441,8 +442,13 @@ def __init__(self, hp_only_mode=False, water_nodes=12, **kwargs):

# 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 +568,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
41 changes: 28 additions & 13 deletions ochre/utils/hpxml.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (-)']
Expand Down Expand Up @@ -1033,16 +1034,17 @@ def parse_water_heater(water_heater, water, construction, solar_fraction=0):
' Double check water heater inputs.')

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'),
"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)": t_set,
# '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,
"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')
Expand All @@ -1051,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
Expand Down Expand Up @@ -1103,8 +1119,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