Skip to content

Commit d0aed03

Browse files
author
Samuel Letellier-Duchesne
authored
Different approach to calculating EndUse Balance is introduced (#324)
* Uses the "Zone Predicted Sensible Load to Setpoint Heat Transfer Rate" to determine if zone is in cooling or in heating * Cleans up required outputs * Uses Predicted load to Temp to figure out if coolinf or heating is happening * remove mech_ventilation
1 parent a1f1a72 commit d0aed03

File tree

3 files changed

+111
-124
lines changed

3 files changed

+111
-124
lines changed

archetypal/idfclass/end_use_balance.py

Lines changed: 107 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010

1111
class EndUseBalance:
12+
HVAC_MODE = ("Zone Predicted Sensible Load to Setpoint Heat Transfer Rate",)
1213
HVAC_INPUT_SENSIBLE = ( # not multiplied by zone or group multipliers
1314
"Zone Air Heat Balance System Air Transfer Rate",
1415
"Zone Air Heat Balance System Convective Heat Gain Rate",
@@ -34,52 +35,36 @@ class EndUseBalance:
3435
"Zone Hot Water Equipment Convective Heating Energy",
3536
"Zone Other Equipment Convective Heating Energy",
3637
)
37-
PEOPLE_GAIN = ("Zone People Sensible Heating Energy",) # checked, Todo: +latent
38+
PEOPLE_GAIN = ("Zone People Total Heating Energy",) # checked
3839
SOLAR_GAIN = ("Zone Windows Total Transmitted Solar Radiation Energy",) # checked
3940
INFIL_GAIN = (
40-
"Zone Infiltration Sensible Heat Gain Energy", # checked
41-
# "Zone Infiltration Latent Heat Gain Energy",
42-
"AFN Zone Infiltration Sensible Heat Gain Energy",
43-
# "AFN Zone Infiltration Latent Heat Gain Energy",
41+
"Zone Infiltration Total Heat Gain Energy", # checked
42+
"AFN Zone Infiltration Total Heat Gain Energy",
4443
)
4544
INFIL_LOSS = (
46-
"Zone Infiltration Sensible Heat Loss Energy", # checked
47-
# "Zone Infiltration Latent Heat Loss Energy",
48-
"AFN Zone Infiltration Sensible Heat Loss Energy",
49-
# "AFN Zone Infiltration Latent Heat Loss Energy",
45+
"Zone Infiltration Total Heat Loss Energy", # checked
46+
"AFN Zone Infiltration Total Heat Loss Energy",
5047
)
51-
VENTILATION_LOSS = ("Zone Air System Sensible Heating Energy",)
52-
VENTILATION_GAIN = ("Zone Air System Sensible Cooling Energy",)
48+
VENTILATION_LOSS = ("Zone Air System Total Heating Energy",)
49+
VENTILATION_GAIN = ("Zone Air System Total Cooling Energy",)
5350
NAT_VENT_GAIN = (
54-
# "Zone Ventilation Total Heat Gain Energy",
55-
"Zone Ventilation Sensible Heat Gain Energy",
56-
# "Zone Ventilation Latent Heat Gain Energy",
57-
"AFN Zone Ventilation Sensible Heat Gain Energy",
58-
# "AFN Zone Ventilation Latent Heat Gain Energy",
51+
"Zone Ventilation Total Heat Gain Energy",
52+
"AFN Zone Ventilation Total Heat Gain Energy",
5953
)
6054
NAT_VENT_LOSS = (
61-
# "Zone Ventilation Total Heat Loss Energy",
62-
"Zone Ventilation Sensible Heat Loss Energy",
63-
# "Zone Ventilation Latent Heat Loss Energy",
64-
"AFN Zone Ventilation Sensible Heat Loss Energy",
65-
# "AFN Zone Ventilation Latent Heat Loss Energy",
66-
)
67-
MECHANICAL_VENT_LOSS = (
68-
"Zone Mechanical Ventilation No Load Heat Removal Energy",
69-
"Zone Mechanical Ventilation Heating Load Increase Energy",
70-
"Zone Mechanical Ventilation Cooling Load Decrease Energy",
71-
)
72-
MECHANICAL_VENT_GAIN = (
73-
"Zone Mechanical Ventilation No Load Heat Addition Energy",
74-
"Zone Mechanical Ventilation Heating Load Decrease Energy",
75-
"Zone Mechanical Ventilation Cooling Load Increase Energy",
55+
"Zone Ventilation Total Heat Loss Energy",
56+
"AFN Zone Ventilation Total Heat Loss Energy",
7657
)
7758
OPAQUE_ENERGY_FLOW = ("Surface Outside Face Conduction Heat Transfer Energy",)
7859
OPAQUE_ENERGY_STORAGE = ("Surface Heat Storage Energy",)
7960
WINDOW_LOSS = ("Zone Windows Total Heat Loss Energy",) # checked
8061
WINDOW_GAIN = ("Zone Windows Total Heat Gain Energy",) # checked
81-
HEAT_RECOVERY_LOSS = ("Heat Exchanger Total Cooling Energy",)
82-
HEAT_RECOVERY_GAIN = ("Heat Exchanger Total Heating Energy",)
62+
HRV_LOSS = ("Heat Exchanger Total Cooling Energy",)
63+
HRV_GAIN = ("Heat Exchanger Total Heating Energy",)
64+
AIR_SYSTEM = (
65+
"Air System Heating Coil Total Heating Energy",
66+
"Air System Cooling Coil Total Cooling Energy",
67+
)
8368

8469
def __init__(
8570
self,
@@ -100,6 +85,7 @@ def __init__(
10085
opaque_storage,
10186
window_flow,
10287
heat_recovery,
88+
air_system,
10389
is_cooling,
10490
is_heating,
10591
units="J",
@@ -122,6 +108,7 @@ def __init__(
122108
self.opaque_storage = opaque_storage
123109
self.window_flow = window_flow
124110
self.heat_recovery = heat_recovery
111+
self.air_system = air_system
125112
self.units = units
126113
self.use_all_solar = use_all_solar
127114
self.is_cooling = is_cooling
@@ -168,15 +155,15 @@ def from_sql_file(
168155
axis=1,
169156
verify_integrity=True,
170157
)
171-
172-
rolling_sign = cls.get_rolling_sign_change(_hvac_input)
158+
mode = sql.timeseries_by_name(cls.HVAC_MODE) # positive = Heating
159+
rolling_sign = cls.get_rolling_sign_change(mode).fillna(0)
173160

174161
# Create both heating and cooling masks
175-
is_heating = rolling_sign > 0
176-
is_cooling = rolling_sign < 0
162+
is_heating = rolling_sign.droplevel(["IndexGroup", "Name"], axis=1) == 1
163+
is_cooling = rolling_sign.droplevel(["IndexGroup", "Name"], axis=1) == -1
177164

178-
heating = _hvac_input[is_heating].fillna(0)
179-
cooling = _hvac_input[is_cooling].fillna(0)
165+
heating = _hvac_input.mul(is_heating, level="KeyValue", axis=1)
166+
cooling = _hvac_input.mul(is_cooling, level="KeyValue", axis=1)
180167

181168
lighting = sql.timeseries_by_name(cls.LIGHTING).to_units(units)
182169
zone_multipliers = sql.zone_info.set_index("ZoneName")["Multiplier"].rename(
@@ -204,23 +191,10 @@ def from_sql_file(
204191
nat_vent_gain = cls.apply_multipliers(nat_vent_gain, zone_multipliers)
205192
nat_vent_loss = sql.timeseries_by_name(cls.NAT_VENT_LOSS).to_units(units)
206193
nat_vent_loss = cls.apply_multipliers(nat_vent_loss, zone_multipliers)
207-
mech_vent_gain = sql.timeseries_by_name(cls.MECHANICAL_VENT_GAIN).to_units(
208-
units
209-
)
210-
mech_vent_gain = cls.apply_multipliers(mech_vent_gain, zone_multipliers)
211-
mech_vent_loss = sql.timeseries_by_name(cls.MECHANICAL_VENT_LOSS).to_units(
212-
units
213-
)
214-
mech_vent_loss = cls.apply_multipliers(mech_vent_loss, zone_multipliers)
215-
heat_recovery_loss = sql.timeseries_by_name(cls.HEAT_RECOVERY_LOSS).to_units(
216-
units
217-
)
218-
heat_recovery_gain = sql.timeseries_by_name(cls.HEAT_RECOVERY_GAIN).to_units(
219-
units
220-
)
221-
heat_recovery = cls.subtract_loss_from_gain(
222-
heat_recovery_gain, heat_recovery_loss, level="KeyValue"
223-
)
194+
hrv_loss = sql.timeseries_by_name(cls.HRV_LOSS).to_units(units)
195+
hrv_gain = sql.timeseries_by_name(cls.HRV_GAIN).to_units(units)
196+
hrv = cls.subtract_loss_from_gain(hrv_gain, hrv_loss, level="KeyValue")
197+
air_system = sql.timeseries_by_name(cls.AIR_SYSTEM).to_units(units)
224198

225199
# subtract losses from gains
226200
infiltration = None
@@ -230,10 +204,6 @@ def from_sql_file(
230204
infiltration = cls.subtract_loss_from_gain(
231205
infil_gain, infil_loss, level="Name"
232206
)
233-
if not any((vent_gain.empty, vent_loss.empty, cooling.empty, heating.empty)):
234-
mech_vent = cls.subtract_loss_from_gain(
235-
mech_vent_gain, mech_vent_loss, level="Name"
236-
)
237207
if nat_vent_gain.shape == nat_vent_loss.shape:
238208
nat_vent = cls.subtract_loss_from_gain(
239209
nat_vent_gain, nat_vent_loss, level="Name"
@@ -296,7 +266,8 @@ def from_sql_file(
296266
opaque_flow,
297267
opaque_storage,
298268
window_flow,
299-
heat_recovery,
269+
hrv,
270+
air_system,
300271
is_cooling,
301272
is_heating,
302273
units,
@@ -492,9 +463,7 @@ def subtract_vent_from_system(cls, system, vent, level="Key_Name"):
492463
index=system.index,
493464
)
494465

495-
def separate_gains_and_losses(
496-
self, component, level="Key_Name", stack_on_level=None
497-
) -> EnergyDataFrame:
466+
def separate_gains_and_losses(self, component, level="Key_Name") -> EnergyDataFrame:
498467
"""Separate gains from losses when cooling and heating occurs for the component.
499468
500469
Args:
@@ -509,38 +478,31 @@ def separate_gains_and_losses(
509478
), f"{component} is not a valid attribute of EndUseBalance."
510479
component_df = getattr(self, component)
511480
assert not component_df.empty, "Expected a component that is not empty."
512-
if isinstance(level, str):
513-
level = [level]
514481
print(component)
515482

516-
# mask when cooling occurs in zone (negative values)
517-
mask = (self.is_cooling.stack("KeyValue") == True).any(axis=1)
518-
519-
# get the dataframe using the attribute name, summarize by `level` and stack so that a Series is returned.
520-
stacked = getattr(self, component).sum(level=level, axis=1).stack(level[0])
483+
c_df = getattr(self, component)
521484

522-
# concatenate the masked values with keys to easily create a MultiIndex when unstacking
485+
# concatenate the Periods
523486
inter = pd.concat(
524487
[
525-
stacked[mask].reindex(stacked.index),
526-
stacked[~mask].reindex(stacked.index),
488+
c_df.mul(self.is_cooling.rename_axis(level, axis=1), level=level),
489+
c_df.mul(self.is_heating.rename_axis(level, axis=1), level=level),
527490
],
528491
keys=["Cooling Periods", "Heating Periods"],
529-
names=["Period"]
530-
+ stacked.index.names, # prepend the new key name to the existing index names.
492+
names=["Period"],
531493
)
532494

533495
# mask when values are positive (gain)
534496
positive_mask = inter >= 0
535497

536-
# concatenate the masked values with keys to easily create a MultiIndex when unstacking
498+
# concatenate the Gain/Loss
537499
final = pd.concat(
538500
[
539501
inter[positive_mask].reindex(inter.index),
540502
inter[~positive_mask].reindex(inter.index),
541503
],
542504
keys=["Heat Gain", "Heat Loss"],
543-
names=["Gain/Loss"] + inter.index.names,
505+
names=["Gain/Loss"],
544506
).unstack(["Period", "Gain/Loss"])
545507
final.sort_index(axis=1, inplace=True)
546508
return final
@@ -560,40 +522,24 @@ def to_df(self, separate_gains_and_losses=False, level="KeyValue"):
560522
"infiltration",
561523
"window_energy_flow",
562524
"nat_vent",
563-
"mech_vent",
564525
]:
565526
if not getattr(self, component).empty:
566527
summary_by_component[component] = (
567528
self.separate_gains_and_losses(
568529
component,
569530
level=level,
570531
)
571-
.unstack(level)
572-
.reorder_levels([level, "Period", "Gain/Loss"], axis=1)
532+
.groupby(level=["KeyValue", "Period", "Gain/Loss"], axis=1)
533+
.sum()
573534
.sort_index(axis=1)
574535
)
575-
for (surface_type), data in (
576-
self.separate_gains_and_losses(
577-
"opaque_flow", ["Zone_Name", "Surface_Type"]
578-
)
579-
.unstack("Zone_Name")
580-
.groupby(level=["Surface_Type"], axis=1)
581-
):
536+
for (surface_type), data in self.separate_gains_and_losses(
537+
"opaque_flow", level="Zone_Name"
538+
).groupby(level=["Surface_Type"], axis=1):
582539
summary_by_component[surface_type] = data.sum(
583540
level=["Zone_Name", "Period", "Gain/Loss"], axis=1
584541
).sort_index(axis=1)
585542

586-
# for (surface_type), data in (
587-
# self.separate_gains_and_losses(
588-
# "opaque_storage", ["Zone_Name", "Surface_Type"]
589-
# )
590-
# .unstack("Zone_Name")
591-
# .groupby(level=["Surface_Type"], axis=1)
592-
# ):
593-
# summary_by_component[surface_type + " Storage"] = data.sum(
594-
# level=["Zone_Name", "Period", "Gain/Loss"], axis=1
595-
# ).sort_index(axis=1)
596-
597543
else:
598544
summary_by_component = {}
599545
for component in [
@@ -606,20 +552,40 @@ def to_df(self, separate_gains_and_losses=False, level="KeyValue"):
606552
"infiltration",
607553
"window_energy_flow",
608554
"nat_vent",
609-
"mech_vent",
610555
]:
611556
component_df = getattr(self, component)
612557
if not component_df.empty:
613558
summary_by_component[component] = component_df.sum(
614559
level=level, axis=1
615560
).sort_index(axis=1)
616-
for (zone_name, surface_type), data in self.face_energy_flow.groupby(
561+
for (zone_name, surface_type), data in self.opaque_flow.groupby(
617562
level=["Zone_Name", "Surface_Type"], axis=1
618563
):
619564
summary_by_component[surface_type] = data.sum(
620565
level="Zone_Name", axis=1
621566
).sort_index(axis=1)
622567
levels = ["Component", "Zone_Name"]
568+
569+
# Add contribution of heating/cooling outside air, if any
570+
if not self.air_system.empty:
571+
summary_by_component["air_system_heating"] = (
572+
self.air_system.xs(
573+
"Air System Heating Coil Total Heating Energy", level="Name", axis=1
574+
)
575+
.assign(**{"Period": "Heating Periods", "Gain/Loss": "Heat Loss"})
576+
.set_index(["Period", "Gain/Loss"], append=True)
577+
.unstack(["Period", "Gain/Loss"])
578+
.droplevel("IndexGroup", axis=1)
579+
)
580+
summary_by_component["air_system_cooling"] = (
581+
self.air_system.xs(
582+
"Air System Cooling Coil Total Cooling Energy", level="Name", axis=1
583+
)
584+
.assign(**{"Period": "Cooling Periods", "Gain/Loss": "Heat Gain"})
585+
.set_index(["Period", "Gain/Loss"], append=True)
586+
.unstack(["Period", "Gain/Loss"])
587+
.droplevel("IndexGroup", axis=1)
588+
)
623589
return pd.concat(
624590
summary_by_component, axis=1, verify_integrity=True, names=levels
625591
)
@@ -693,6 +659,9 @@ def to_sankey(self, path_or_buf):
693659
"interior_equipment": "Equipment",
694660
"window_energy_flow": "Windows",
695661
"Wall": "Walls",
662+
"air_system_heating": "OA Heating",
663+
"air_system_cooling": "OA Cooling",
664+
"cooling": "Cooling",
696665
},
697666
inplace=True,
698667
)
@@ -752,9 +721,9 @@ def to_sankey(self, path_or_buf):
752721
)
753722

754723
system_input = (
755-
system_input
756-
# .replace({0: np.NaN})
757-
.dropna(how="all").dropna(how="all", axis=1)
724+
system_input.replace({0: np.NaN})
725+
.dropna(how="all")
726+
.dropna(how="all", axis=1)
758727
)
759728
system_input.rename_axis("source", axis=1, inplace=True)
760729
system_input.rename_axis("target", axis=0, inplace=True)
@@ -789,21 +758,41 @@ def to_sankey(self, path_or_buf):
789758
link_cooling_system_to_gains,
790759
) = self._sankey_cooling(cooling_load, load_type="cooling")
791760

792-
return (
793-
pd.DataFrame(
794-
system_input_data
795-
+ link_heating_system_to_gains
796-
+ heating_energy_to_heating_system
797-
+ heating_load_source_data
798-
+ heating_load_target_data
799-
+ cooling_energy_to_heating_system
800-
+ cooling_load_source_data
801-
+ cooling_load_target_data
802-
+ link_cooling_system_to_gains
803-
)
804-
# .div(floor_area)
805-
.to_csv(path_or_buf, index=False)
761+
flows = pd.DataFrame(
762+
system_input_data
763+
+ heating_energy_to_heating_system
764+
+ heating_load_source_data
765+
+ heating_load_target_data
766+
+ cooling_energy_to_heating_system
767+
+ cooling_load_source_data
768+
+ cooling_load_target_data
769+
)
770+
# Fix the HVAC difference
771+
diff = (
772+
flows.loc[flows.source == "Heating Load", "value"].sum()
773+
- flows.loc[flows.target == "Heating Load", "value"].sum()
774+
)
775+
flows.loc[flows.source == "Heating System", "value"] = (
776+
flows.loc[flows.source == "Heating System", "value"] + diff
777+
)
778+
779+
diff = (
780+
flows.loc[flows.source == "Cooling Load", "value"].sum()
781+
- flows.loc[flows.target == "Cooling Load", "value"].sum()
782+
)
783+
flows.loc[flows.source == "Cooling System", "value"] = (
784+
flows.loc[flows.source == "Cooling System", "value"] + diff
785+
)
786+
787+
# fix names
788+
flows.replace({"OA Heating Heat Losses": "OA Heating"}, inplace=True)
789+
790+
# TO EUI
791+
flows["value"] = flows["value"] / floor_area
792+
links = pd.DataFrame(
793+
link_heating_system_to_gains + link_cooling_system_to_gains
806794
)
795+
return pd.concat([flows, links]).to_csv(path_or_buf, index=False)
807796

808797
def _sankey_heating(self, load, load_type="heating"):
809798
assert load_type in ["heating", "cooling"]

0 commit comments

Comments
 (0)