diff --git a/epinterface/analysis/zone_assignment_viz.py b/epinterface/analysis/zone_assignment_viz.py new file mode 100644 index 0000000..6b4f6ab --- /dev/null +++ b/epinterface/analysis/zone_assignment_viz.py @@ -0,0 +1,161 @@ +"""matplotlib plots comparing resolved zone-assignment scalars and sim energy.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Literal + +import pandas as pd + +if TYPE_CHECKING: + from epinterface.sbem.builder import ModelRunResults + +DEFAULT_ASSIGNMENT_METRICS: tuple[str, ...] = ( + "LightingPowerDensity", + "EquipmentPowerDensity", + "FacadeRValue", +) + +DEFAULT_SIM_ENERGY_METRICS: tuple[str, ...] = ( + "sim_lighting_kwh_per_m2", + "sim_equipment_kwh_per_m2", + "sim_heating_kwh_per_m2", + "sim_cooling_kwh_per_m2", + "sim_total_kwh_per_m2", +) + + +def _import_plt(): + try: + import matplotlib.pyplot as plt + from matplotlib.axes import Axes + from matplotlib.figure import Figure + except ImportError as exc: + msg = "matplotlib is required for plotting (install dev deps: uv sync --group dev)" + raise ImportError(msg) from exc + return plt, Axes, Figure + + +def _plot_label_column( + df: pd.DataFrame, groupby: Literal["zone", "storey_role"] +) -> pd.Series: + if groupby == "zone": + return df["ep_zone_name"].astype(str) + return "S" + df["storey_index"].astype(str) + " / " + df["role"].astype(str) + + +def _horizontal_metric_bars( + df: pd.DataFrame, + metrics: tuple[str, ...], + *, + groupby: Literal["zone", "storey_role"], + title_prefix: str = "", + ax=None, +): + plt, Axes, Figure = _import_plt() + plot_df = df.copy() + plot_df["_plot_label"] = _plot_label_column(plot_df, groupby) + + present = [m for m in metrics if m in plot_df.columns] + if not present: + msg = f"none of the metrics {metrics!r} appear in columns {list(plot_df.columns)!r}" + raise ValueError(msg) + + roles = ( + sorted(plot_df["role"].astype(str).unique()) + if "role" in plot_df.columns + else ["all"] + ) + cmap = plt.colormaps["tab10"] + role_color = {r: cmap(i % 10) for i, r in enumerate(roles)} + + if ax is not None: + if len(present) > 1: + msg = "pass a single metric when ax is provided" + raise ValueError(msg) + metric = present[0] + if not isinstance(ax, Axes): + msg = "ax must be a matplotlib Axes when provided" + raise TypeError(msg) + sub = plot_df.sort_values(["storey_index", "role", "_plot_label"]) + colors = [role_color.get(str(r), cmap(0)) for r in sub.get("role", sub.index)] + ax.barh(sub["_plot_label"], sub[metric], color=colors) + ax.set_xlabel(metric) + ax.set_title(f"{title_prefix}{metric}".strip()) + return ax.figure + + fig_h = max(3.0, 2.0 * len(present)) + fig, axes = plt.subplots(len(present), 1, figsize=(9, fig_h), squeeze=False) + for axi, metric in zip(axes.flatten(), present, strict=True): + sub = plot_df.sort_values(["storey_index", "role", "_plot_label"]) + colors = [role_color.get(str(r), cmap(0)) for r in sub.get("role", sub.index)] + axi.barh(sub["_plot_label"], sub[metric], color=colors) + axi.set_xlabel(metric) + axi.set_title(f"{title_prefix}{metric}".strip()) + fig.tight_layout() + if not isinstance(fig, Figure): + msg = "expected matplotlib Figure" + raise TypeError(msg) + return fig + + +def plot_zone_comparison( + summary: pd.DataFrame, + metrics: tuple[str, ...] = DEFAULT_ASSIGNMENT_METRICS, + *, + groupby: Literal["zone", "storey_role"] = "zone", + ax=None, +): + """Horizontal bars for resolved assignment scalars (inputs).""" + return _horizontal_metric_bars(summary, metrics, groupby=groupby, ax=ax) + + +def plot_zone_energy_comparison( + summary: pd.DataFrame, + metrics: tuple[str, ...] = DEFAULT_SIM_ENERGY_METRICS, + *, + groupby: Literal["zone", "storey_role"] = "zone", + ax=None, + results: ModelRunResults | None = None, +): + """Horizontal bars of simulated annual site energy (kWh/m2) per zone.""" + if results is not None: + from epinterface.analysis.zone_energy import ( + merge_assignment_and_energy, + zone_energy_summary, + ) + + energy = zone_energy_summary(results.sql, results.idf) + summary = merge_assignment_and_energy(summary, energy) + present = tuple(m for m in metrics if m in summary.columns) + return _horizontal_metric_bars( + summary, + present or DEFAULT_SIM_ENERGY_METRICS, + groupby=groupby, + title_prefix="sim: ", + ax=ax, + ) + + +def plot_assignment_and_energy( + summary: pd.DataFrame, + *, + results: ModelRunResults, + groupby: Literal["zone", "storey_role"] = "zone", +): + """Return (assignment_figure, energy_figure, merged_summary).""" + from epinterface.analysis.zone_energy import ( + merge_assignment_and_energy, + zone_energy_summary, + ) + + energy = zone_energy_summary(results.sql, results.idf) + merged = merge_assignment_and_energy(summary, energy) + assign_metrics = tuple(m for m in DEFAULT_ASSIGNMENT_METRICS if m in merged.columns) + sim_metrics = tuple(m for m in DEFAULT_SIM_ENERGY_METRICS if m in merged.columns) + assign_fig = _horizontal_metric_bars( + merged, assign_metrics, groupby=groupby, title_prefix="assigned: " + ) + energy_fig = _horizontal_metric_bars( + merged, sim_metrics, groupby=groupby, title_prefix="sim: " + ) + return assign_fig, energy_fig, merged diff --git a/epinterface/analysis/zone_energy.py b/epinterface/analysis/zone_energy.py new file mode 100644 index 0000000..763b363 --- /dev/null +++ b/epinterface/analysis/zone_energy.py @@ -0,0 +1,154 @@ +"""Per-zone annual energy from EnergyPlus SQL (post-simulation).""" + +from __future__ import annotations + +import pandas as pd +from archetypal.idfclass import IDF +from archetypal.idfclass.sql import Sql + +from epinterface.geometry import get_zone_floor_area + +J_to_kWh = 1.0 / 3_600_000.0 +GJ_to_kWh = 277.778 + +_ZONE_ENERGY_VARS: tuple[tuple[str, str], ...] = ( + ("Zone Lights Electricity Energy", "sim_lighting_kwh_per_m2"), + ("Zone Electric Equipment Electricity Energy", "sim_equipment_kwh_per_m2"), + ("Zone Ideal Loads Zone Total Heating Energy", "sim_heating_kwh_per_m2"), + ("Zone Ideal Loads Zone Total Cooling Energy", "sim_cooling_kwh_per_m2"), +) + + +def _norm_zone_key(name: str) -> str: + return name.replace("_", " ").upper() + + +def _zone_name_lookup(zone_names: list[str]) -> dict[str, str]: + return {_norm_zone_key(zn): zn for zn in zone_names} + + +def _match_zone_name(key_value: str, zone_lookup: dict[str, str]) -> str | None: + """Map SQL KeyValue (zone or ideal-loads system name) to EP zone name.""" + kv = key_value.strip() + if kv in zone_lookup.values(): + return kv + nkv = _norm_zone_key(kv) + if nkv in zone_lookup: + return zone_lookup[nkv] + suffix = " IDEAL LOADS AIR SYSTEM" + if nkv.endswith(suffix): + candidate = nkv[: -len(suffix)] + if candidate in zone_lookup: + return zone_lookup[candidate] + return None + + +def _annual_kwh_per_m2_from_hourly( + sql: Sql, variable_name: str, idf: IDF +) -> dict[str, float]: + """Sum hourly zone energy [J] and normalize by zone floor area.""" + zone_names = [z.Name for z in idf.idfobjects["ZONE"]] + zone_lookup = _zone_name_lookup(zone_names) + try: + hourly = sql.timeseries_by_name([variable_name], "Hourly") + except Exception: + return {} + if hourly.empty: + return {} + + totals: dict[str, float] = {} + if isinstance(hourly.columns, pd.MultiIndex): + for col in hourly.columns: + key_value = str(col[1]) if len(col) > 1 else str(col[0]) + zn = _match_zone_name(key_value, zone_lookup) + if zn is None: + continue + totals[zn] = totals.get(zn, 0.0) + float(hourly[col].sum()) + else: + annual_j = hourly.sum() + for key, joules in annual_j.items(): + zn = _match_zone_name(str(key), zone_lookup) + if zn is None: + continue + totals[zn] = totals.get(zn, 0.0) + float(joules) + + return { + zn: joules * J_to_kWh / float(get_zone_floor_area(idf, zn)) + for zn, joules in totals.items() + if float(get_zone_floor_area(idf, zn)) > 0 + } + + +def _lighting_kwh_per_m2_from_tabular(sql: Sql, idf: IDF) -> dict[str, float]: + """Lighting consumption from LightingSummary when hourly vars are absent.""" + zone_lookup = _zone_name_lookup([z.Name for z in idf.idfobjects["ZONE"]]) + try: + tbl = sql.tabular_data_by_name( + "LightingSummary", "Interior Lighting", "Entire Facility" + ) + except Exception: + return {} + zone_col = ("Zone Name", "") + cons_col = ("Consumption", "GJ") + area_col = ("Space Area", "m2") + if cons_col not in tbl.columns or zone_col not in tbl.columns: + return {} + out: dict[str, float] = {} + for _, row in tbl.iterrows(): + zn = row.loc[zone_col] + if not isinstance(zn, str) or not zn.strip(): + continue + cons_val = row.loc[cons_col] + if pd.isna(cons_val): + continue + gj = float(cons_val) + if area_col in tbl.columns: + area_val = row.loc[area_col] + area = 0.0 if pd.isna(area_val) else float(area_val) + else: + area = 0.0 + if area <= 0: + continue + zn_idf = zone_lookup.get(_norm_zone_key(zn.strip())) + if zn_idf is None: + continue + out[zn_idf] = gj * GJ_to_kWh / area + return out + + +def zone_energy_summary(sql: Sql, idf: IDF) -> pd.DataFrame: + """Annual simulated site energy per zone, normalized to kWh/m2.""" + zone_names = [z.Name for z in idf.idfobjects["ZONE"]] + rows: list[dict[str, float | str | None]] = [] + col_data: dict[str, dict[str, float]] = {} + + lighting = _annual_kwh_per_m2_from_hourly( + sql, "Zone Lights Electricity Energy", idf + ) + if not lighting: + lighting = _lighting_kwh_per_m2_from_tabular(sql, idf) + col_data["sim_lighting_kwh_per_m2"] = lighting + + for var_name, col_name in _ZONE_ENERGY_VARS[1:]: + col_data[col_name] = _annual_kwh_per_m2_from_hourly(sql, var_name, idf) + + for zn in zone_names: + row: dict[str, float | str | None] = {"ep_zone_name": zn} + for col_name in [c for _, c in _ZONE_ENERGY_VARS]: + row[col_name] = col_data.get(col_name, {}).get(zn) + sim_total = sum( + float(v) + for k, v in row.items() + if k.startswith("sim_") and isinstance(v, int | float) + ) + row["sim_total_kwh_per_m2"] = sim_total if sim_total else None + rows.append(row) + return pd.DataFrame(rows) + + +def merge_assignment_and_energy( + assignment: pd.DataFrame, + energy: pd.DataFrame, +) -> pd.DataFrame: + """Join resolved assignment summary with simulated zone energy.""" + return assignment.merge(energy, on="ep_zone_name", how="left") diff --git a/epinterface/sbem/builder.py b/epinterface/sbem/builder.py index 4381d4d..f4a0d00 100644 --- a/epinterface/sbem/builder.py +++ b/epinterface/sbem/builder.py @@ -56,6 +56,7 @@ from epinterface.sbem.components.zones import ZoneComponent from epinterface.sbem.exceptions import NotImplementedParameter from epinterface.sbem.prisma.client import PrismaSettings +from epinterface.sbem.zone_assignment import ZoneAssignmentTable, parse_zone_key from epinterface.settings import energyplus_settings from epinterface.weather import BaseWeather @@ -69,7 +70,19 @@ "Zone Mean Radiant Temperature", ] +ZoneEnergyHourlyVariables = Literal[ + "Zone Lights Electricity Energy", + "Zone Electric Equipment Electricity Energy", + "Zone Ideal Loads Zone Total Heating Energy", + "Zone Ideal Loads Zone Total Cooling Energy", +] + AVAILABLE_HOURLY_VARIABLES = get_args(AvailableHourlyVariables) +ZONE_ENERGY_HOURLY_VARIABLES = get_args(ZoneEnergyHourlyVariables) +ALL_HOURLY_OUTPUT_VARIABLES = ( + *AVAILABLE_HOURLY_VARIABLES, + *ZONE_ENERGY_HOURLY_VARIABLES, +) class SimulationPathConfig(BaseModel): @@ -93,6 +106,7 @@ class SurfaceHandler(BaseModel): original_surface_type: str | None surface_group: Literal["glazing", "opaque", "internal_mass"] zone_name_contains: str | None + zone_name_equals: str | None = None outside_boundary_condition_object_contains: str | None @model_validator(mode="after") @@ -143,7 +157,14 @@ def assign_constructions_to_objs( ) # Now we can find the matching idf objects that need a construction. - srfs = [srf for srf in idf.idfobjects[obj_type_key] if self.check_srf(srf)] + wall_zone_lookup = { + w.Name: w.Zone_Name for w in idf.idfobjects["BUILDINGSURFACE:DETAILED"] + } + srfs = [ + srf + for srf in idf.idfobjects[obj_type_key] + if self.check_srf(srf, wall_zone_lookup) + ] idf = construction.add_to_idf(idf) # and then we can finally assign the construction to the surfaces. @@ -151,11 +172,12 @@ def assign_constructions_to_objs( srf.Construction_Name = construction.Name return idf - def check_srf(self, srf): + def check_srf(self, srf, wall_zone_lookup: dict[str, str]): """Check if the surface matches the filters. Args: srf (eppy.IDF.BLOCK): The surface to check. + wall_zone_lookup (dict[str, str]): Building surface Name -> Zone_Name. Returns: match (bool): True if the surface matches the filters. @@ -164,7 +186,7 @@ def check_srf(self, srf): self.check_construction_type(srf) and self.check_boundary(srf) and self.check_construction_name(srf) - and self.check_zone_name(srf) + and self.check_zone_name(srf, wall_zone_lookup) and self.check_outside_boundary_condition_object(srf) ) @@ -219,16 +241,19 @@ def check_construction_name(self, srf): # Check the original construction name return srf.Construction_Name.lower() == self.original_construction_name.lower() - # TODO: convert this to a regex for better control - def check_zone_name(self, srf): - """Check that the zone name for the surface contains the expected substring.""" - if self.zone_name_contains is None: - # Ignore the zone name check when filter not provided - return True + def check_zone_name(self, srf, wall_zone_lookup: dict[str, str]): + """Check that the zone name for the surface matches filters.""" if self.surface_group == "glazing": - # Ignore the zone name check for windows + if self.zone_name_equals is None: + return True + wall_name = getattr(srf, "Building_Surface_Name", "") + zn = wall_zone_lookup.get(str(wall_name)) + return zn == self.zone_name_equals + if self.zone_name_equals is not None: + zone_name = srf.Zone_Name + return zone_name == self.zone_name_equals + if self.zone_name_contains is None: return True - # Check the zone name zone_name = srf.Zone_Name return self.zone_name_contains.lower() in zone_name.lower() @@ -530,6 +555,135 @@ def handle_envelope( ) return idf + def assign_facade_and_glazing_for_zone( + self, + idf: IDF, + facade_assembly: ConstructionAssemblyComponent, + window: GlazingConstructionSimpleComponent | None, + zone_name: str, + ) -> IDF: + """Facade plus fenestration for one thermal zone.""" + facade_handler = self.Facade.model_copy( + update={"zone_name_equals": zone_name, "zone_name_contains": None} + ) + idf = facade_handler.assign_constructions_to_objs( + idf=idf, construction=facade_assembly + ) + if window: + window_handler = self.Window.model_copy( + update={"zone_name_equals": zone_name, "zone_name_contains": None} + ) + idf = window_handler.assign_constructions_to_objs( + idf=idf, construction=window + ) + return idf + + def handle_shared_opaque_envelope( + self, + idf: IDF, + constructions: EnvelopeAssemblyComponent, + with_attic: bool, + with_basement: bool, + exposed_basement_frac: float = 0, + ) -> IDF: + """roof/partition/slab/partitions/ground/internal mass — excludes facade/windows.""" + floor_ceiling_reversed = constructions.FloorCeilingAssembly.reversed + attic_floor_reversed = constructions.AtticFloorAssembly.reversed + basement_ceiling_reversed = constructions.BasementCeilingAssembly.reversed + + outdoor_roof_bc = ( + constructions.AtticRoofAssembly + if with_attic + else constructions.FlatRoofAssembly + ) + idf = self.RoofOutdoorBC.assign_constructions_to_objs( + idf=idf, construction=outdoor_roof_bc + ) + + idf = self.Partition.assign_constructions_to_objs( + idf=idf, construction=constructions.PartitionAssembly + ) + idf = self.FloorCeilingFloor.assign_constructions_to_objs( + idf=idf, construction=floor_ceiling_reversed + ) + idf = self.FloorCeilingCeiling.assign_constructions_to_objs( + idf=idf, construction=constructions.FloorCeilingAssembly + ) + if with_basement: + idf = self.BasementCeilingCeiling.assign_constructions_to_objs( + idf=idf, construction=constructions.BasementCeilingAssembly + ) + idf = self.BasementCeilingFloor.assign_constructions_to_objs( + idf=idf, construction=basement_ceiling_reversed + ) + if with_attic: + idf = self.AtticFloorFloor.assign_constructions_to_objs( + idf=idf, construction=constructions.AtticFloorAssembly + ) + idf = self.AtticFloorCeiling.assign_constructions_to_objs( + idf=idf, construction=attic_floor_reversed + ) + + idf = self.GroundSlab.assign_constructions_to_objs( + idf=idf, construction=constructions.GroundSlabAssembly + ) + idf = self.GroundWall.assign_constructions_to_objs( + idf=idf, construction=constructions.GroundWallAssembly + ) + + if with_basement and exposed_basement_frac > 0: + basement_wall_surfaces = [ + srf + for srf in idf.idfobjects["BUILDINGSURFACE:DETAILED"] + if srf.Outside_Boundary_Condition == "ground" + and srf.Surface_Type == "wall" + ] + for srf in basement_wall_surfaces: + coords = srf.coords + z_coords = [c[2] for c in coords] + min_z = min(z_coords) + max_z = max(z_coords) + h = max_z - min_z + unexposed_height = h * (1 - exposed_basement_frac) + unexposed_height = min(max(unexposed_height, 0.15), h - 0.15) + cut_z = min_z + unexposed_height + z_coords_lower_section = [z if z == min_z else cut_z for z in z_coords] + z_coords_upper_section = [z if z == max_z else cut_z for z in z_coords] + coords_lower_section = [ + (c[0], c[1], z) + for c, z in zip(coords, z_coords_lower_section, strict=False) + ] + coords_upper_section = [ + (c[0], c[1], z) + for c, z in zip(coords, z_coords_upper_section, strict=False) + ] + new_bottom_srf = idf.copyidfobject(srf) + new_top_srf = idf.copyidfobject(srf) + new_bottom_srf.setcoords(coords_lower_section) + new_top_srf.setcoords(coords_upper_section) + new_bottom_srf.Name = f"{srf.Name}_bottom" + new_top_srf.Name = f"{srf.Name}_top" + new_top_srf.Outside_Boundary_Condition = "outdoors" + idf.removeidfobject(srf) + + if constructions.InternalMassAssembly is not None: + for zone in idf.idfobjects["ZONE"]: + floor_area = get_zone_floor_area(idf, zone.Name) * ( + constructions.InternalMassExposedAreaPerArea or 0 + ) + internal_mass = InternalMass( + Name=f"{zone.Name}_InternalMass", + Zone_or_ZoneList_Name=zone.Name, + Construction_Name=constructions.InternalMassAssembly.Name, + Surface_Area=floor_area, + ) + idf = internal_mass.add(idf) + + idf = self.InternalMass.assign_constructions_to_objs( + idf=idf, construction=constructions.InternalMassAssembly + ) + return idf + @dataclass class AddedZoneLists: @@ -567,7 +721,18 @@ class Model(BaseWeather, validate_assignment=True): Attic: AtticAssumptions Basement: BasementAssumptions # TODO: should we have another field for whether or not the attic is ventilated, i.e. high infiltration? - Zone: ZoneComponent + Zone: ZoneComponent | None = Field(default=None) + zone_assignments: ZoneAssignmentTable | None = Field(default=None) + + @model_validator(mode="after") + def validate_zone_vs_assignments(self): + """Exactly one of Zone or zone_assignments must be provided.""" + z = self.Zone is not None + a = self.zone_assignments is not None + if z == a: + msg = "Exactly one of Zone or zone_assignments must be set on Model." + raise ValueError(msg) + return self @field_validator("geometry", mode="after") @classmethod @@ -602,25 +767,20 @@ def total_conditioned_area(self) -> float: @property def total_people(self) -> float: - """The total number of people in the model. - - Returns: - ppl (float): The total number of people in the model - - """ - raise NotImplementedError( - "Total people is not yet implemented because of attics/basements etc." - ) + """Approximate occupant count from density times conditioned floor area.""" + area = self.total_conditioned_area + if self.zone_assignments is not None: + dens = self.zone_assignments.defaults.OccupantDensity + return float(dens * area) + if self.Zone is None: + msg = "Model has no Zone or zone_assignments for occupant count." + raise ValueError(msg) ppl_per_m2 = ( self.Zone.Operations.SpaceUse.Occupancy.PeopleDensity if self.Zone.Operations.SpaceUse.Occupancy.IsOn else 0 ) - total_area = self.total_conditioned_area # this is wrong - it should be based off of occupied area which may be different depending on attic/basement use fractions. - total_ppl = ppl_per_m2 * total_area - return total_ppl - - # validate the attic conditioning + return float(ppl_per_m2 * area) @model_validator(mode="after") def attic_check(self): @@ -766,14 +926,22 @@ def compute_dhw(self) -> float: energy (float): The annual domestic hot water energy demand (kWh/m2) """ # TODO: this should be computed from the DHW schedule - if not self.Zone.Operations.DHW.IsOn: + if self.zone_assignments is not None: + from epinterface.sbem.flat_model import zone_params_to_zone_component + + ops = zone_params_to_zone_component( + self.zone_assignments.defaults + ).Operations + else: + if self.Zone is None: + msg = "Model has no Zone for DHW computation." + raise ValueError(msg) + ops = self.Zone.Operations + if not ops.DHW.IsOn: return 0 - flow_rate_per_person = ( - self.Zone.Operations.SpaceUse.WaterUse.FlowRatePerPerson - ) # m3/day/person, average + flow_rate_per_person = ops.SpaceUse.WaterUse.FlowRatePerPerson temperature_rise = ( - self.Zone.Operations.DHW.WaterSupplyTemperature - - self.Zone.Operations.DHW.WaterTemperatureInlet + ops.DHW.WaterSupplyTemperature - ops.DHW.WaterTemperatureInlet ) # K water_density = physical_constants.WaterDensity_kg_per_m3 # kg/m3 c = physical_constants.WaterSpecificHeat_J_per_kg_degK # J/kg.K @@ -867,7 +1035,7 @@ def build( # noqa: C901 "Variable_Name": variable, "Reporting_Frequency": "Hourly", } - for variable in AVAILABLE_HOURLY_VARIABLES + for variable in ALL_HOURLY_OUTPUT_VARIABLES ] ) idf = IDF( @@ -885,7 +1053,7 @@ def build( # noqa: C901 if output.Key_Name not in desired_meters: idf.removeidfobject(output) for output in idf.idfobjects["OUTPUT:VARIABLE"]: - if output.Variable_Name not in AVAILABLE_HOURLY_VARIABLES: + if output.Variable_Name not in ALL_HOURLY_OUTPUT_VARIABLES: idf.removeidfobject(output) ddy = IDF( @@ -909,107 +1077,236 @@ def build( # noqa: C901 # construct zone lists idf, added_zone_lists = self.add_zone_lists(idf) - # Handle main zones - for zone in added_zone_lists.main_zone_list.Names: - self.Zone.add_to_idf_zone(idf, zone) + def zone_tag(zn: str) -> str: + return "".join(c if c.isalnum() else "_" for c in zn) - # Handle setting ground temperature - subtractor = ( - 4 if (self.geometry.basement and not self.Basement.Conditioned) else 2 - ) - has_heating = self.Zone.Operations.HVAC.ConditioningSystems.Heating is not None - has_cooling = self.Zone.Operations.HVAC.ConditioningSystems.Cooling is not None - hsp = self.Zone.Operations.SpaceUse.Thermostat.HeatingSchedule - csp = self.Zone.Operations.SpaceUse.Thermostat.CoolingSchedule - epw = EPW(epw_path.as_posix()) - epw_ground_vals_all = epw.monthly_ground_temperature - if self.geometry.basement: - # if there is a basement, we use the 2m depth to account for the basement depth. - epw_ground_vals = epw_ground_vals_all[4].values - else: - # if there is no basement, we use the 0.5m depth to account for the ground temperature. - epw_ground_vals = epw_ground_vals_all[0.5].values - low_ground_val = min(epw_ground_vals) - high_ground_val = max(epw_ground_vals) - phase = (np.array(epw_ground_vals) - low_ground_val) / ( - high_ground_val - low_ground_val - ) - if has_heating and has_cooling: - winter_line = np.array(hsp.MonthlyAverageValues) - subtractor - summer_line = np.array(csp.MonthlyAverageValues) - subtractor - elif has_heating: - winter_line = np.array(hsp.MonthlyAverageValues) - subtractor - summer_line = np.array(hsp.MonthlyAverageValues) - elif has_cooling: - winter_line = np.array(csp.MonthlyAverageValues) - subtractor - summer_line = np.array(csp.MonthlyAverageValues) - subtractor - else: - # No heating or cooling, so we use the default ground temperature, should not matter much. - winter_line = np.array(assumed_constants.SiteGroundTemperature_degC) - summer_line = np.array(assumed_constants.SiteGroundTemperature_degC) - interp_temp = phase * np.abs(summer_line - winter_line) + winter_line - ground_vals = [max(epw_ground_vals[i], interp_temp[i]) for i in range(12)] - idf = SiteGroundTemperature.FromValues(ground_vals).add(idf) - - # handle basements - if self.Basement.UseFraction or self.Basement.Conditioned: - new_zone_def = self.Zone.model_copy(deep=True) - frac = self.Basement.UseFraction or 0 - pd = new_zone_def.Operations.SpaceUse.Occupancy.PeopleDensity - epd = new_zone_def.Operations.SpaceUse.Equipment.PowerDensity - lpd = new_zone_def.Operations.SpaceUse.Lighting.PowerDensity - - new_zone_def.Operations.SpaceUse.Equipment.PowerDensity = frac * epd - new_zone_def.Operations.SpaceUse.Lighting.PowerDensity = frac * lpd - new_zone_def.Operations.SpaceUse.Occupancy.PeopleDensity = frac * pd - - # # handle infiltration for basements, which we are assuming is 0 (or whatever is in assumed constants) - new_zone_def.Envelope.Infiltration = ( - new_zone_def.Envelope.BasementInfiltration + if self.zone_assignments is not None: + from epinterface.sbem.flat_model import zone_params_to_zone_component + + tbl = self.zone_assignments + + def apply_zone(zone_name: str) -> None: + key = parse_zone_key(zone_name, self.geometry) + params = tbl.resolve( + key, + attic_use_fraction=self.Attic.UseFraction, + attic_conditioned=self.Attic.Conditioned, + basement_use_fraction=self.Basement.UseFraction, + basement_conditioned=self.Basement.Conditioned, + geometry=self.geometry, + ) + zc = zone_params_to_zone_component(params, id_tag=zone_tag(zone_name)) + zc.add_to_idf_zone(idf, zone_name) + + for zone in added_zone_lists.main_zone_list.Names: + apply_zone(zone) + + if self.Basement.UseFraction or self.Basement.Conditioned: + if self.Basement.Conditioned: + print( + "WARNING: Basement conditioned, but Cooling disabled due to MASSACHUSETTS ASSUMPTIONS" + ) + for zone in added_zone_lists.basement_zone_list.Names: + apply_zone(zone) + + if self.Attic.UseFraction or self.Attic.Conditioned: + for zone in added_zone_lists.attic_zone_list.Names: + apply_zone(zone) + + subtractor = ( + 4 if (self.geometry.basement and not self.Basement.Conditioned) else 2 ) + hsp_acc = np.zeros(12) + csp_acc = np.zeros(12) + wsum = 0.0 + has_heating = False + has_cooling = False + for zone_name in added_zone_lists.main_zone_list.Names: + area = get_zone_floor_area(idf, zone_name) + key = parse_zone_key(zone_name, self.geometry) + params = tbl.resolve( + key, + attic_use_fraction=self.Attic.UseFraction, + attic_conditioned=self.Attic.Conditioned, + basement_use_fraction=self.Basement.UseFraction, + basement_conditioned=self.Basement.Conditioned, + geometry=self.geometry, + ) + zc = zone_params_to_zone_component(params, id_tag=zone_tag(zone_name)) + hs = zc.Operations.SpaceUse.Thermostat.HeatingSchedule.MonthlyAverageValues + cs = zc.Operations.SpaceUse.Thermostat.CoolingSchedule.MonthlyAverageValues + hsp_acc += np.asarray(hs, dtype=float) * area + csp_acc += np.asarray(cs, dtype=float) * area + wsum += area + cond = zc.Operations.HVAC.ConditioningSystems + has_heating = has_heating or (cond.Heating is not None) + has_cooling = has_cooling or (cond.Cooling is not None) + if wsum <= 0: + msg = "cannot compute ground temperatures without main zone floor area" + raise ValueError(msg) + hsp_line = hsp_acc / wsum + csp_line = csp_acc / wsum - if not self.Basement.Conditioned: - new_zone_def.Operations.HVAC.Ventilation.Provider = "None" - new_zone_def.Operations.HVAC.ConditioningSystems.Heating = None - new_zone_def.Operations.HVAC.ConditioningSystems.Cooling = None + epw = EPW(epw_path.as_posix()) + epw_ground_vals_all = epw.monthly_ground_temperature + if self.geometry.basement: + epw_ground_vals = epw_ground_vals_all[4].values + else: + epw_ground_vals = epw_ground_vals_all[0.5].values + low_ground_val = min(epw_ground_vals) + high_ground_val = max(epw_ground_vals) + phase = (np.array(epw_ground_vals) - low_ground_val) / ( + high_ground_val - low_ground_val + ) + if has_heating and has_cooling: + winter_line = np.asarray(hsp_line) - subtractor + summer_line = np.asarray(csp_line) - subtractor + elif has_heating: + winter_line = np.asarray(hsp_line) - subtractor + summer_line = np.asarray(hsp_line) + elif has_cooling: + winter_line = np.asarray(csp_line) - subtractor + summer_line = np.asarray(csp_line) - subtractor else: - # TODO: make this configurable!!!! - print( - "WARNING: Basement conditioned, but Cooling disabled due to MASSACHUSETTS ASSUMPTIONS" + winter_line = np.array(assumed_constants.SiteGroundTemperature_degC) + summer_line = np.array(assumed_constants.SiteGroundTemperature_degC) + interp_temp = phase * np.abs(summer_line - winter_line) + winter_line + ground_vals = [max(epw_ground_vals[i], interp_temp[i]) for i in range(12)] + idf = SiteGroundTemperature.FromValues(ground_vals).add(idf) + + ref_zone = zone_params_to_zone_component(tbl.defaults) + handlers = SurfaceHandlers.Default( + basement_suffix=self.geometry.basement_suffix + if self.geometry.basement + else "NO-OP" + ) + idf = handlers.handle_shared_opaque_envelope( + idf, + ref_zone.Envelope.Assemblies, + with_attic=(self.geometry.roof_height or 0) > 0, + with_basement=self.geometry.basement, + exposed_basement_frac=self.geometry.exposed_basement_frac, + ) + for zone_name in added_zone_lists.conditioned_zone_list.Names: + key = parse_zone_key(zone_name, self.geometry) + params = tbl.resolve( + key, + attic_use_fraction=self.Attic.UseFraction, + attic_conditioned=self.Attic.Conditioned, + basement_use_fraction=self.Basement.UseFraction, + basement_conditioned=self.Basement.Conditioned, + geometry=self.geometry, + ) + zc = zone_params_to_zone_component(params, id_tag=zone_tag(zone_name)) + idf = handlers.assign_facade_and_glazing_for_zone( + idf, + zc.Envelope.Assemblies.FacadeAssembly, + zc.Envelope.Window, + zone_name, ) - new_zone_def.Operations.HVAC.ConditioningSystems.Cooling = None - - for zone in added_zone_lists.basement_zone_list.Names: - new_zone_def.add_to_idf_zone(idf, zone) - - if self.Attic.UseFraction or self.Attic.Conditioned: - new_zone_def = self.Zone.model_copy(deep=True) - frac = self.Attic.UseFraction or 0 - pd = new_zone_def.Operations.SpaceUse.Occupancy.PeopleDensity - epd = new_zone_def.Operations.SpaceUse.Equipment.PowerDensity - lpd = new_zone_def.Operations.SpaceUse.Lighting.PowerDensity - - new_zone_def.Operations.SpaceUse.Equipment.PowerDensity = frac * epd - new_zone_def.Operations.SpaceUse.Lighting.PowerDensity = frac * lpd - new_zone_def.Operations.SpaceUse.Occupancy.PeopleDensity = frac * pd - - # handle infiltration for roofs - # because the *regular* infiltration object is the one that gets added, we simply copy - # the desired attic infiltration into the relevant section. - new_zone_def.Envelope.Infiltration = new_zone_def.Envelope.AtticInfiltration - - if not self.Attic.Conditioned: - new_zone_def.Operations.HVAC.Ventilation.Provider = "None" - new_zone_def.Operations.HVAC.ConditioningSystems.Heating = None - new_zone_def.Operations.HVAC.ConditioningSystems.Cooling = None - - # TODO: handle mutating infiltration object when "ventilated attics" are set - for zone in added_zone_lists.attic_zone_list.Names: - new_zone_def.add_to_idf_zone(idf, zone) - - idf = self.add_constructions( - idf, self.Zone.Envelope.Assemblies, self.Zone.Envelope.Window - ) + + else: + if self.Zone is None: + msg = "Model has no Zone for legacy build path." + raise ValueError(msg) + # Handle main zones + for zone in added_zone_lists.main_zone_list.Names: + self.Zone.add_to_idf_zone(idf, zone) + + # Handle setting ground temperature + subtractor = ( + 4 if (self.geometry.basement and not self.Basement.Conditioned) else 2 + ) + has_heating = ( + self.Zone.Operations.HVAC.ConditioningSystems.Heating is not None + ) + has_cooling = ( + self.Zone.Operations.HVAC.ConditioningSystems.Cooling is not None + ) + hsp = self.Zone.Operations.SpaceUse.Thermostat.HeatingSchedule + csp = self.Zone.Operations.SpaceUse.Thermostat.CoolingSchedule + epw = EPW(epw_path.as_posix()) + epw_ground_vals_all = epw.monthly_ground_temperature + if self.geometry.basement: + epw_ground_vals = epw_ground_vals_all[4].values + else: + epw_ground_vals = epw_ground_vals_all[0.5].values + low_ground_val = min(epw_ground_vals) + high_ground_val = max(epw_ground_vals) + phase = (np.array(epw_ground_vals) - low_ground_val) / ( + high_ground_val - low_ground_val + ) + if has_heating and has_cooling: + winter_line = np.array(hsp.MonthlyAverageValues) - subtractor + summer_line = np.array(csp.MonthlyAverageValues) - subtractor + elif has_heating: + winter_line = np.array(hsp.MonthlyAverageValues) - subtractor + summer_line = np.array(hsp.MonthlyAverageValues) + elif has_cooling: + winter_line = np.array(csp.MonthlyAverageValues) - subtractor + summer_line = np.array(csp.MonthlyAverageValues) - subtractor + else: + winter_line = np.array(assumed_constants.SiteGroundTemperature_degC) + summer_line = np.array(assumed_constants.SiteGroundTemperature_degC) + interp_temp = phase * np.abs(summer_line - winter_line) + winter_line + ground_vals = [max(epw_ground_vals[i], interp_temp[i]) for i in range(12)] + idf = SiteGroundTemperature.FromValues(ground_vals).add(idf) + + # handle basements + if self.Basement.UseFraction or self.Basement.Conditioned: + new_zone_def = self.Zone.model_copy(deep=True) + frac = self.Basement.UseFraction or 0 + pd = new_zone_def.Operations.SpaceUse.Occupancy.PeopleDensity + epd = new_zone_def.Operations.SpaceUse.Equipment.PowerDensity + lpd = new_zone_def.Operations.SpaceUse.Lighting.PowerDensity + + new_zone_def.Operations.SpaceUse.Equipment.PowerDensity = frac * epd + new_zone_def.Operations.SpaceUse.Lighting.PowerDensity = frac * lpd + new_zone_def.Operations.SpaceUse.Occupancy.PeopleDensity = frac * pd + + new_zone_def.Envelope.Infiltration = ( + new_zone_def.Envelope.BasementInfiltration + ) + + if not self.Basement.Conditioned: + new_zone_def.Operations.HVAC.Ventilation.Provider = "None" + new_zone_def.Operations.HVAC.ConditioningSystems.Heating = None + new_zone_def.Operations.HVAC.ConditioningSystems.Cooling = None + else: + print( + "WARNING: Basement conditioned, but Cooling disabled due to MASSACHUSETTS ASSUMPTIONS" + ) + new_zone_def.Operations.HVAC.ConditioningSystems.Cooling = None + + for zone in added_zone_lists.basement_zone_list.Names: + new_zone_def.add_to_idf_zone(idf, zone) + + if self.Attic.UseFraction or self.Attic.Conditioned: + new_zone_def = self.Zone.model_copy(deep=True) + frac = self.Attic.UseFraction or 0 + pd = new_zone_def.Operations.SpaceUse.Occupancy.PeopleDensity + epd = new_zone_def.Operations.SpaceUse.Equipment.PowerDensity + lpd = new_zone_def.Operations.SpaceUse.Lighting.PowerDensity + + new_zone_def.Operations.SpaceUse.Equipment.PowerDensity = frac * epd + new_zone_def.Operations.SpaceUse.Lighting.PowerDensity = frac * lpd + new_zone_def.Operations.SpaceUse.Occupancy.PeopleDensity = frac * pd + + new_zone_def.Envelope.Infiltration = ( + new_zone_def.Envelope.AtticInfiltration + ) + + if not self.Attic.Conditioned: + new_zone_def.Operations.HVAC.Ventilation.Provider = "None" + new_zone_def.Operations.HVAC.ConditioningSystems.Heating = None + new_zone_def.Operations.HVAC.ConditioningSystems.Cooling = None + + for zone in added_zone_lists.attic_zone_list.Names: + new_zone_def.add_to_idf_zone(idf, zone) + + idf = self.add_constructions( + idf, self.Zone.Envelope.Assemblies, self.Zone.Envelope.Window + ) # > operations # ----> space use @@ -1082,7 +1379,7 @@ def get_warnings(self, idf: IDF) -> str: return err_text def standard_results_postprocess( - self, sql: Sql, ep_version_major: int + self, sql: Sql, ep_version_major: int, idf: IDF | None = None ) -> pd.Series: """Postprocess the sql file to get the standard results. @@ -1093,22 +1390,90 @@ def standard_results_postprocess( Args: sql (Sql): The sql file to postprocess. ep_version_major (int): The major version of EnergyPlus. + idf (IDF | None): Built model (required for area-weighted COP when using zone_assignments). Returns: series (pd.Series): The postprocessed results. """ - ops = self.Zone.Operations - cond_sys = ops.HVAC.ConditioningSystems - heat_cop = ( - cond_sys.Heating.effective_system_cop if cond_sys.Heating is not None else 1 - ) - cool_cop = ( - cond_sys.Cooling.effective_system_cop if cond_sys.Cooling is not None else 1 - ) - dhw_cop = ops.DHW.effective_system_cop - heat_fuel = cond_sys.Heating.Fuel if cond_sys.Heating is not None else None - cool_fuel = cond_sys.Cooling.Fuel if cond_sys.Cooling is not None else None - dhw_fuel = ops.DHW.FuelType + if self.zone_assignments is None: + if self.Zone is None: + msg = "Model has no Zone for results postprocess." + raise ValueError(msg) + ops = self.Zone.Operations + cond_sys = ops.HVAC.ConditioningSystems + heat_cop = ( + cond_sys.Heating.effective_system_cop + if cond_sys.Heating is not None + else 1 + ) + cool_cop = ( + cond_sys.Cooling.effective_system_cop + if cond_sys.Cooling is not None + else 1 + ) + dhw_cop = ops.DHW.effective_system_cop + heat_fuel = cond_sys.Heating.Fuel if cond_sys.Heating is not None else None + cool_fuel = cond_sys.Cooling.Fuel if cond_sys.Cooling is not None else None + dhw_fuel = ops.DHW.FuelType + else: + from epinterface.sbem.flat_model import zone_params_to_zone_component + + tbl = self.zone_assignments + ref_ops = zone_params_to_zone_component(tbl.defaults).Operations + cond_ref = ref_ops.HVAC.ConditioningSystems + heat_fuel = cond_ref.Heating.Fuel if cond_ref.Heating is not None else None + cool_fuel = cond_ref.Cooling.Fuel if cond_ref.Cooling is not None else None + dhw_fuel = ref_ops.DHW.FuelType + if idf is None: + heat_cop = ( + cond_ref.Heating.effective_system_cop + if cond_ref.Heating is not None + else 1 + ) + cool_cop = ( + cond_ref.Cooling.effective_system_cop + if cond_ref.Cooling is not None + else 1 + ) + dhw_cop = ref_ops.DHW.effective_system_cop + else: + heat_num = 0.0 + cool_num = 0.0 + dhw_num = 0.0 + den = 0.0 + for z in idf.idfobjects["ZONE"]: + area = float(get_zone_floor_area(idf, z.Name)) + key = parse_zone_key(z.Name, self.geometry) + params = tbl.resolve( + key, + attic_use_fraction=self.Attic.UseFraction, + attic_conditioned=self.Attic.Conditioned, + basement_use_fraction=self.Basement.UseFraction, + basement_conditioned=self.Basement.Conditioned, + geometry=self.geometry, + ) + tag = "".join(c if c.isalnum() else "_" for c in z.Name) + zc = zone_params_to_zone_component(params, id_tag=tag) + cond = zc.Operations.HVAC.ConditioningSystems + hf = ( + cond.Heating.effective_system_cop + if cond.Heating is not None + else 1.0 + ) + cf = ( + cond.Cooling.effective_system_cop + if cond.Cooling is not None + else 1.0 + ) + df = zc.Operations.DHW.effective_system_cop + heat_num += hf * area + cool_num += cf * area + dhw_num += df * area + den += area + heat_cop = heat_num / den if den else 1.0 + cool_cop = cool_num / den if den else 1.0 + dhw_cop = dhw_num / den if den else 1.0 + all_fuel_names = sorted({*get_args(FuelType), *get_args(DHWFuelType)}) return energy_and_peak_postprocess( sql, @@ -1165,7 +1530,7 @@ def run( msg = f"EnergyPlus version not found in IDF file: {idf.idfobjects['VERSION']}" raise ValueError(msg) results = self.standard_results_postprocess( - sql, ep_version_major=idf.as_version.major + sql, ep_version_major=idf.as_version.major, idf=idf ) zone_weights, zone_names = self.get_zone_weights_and_names(idf) @@ -1307,10 +1672,10 @@ class ModelRunResults: # "/Users/daryaguettler/globi/data/Brazil/component-map.yaml" # ) database_path = Path( - "/Users/daryaguettler/globi/data/Portugal/components-lib.db" + "/Users/daryaguettler/school-repos/globi/inputs/components-lib.db" ) component_map_path = Path( - "/Users/daryaguettler/globi/data/Portugal/component-map.yaml" + "/Users/daryaguettler/school-repos/globi/inputs/component-map.yaml" ) settings = PrismaSettings( database_path=database_path, @@ -1334,11 +1699,15 @@ class ModelRunResults: # "scenario": "withAC", # } context = { - "Region": "I1_V2", - "City": "LS", - "Typology": "Single_Family_Residential", - "Age_buckets": "1971_1980", - "scenario": "Baseline", + "Region": "Manchester", + "Typology": "domestic", + "Fabric": "construction0", + "Windows": "default_glazing", + "Floor_use": "LivingRoomKitchen", + "Heating": "NaturalGasHeating", + "Cooling": "ACCentral", + "Distribution": "AirDuctsConditionedUninsulated", + "scenario": "No_DSR", } with settings.db: zone = cast(ZoneComponent, selector.get_component(context=context, db=db)) diff --git a/epinterface/sbem/building_flat_model.py b/epinterface/sbem/building_flat_model.py new file mode 100644 index 0000000..214dcad --- /dev/null +++ b/epinterface/sbem/building_flat_model.py @@ -0,0 +1,429 @@ +"""Building-level flat model with per-floor zone role assignments.""" + +from __future__ import annotations + +import tempfile +from collections.abc import Callable +from pathlib import Path +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from epinterface.sbem.builder import ModelRunResults + +import pandas as pd +from archetypal import IDF +from pydantic import BaseModel, Field, field_validator, model_validator + +from epinterface.analysis.overheating import OverheatingAnalysisConfig +from epinterface.geometry import ShoeboxGeometry, ZoningType +from epinterface.sbem.builder import ( + AtticAssumptions, + BasementAssumptions, + Model, + SimulationPathConfig, +) +from epinterface.sbem.flat_model import ( + FlatModel, + _eppy_lights_watts_per_area, + _eppy_lights_zone_name, +) +from epinterface.sbem.zone_assignment import ( + ParsedZoneKey, + ZoneAssignmentTable, + ZoneRole, + parse_zone_key, +) +from epinterface.sbem.zone_params import ZoneParams +from epinterface.weather import WeatherUrl + +_GEOMETRY_SHELL_FIELDS = ("Width", "Depth", "F2FHeight", "NFloors", "WWR", "Rotation") + + +def roles_for_zoning(zoning: ZoningType) -> frozenset[str]: + """Valid zone role keys for a zoning strategy.""" + if zoning == "by_storey": + return frozenset({ZoneRole.floor.value}) + return frozenset({ + ZoneRole.core.value, + ZoneRole.perim_1.value, + ZoneRole.perim_2.value, + ZoneRole.perim_3.value, + ZoneRole.perim_4.value, + }) + + +class BuildingShellParams(BaseModel): + """Building-only inputs: geometry shell, weather, and zoning.""" + + EPWURI: WeatherUrl | Path + zoning: ZoningType = Field(default="core/perim") + Width: float + Depth: float + F2FHeight: float + NFloors: int = Field(ge=1) + WWR: float = Field( + ge=0, le=1 + ) # TODO: discuss- maybe this shoudl be floor level? could be the case for ground level shop vs. resi top floors + Rotation: float = 0.0 + + @classmethod + def from_defaults( + cls, + defaults: ZoneParams, + *, + EPWURI: WeatherUrl | Path, + zoning: ZoningType = "core/perim", + ) -> BuildingShellParams: + """Build shell params from zone defaults plus weather and zoning.""" + return cls( + EPWURI=EPWURI, + zoning=zoning, + **{name: getattr(defaults, name) for name in _GEOMETRY_SHELL_FIELDS}, + ) + + +class FloorFlatModel(BaseModel): + """Per-storey zone role patches applied on top of building defaults.""" + + storey_index: int = Field( + ge=0 + ) # TODO: i'm keeping the naming convention with the e since its used throughout repo - ensure its consistent everywhere and remove e maybe later + zones: dict[str, dict[str, Any]] = Field(default_factory=dict) + + +class BuildingFlatModel(BaseModel): + """Flat building model: shell, building-wide defaults, and per-floor zone patches.""" + + shell: BuildingShellParams + defaults: ZoneParams + floors: list[FloorFlatModel] = Field(default_factory=list) + attic: AtticAssumptions = Field( + default_factory=lambda: AtticAssumptions(UseFraction=None, Conditioned=False) + ) + basement: BasementAssumptions = Field( + default_factory=lambda: BasementAssumptions(UseFraction=None, Conditioned=False) + ) + + @model_validator(mode="after") + def validate_shell_matches_defaults(self) -> BuildingFlatModel: + """Ensure shell geometry fields match building defaults.""" + for name in _GEOMETRY_SHELL_FIELDS: + shell_val = getattr(self.shell, name) + default_val = getattr(self.defaults, name) + if shell_val != default_val: + msg = ( + f"shell.{name} ({shell_val!r}) must match " + f"defaults.{name} ({default_val!r})" + ) + raise ValueError(msg) + return self + + @model_validator(mode="after") + def validate_floors(self) -> BuildingFlatModel: + """Validate floor indices and zone roles for the shell zoning.""" + seen: set[int] = set() + valid_roles = roles_for_zoning(self.shell.zoning) + for floor in self.floors: + if floor.storey_index in seen: + msg = f"duplicate floor storey_index: {floor.storey_index}" + raise ValueError(msg) + seen.add(floor.storey_index) + if floor.storey_index >= self.shell.NFloors: + msg = ( + f"floor storey_index {floor.storey_index} must be " + f"< NFloors ({self.shell.NFloors})" + ) + raise ValueError(msg) + for role in floor.zones: + if role not in valid_roles: + msg = f"invalid role {role!r} for zoning {self.shell.zoning!r}" + raise ValueError(msg) + return self + + @field_validator("floors") + @classmethod + def sort_floors(cls, floors: list[FloorFlatModel]) -> list[FloorFlatModel]: + """Keep floors ordered by storey index.""" + return sorted(floors, key=lambda f: f.storey_index) + + def zone_overrides(self) -> dict[int, dict[str, dict[str, Any]]]: + """Compile floor zone patches into legacy override dict shape.""" + out: dict[int, dict[str, dict[str, Any]]] = {} + for floor in self.floors: + if floor.zones: + out[floor.storey_index] = { + role: dict(patch) for role, patch in floor.zones.items() + } + return out + + def zone_assignment_table(self) -> ZoneAssignmentTable: + """Building defaults plus sparse per-floor zone role overrides.""" + return ZoneAssignmentTable( + defaults=self.defaults, + overrides=self.zone_overrides(), + ) + + def resolve_params( + self, + key: ParsedZoneKey, + *, + geometry: ShoeboxGeometry, + attic_use_fraction: float | None = None, + attic_conditioned: bool = False, + basement_use_fraction: float | None = None, + basement_conditioned: bool = False, + ) -> ZoneParams: + """Merge building defaults and floor zone patches for one zone key.""" + return self.zone_assignment_table().resolve( + key, + attic_use_fraction=attic_use_fraction, + attic_conditioned=attic_conditioned, + basement_use_fraction=basement_use_fraction, + basement_conditioned=basement_conditioned, + geometry=geometry, + ) + + def to_model(self) -> tuple[Model, Callable[[IDF], IDF]]: + """Return runnable Model and post-geometry rotation callback.""" + geometry = ShoeboxGeometry( + x=0, + y=0, + w=self.shell.Width, + d=self.shell.Depth, + h=self.shell.F2FHeight, + num_stories=self.shell.NFloors, + zoning=self.shell.zoning, + roof_height=None, + wwr=self.shell.WWR, + basement=False, + ) + rotation = self.shell.Rotation + + def post_geometry_callback(idf: IDF) -> IDF: + idf.rotate(rotation) + return idf + + return ( + Model( + geometry=geometry, + Zone=None, + zone_assignments=self.zone_assignment_table(), + Attic=self.attic, + Basement=self.basement, + Weather=self.shell.EPWURI, + ), + post_geometry_callback, + ) + + def build_idf( + self, + *, + output_dir: Path | None = None, + weather_dir: Path | None = None, + ) -> IDF: + """Build an IDF without running EnergyPlus.""" + model, cb = self.to_model() + out = ( + Path(tempfile.mkdtemp(prefix="flat_build_")) + if output_dir is None + else Path(output_dir) + ) + out.mkdir(parents=True, exist_ok=True) + cfg = ( + SimulationPathConfig(output_dir=out, weather_dir=weather_dir) + if weather_dir is not None + else SimulationPathConfig(output_dir=out) + ) + return model.build(cfg, post_geometry_callback=cb) + + def zone_assignment_summary(self, idf: IDF | None = None) -> pd.DataFrame: + """Resolved zone params plus optional cross-check against built IDF.""" + model, _ = self.to_model() + geom = model.geometry + tbl = self.zone_assignment_table() + idf_built = idf or self.build_idf() + lights_by_zone: dict[str, float] = {} + for lg in idf_built.idfobjects["LIGHTS"]: + zn = _eppy_lights_zone_name(lg) + lights_by_zone[zn] = _eppy_lights_watts_per_area(lg) + + facade_by_zone: dict[str, str] = {} + for srf in idf_built.idfobjects["BUILDINGSURFACE:DETAILED"]: + if ( + str(srf.Surface_Type).lower() == "wall" + and str(srf.Outside_Boundary_Condition).lower() == "outdoors" + ): + zn = str(srf.Zone_Name) + facade_by_zone.setdefault(zn, str(srf.Construction_Name)) + + rows = [] + for z in idf_built.idfobjects["ZONE"]: + zn = z.Name + key = parse_zone_key(zn, geom) + params = tbl.resolve( + key, + attic_use_fraction=model.Attic.UseFraction, + attic_conditioned=model.Attic.Conditioned, + basement_use_fraction=model.Basement.UseFraction, + basement_conditioned=model.Basement.Conditioned, + geometry=geom, + ) + rows.append({ + "ep_zone_name": zn, + "storey_index": key.storey_index, + "role": key.role.value, + "category": key.category, + "LightingPowerDensity": params.LightingPowerDensity, + "EquipmentPowerDensity": params.EquipmentPowerDensity, + "FacadeRValue": params.FacadeRValue, + "idf_lighting_w_per_m2": lights_by_zone.get(zn), + "idf_facade_construction": facade_by_zone.get(zn), + }) + return pd.DataFrame(rows) + + def zone_simulation_summary(self, results: ModelRunResults) -> pd.DataFrame: + """Assignment summary joined with per-zone simulated energy from a model run.""" + from epinterface.analysis.zone_energy import ( + merge_assignment_and_energy, + zone_energy_summary, + ) + + assign = self.zone_assignment_summary(idf=results.idf) + energy = zone_energy_summary(results.sql, results.idf) + return merge_assignment_and_energy(assign, energy) + + def simulate( + self, + overheating_config: OverheatingAnalysisConfig | None = None, + eplus_parent_dir: Path | None = None, + ): + """Simulate the model and return run results.""" + model, cb = self.to_model() + return model.run( + post_geometry_callback=cb, + eplus_parent_dir=eplus_parent_dir, + overheating_config=overheating_config, + ) + + def to_flat_model(self) -> FlatModel: + """Convert to legacy FlatModel with compiled zone_overrides.""" + data = self.defaults.model_dump() + data.update({ + "EPWURI": self.shell.EPWURI, + "zoning": self.shell.zoning, + "zone_overrides": self.zone_overrides(), + }) + return FlatModel.model_validate(data) + + @classmethod + def from_flat_model(cls, flat: FlatModel) -> BuildingFlatModel: + """Build structured model from legacy FlatModel.""" + defaults = flat.zone_params_defaults() + shell = BuildingShellParams.from_defaults( + defaults, + EPWURI=flat.EPWURI, + zoning=flat.zoning, + ) + floors = [ + FloorFlatModel(storey_index=storey, zones=roles) + for storey, roles in sorted(flat.zone_overrides.items()) + ] + return cls( + shell=shell, + defaults=defaults, + floors=floors, + ) + + @classmethod + def example_with_zone_overrides(cls) -> BuildingFlatModel: + """Two-storey core/perim shoebox with contrasting zone overrides.""" + lp = 10.0 + defaults = ZoneParams( + F2FHeight=3.25, + Width=12, + Depth=12, + Rotation=0, + WWR=0.2, + NFloors=2, + FacadeRValue=3.0, + RoofRValue=3.0, + SlabRValue=3.0, + WindowUValue=3.0, + WindowSHGF=0.7, + WindowTVis=0.5, + InfiltrationACH=0.5, + VentFlowRatePerArea=0.001, + VentFlowRatePerPerson=0.0085, + VentProvider="Mechanical", + VentHRV="NoHRV", + VentEconomizer="NoEconomizer", + VentDCV="NoDCV", + DHWFlowRatePerPerson=0.010, + DHWFuel="Electricity", + DHWSystemCOP=1.0, + DHWDistributionCOP=1.0, + EquipmentPowerDensity=25, + LightingPowerDensity=lp, + OccupantDensity=0.01, + EquipmentBase=0.4, + EquipmentAMInterp=0.5, + EquipmentLunchInterp=0.8, + EquipmentPMInterp=0.5, + EquipmentWeekendPeakInterp=0.25, + EquipmentSummerPeakInterp=0.5, + LightingBase=0.3, + LightingAMInterp=0.75, + LightingLunchInterp=0.75, + LightingPMInterp=0.9, + LightingWeekendPeakInterp=0.75, + LightingSummerPeakInterp=0.9, + OccupancyBase=0.05, + OccupancyAMInterp=0.25, + OccupancyLunchInterp=0.9, + OccupancyPMInterp=0.5, + OccupancyWeekendPeakInterp=0.15, + OccupancySummerPeakInterp=0.85, + HeatingSetpointBase=21, + SetpointDeadband=2, + HeatingSetpointSetback=2, + CoolingSetpointSetback=2, + NightSetback=0.5, + WeekendSetback=0.5, + SummerSetback=0.5, + HeatingFuel="Electricity", + CoolingFuel="Electricity", + HeatingSystemCOP=1.0, + CoolingSystemCOP=1.0, + HeatingDistributionCOP=1.0, + CoolingDistributionCOP=1.0, + IdealLoadsHeatingOn=True, + IdealLoadsCoolingOn=True, + ) + shell = BuildingShellParams.from_defaults( + defaults, + EPWURI=WeatherUrl( # pyright: ignore[reportCallIssue] + "https://climate.onebuilding.org/WMO_Region_4_North_and_Central_America/USA_United_States_of_America/MA_Massachusetts/USA_MA_Bedford-Hanscom.Field.AP.744900_TMYx.2009-2023.zip" + ), + zoning="core/perim", + ) + return cls( + shell=shell, + defaults=defaults, + floors=[ + FloorFlatModel( + storey_index=0, + zones={ + "perim_1": {"LightingPowerDensity": lp * 1.5}, + "perim_2": {"LightingPowerDensity": lp * 1.5}, + "perim_3": {"LightingPowerDensity": lp * 1.5}, + "perim_4": {"LightingPowerDensity": lp * 1.5}, + }, + ), + FloorFlatModel( + storey_index=1, + zones={ + "core": {"LightingPowerDensity": lp * 0.5, "FacadeRValue": 6.0} + }, + ), + ], + ) diff --git a/epinterface/sbem/flat_model.py b/epinterface/sbem/flat_model.py index b74e125..37c5ca1 100644 --- a/epinterface/sbem/flat_model.py +++ b/epinterface/sbem/flat_model.py @@ -1,14 +1,28 @@ """Flat model used for calibration.""" +from __future__ import annotations + +import tempfile from collections.abc import Callable from pathlib import Path +from typing import TYPE_CHECKING, Any, cast + +if TYPE_CHECKING: + from epinterface.sbem.builder import ModelRunResults + from epinterface.sbem.building_flat_model import BuildingFlatModel +import pandas as pd from archetypal import IDF from pydantic import BaseModel, Field from epinterface.analysis.overheating import OverheatingAnalysisConfig -from epinterface.geometry import ShoeboxGeometry -from epinterface.sbem.builder import AtticAssumptions, BasementAssumptions, Model +from epinterface.geometry import ShoeboxGeometry, ZoningType +from epinterface.sbem.builder import ( + AtticAssumptions, + BasementAssumptions, + Model, + SimulationPathConfig, +) from epinterface.sbem.components.envelope import ( ConstructionAssemblyComponent, ConstructionLayerComponent, @@ -35,18 +49,18 @@ ) from epinterface.sbem.components.systems import ( ConditioningSystemsComponent, - DCVMethod, DHWComponent, - DHWFuelType, - EconomizerMethod, - FuelType, - HRVMethod, ThermalSystemComponent, VentilationComponent, - VentilationProvider, ZoneHVACComponent, ) from epinterface.sbem.components.zones import ZoneComponent +from epinterface.sbem.zone_assignment import ( + ParsedZoneKey, + ZoneAssignmentTable, + parse_zone_key, +) +from epinterface.sbem.zone_params import ZoneParams from epinterface.weather import WeatherUrl xps_board = ConstructionMaterialComponent( @@ -409,6 +423,79 @@ def to_schedule(self, name: str, category: YearScheduleCategory): return year +def parameteric_weekday_hourly(params: ParametericYear) -> list[float]: + """24 weekday hour fractions from parametric schedule shape.""" + peak = 1.0 + am_inter = params.Base + params.AMInterp * (peak - params.Base) + lunch_inter = params.Base + params.LunchInterp * (peak - params.Base) + pm_inter = params.Base + params.PMInterp * (peak - params.Base) + return [ + params.Base, + params.Base, + params.Base, + params.Base, + params.Base, + params.Base, + am_inter, + am_inter, + am_inter, + peak, + peak, + peak, + lunch_inter, + lunch_inter, + peak, + peak, + peak, + peak, + pm_inter, + pm_inter, + pm_inter, + params.Base, + params.Base, + params.Base, + ] + + +def year_schedule_from_weekday_hourly( + name: str, + category: YearScheduleCategory, + hourly: list[float], +) -> YearComponent: + """Year schedule with the same weekday profile every day (all seasons).""" + day = DayComponent( + Name=f"{name}_CustomDay", + Type="Fraction", + **{f"Hour_{h:02d}": hourly[h] for h in range(24)}, + ) + week = WeekComponent( + Name=f"{name}_CustomWeek", + Monday=day, + Tuesday=day, + Wednesday=day, + Thursday=day, + Friday=day, + Saturday=day, + Sunday=day, + ) + return YearComponent( + Name=f"{name}_CustomYear", + Type=category, + January=week, + February=week, + March=week, + April=week, + May=week, + June=week, + July=week, + August=week, + September=week, + October=week, + November=week, + December=week, + ) + + class ParametricSetpoints(BaseModel): """A model for a setpoint schedule that is parameterized by the base, and the setbacks.""" @@ -420,8 +507,9 @@ class ParametricSetpoints(BaseModel): WeekendSetback: float = Field(ge=0, le=1) SummerSetback: float = Field(ge=0, le=1) - def to_schedules(self): + def to_schedules(self, name_suffix: str = ""): """Convert the setpoint parameters to a set of schedules.""" + sfx = name_suffix.replace(" ", "_") hsp = self.HeatingSetpoint csp = hsp + self.DeadBand hsp_setback = hsp - self.HeatingSetback @@ -444,7 +532,7 @@ def to_schedules(self): ) hsp_standard_day = DayComponent( - Name="HSP_Standard_Day", + Name=f"HSP_Standard_Day{sfx}", Type="Temperature", Hour_00=hsp_night, Hour_01=hsp_night, @@ -473,7 +561,7 @@ def to_schedules(self): ) hsp_weekend_day = DayComponent( - Name="HSP_Weekend_Day", + Name=f"HSP_Weekend_Day{sfx}", Type="Temperature", Hour_00=hsp_night, Hour_01=hsp_night, @@ -502,7 +590,7 @@ def to_schedules(self): ) hsp_summer_day = DayComponent( - Name="HSP_Summer_Day", + Name=f"HSP_Summer_Day{sfx}", Type="Temperature", Hour_00=hsp_night, Hour_01=hsp_night, @@ -531,7 +619,7 @@ def to_schedules(self): ) hsp_summer_weekend_day = DayComponent( - Name="HSP_Summer_Weekend_Day", + Name=f"HSP_Summer_Weekend_Day{sfx}", Type="Temperature", Hour_00=hsp_night, Hour_01=hsp_night, @@ -560,7 +648,7 @@ def to_schedules(self): ) csp_standard_day = DayComponent( - Name="CSP_Standard_Day", + Name=f"CSP_Standard_Day{sfx}", Type="Temperature", Hour_00=csp_night, Hour_01=csp_night, @@ -589,7 +677,7 @@ def to_schedules(self): ) csp_weekend_day = DayComponent( - Name="CSP_Weekend_Day", + Name=f"CSP_Weekend_Day{sfx}", Type="Temperature", Hour_00=csp_night, Hour_01=csp_night, @@ -618,7 +706,7 @@ def to_schedules(self): ) csp_summer_day = DayComponent( - Name="CSP_Summer_Day", + Name=f"CSP_Summer_Day{sfx}", Type="Temperature", Hour_00=csp_night, Hour_01=csp_night, @@ -647,7 +735,7 @@ def to_schedules(self): ) csp_summer_weekend_day = DayComponent( - Name="CSP_Summer_Weekend_Day", + Name=f"CSP_Summer_Weekend_Day{sfx}", Type="Temperature", Hour_00=csp_night, Hour_01=csp_night, @@ -676,7 +764,7 @@ def to_schedules(self): ) hsp_standard_week = WeekComponent( - Name="HSP_Standard_Week", + Name=f"HSP_Standard_Week{sfx}", Monday=hsp_standard_day, Tuesday=hsp_standard_day, Wednesday=hsp_standard_day, @@ -687,7 +775,7 @@ def to_schedules(self): ) csp_standard_week = WeekComponent( - Name="CSP_Standard_Week", + Name=f"CSP_Standard_Week{sfx}", Monday=csp_standard_day, Tuesday=csp_standard_day, Wednesday=csp_standard_day, @@ -698,7 +786,7 @@ def to_schedules(self): ) hsp_summer_week = WeekComponent( - Name="HSP_Summer_Week", + Name=f"HSP_Summer_Week{sfx}", Monday=hsp_summer_day, Tuesday=hsp_summer_day, Wednesday=hsp_summer_day, @@ -709,7 +797,7 @@ def to_schedules(self): ) csp_summer_week = WeekComponent( - Name="CSP_Summer_Week", + Name=f"CSP_Summer_Week{sfx}", Monday=csp_summer_day, Tuesday=csp_summer_day, Wednesday=csp_summer_day, @@ -720,7 +808,7 @@ def to_schedules(self): ) hsp_year = YearComponent( - Name="HSP_Year", + Name=f"HSP_Year{sfx}", Type="Setpoint", January=hsp_standard_week, February=hsp_standard_week, @@ -737,7 +825,7 @@ def to_schedules(self): ) csp_year = YearComponent( - Name="CSP_Year", + Name=f"CSP_Year{sfx}", Type="Setpoint", January=csp_standard_week, February=csp_standard_week, @@ -756,7 +844,1221 @@ def to_schedules(self): return hsp_year, csp_year -class FlatModel(BaseModel): +def zone_params_to_zone_component( + params: ZoneParams, *, id_tag: str = "" +) -> ZoneComponent: + """Build a ZoneComponent from flat scalar zone parameters. + + Args: + params: Zone calibration scalars. + id_tag: optional suffix for facade/window/envelope names so multiple zones can differ. + """ + sfx = ("_" + id_tag.replace(" ", "_")) if id_tag else "" + # occ_regular_workday = DayComponent( + # Name=f"Occupancy_Regular_Workday{sfx}", + # Type="Fraction", + # Hour_00=params.OccupancyRegularWeekdayNight, + # Hour_01=params.OccupancyRegularWeekdayNight, + # Hour_02=params.OccupancyRegularWeekdayNight, + # Hour_03=params.OccupancyRegularWeekdayNight, + # Hour_04=params.OccupancyRegularWeekdayNight, + # Hour_05=params.OccupancyRegularWeekdayNight, + # Hour_06=params.OccupancyRegularWeekdayEarlyMorning, + # Hour_07=params.OccupancyRegularWeekdayEarlyMorning, + # Hour_08=params.OccupancyRegularWeekdayEarlyMorning, + # Hour_09=params.OccupancyRegularWeekdayMorning, + # Hour_10=params.OccupancyRegularWeekdayMorning, + # Hour_11=params.OccupancyRegularWeekdayMorning, + # Hour_12=params.OccupancyRegularWeekdayLunch, + # Hour_13=params.OccupancyRegularWeekdayLunch, + # Hour_14=params.OccupancyRegularWeekdayAfternoon, + # Hour_15=params.OccupancyRegularWeekdayAfternoon, + # Hour_16=params.OccupancyRegularWeekdayAfternoon, + # Hour_17=params.OccupancyRegularWeekdayAfternoon, + # Hour_18=params.OccupancyRegularWeekdayEvening, + # Hour_19=params.OccupancyRegularWeekdayEvening, + # Hour_20=params.OccupancyRegularWeekdayEvening, + # Hour_21=params.OccupancyRegularWeekdayNight, + # Hour_22=params.OccupancyRegularWeekdayNight, + # Hour_23=params.OccupancyRegularWeekdayNight, + # ) + + # occ_regular_weekend = DayComponent( + # Name=f"Occupancy_Regular_Weekend{sfx}", + # Type="Fraction", + # Hour_00=params.OccupancyRegularWeekendNight, + # Hour_01=params.OccupancyRegularWeekendNight, + # Hour_02=params.OccupancyRegularWeekendNight, + # Hour_03=params.OccupancyRegularWeekendNight, + # Hour_04=params.OccupancyRegularWeekendNight, + # Hour_05=params.OccupancyRegularWeekendNight, + # Hour_06=params.OccupancyRegularWeekendEarlyMorning, + # Hour_07=params.OccupancyRegularWeekendEarlyMorning, + # Hour_08=params.OccupancyRegularWeekendEarlyMorning, + # Hour_09=params.OccupancyRegularWeekendMorning, + # Hour_10=params.OccupancyRegularWeekendMorning, + # Hour_11=params.OccupancyRegularWeekendMorning, + # Hour_12=params.OccupancyRegularWeekendLunch, + # Hour_13=params.OccupancyRegularWeekendLunch, + # Hour_14=params.OccupancyRegularWeekendAfternoon, + # Hour_15=params.OccupancyRegularWeekendAfternoon, + # Hour_16=params.OccupancyRegularWeekendAfternoon, + # Hour_17=params.OccupancyRegularWeekendAfternoon, + # Hour_18=params.OccupancyRegularWeekendEvening, + # Hour_19=params.OccupancyRegularWeekendEvening, + # Hour_20=params.OccupancyRegularWeekendEvening, + # Hour_21=params.OccupancyRegularWeekendNight, + # Hour_22=params.OccupancyRegularWeekendNight, + # Hour_23=params.OccupancyRegularWeekendNight, + # ) + + # occ_summer_workday = DayComponent( + # Name=f"Occupancy_Summer_Workday{sfx}", + # Type="Fraction", + # Hour_00=params.OccupancySummerWeekdayNight, + # Hour_01=params.OccupancySummerWeekdayNight, + # Hour_02=params.OccupancySummerWeekdayNight, + # Hour_03=params.OccupancySummerWeekdayNight, + # Hour_04=params.OccupancySummerWeekdayNight, + # Hour_05=params.OccupancySummerWeekdayNight, + # Hour_06=params.OccupancySummerWeekdayEarlyMorning, + # Hour_07=params.OccupancySummerWeekdayEarlyMorning, + # Hour_08=params.OccupancySummerWeekdayEarlyMorning, + # Hour_09=params.OccupancySummerWeekdayMorning, + # Hour_10=params.OccupancySummerWeekdayMorning, + # Hour_11=params.OccupancySummerWeekdayMorning, + # Hour_12=params.OccupancySummerWeekdayLunch, + # Hour_13=params.OccupancySummerWeekdayLunch, + # Hour_14=params.OccupancySummerWeekdayAfternoon, + # Hour_15=params.OccupancySummerWeekdayAfternoon, + # Hour_16=params.OccupancySummerWeekdayAfternoon, + # Hour_17=params.OccupancySummerWeekdayAfternoon, + # Hour_18=params.OccupancySummerWeekdayEvening, + # Hour_19=params.OccupancySummerWeekdayEvening, + # Hour_20=params.OccupancySummerWeekdayEvening, + # Hour_21=params.OccupancySummerWeekdayNight, + # Hour_22=params.OccupancySummerWeekdayNight, + # Hour_23=params.OccupancySummerWeekdayNight, + # ) + + # occ_summer_weekend = DayComponent( + # Name=f"Occupancy_Summer_Weekend{sfx}", + # Type="Fraction", + # Hour_00=params.OccupancySummerWeekendNight, + # Hour_01=params.OccupancySummerWeekendNight, + # Hour_02=params.OccupancySummerWeekendNight, + # Hour_03=params.OccupancySummerWeekendNight, + # Hour_04=params.OccupancySummerWeekendNight, + # Hour_05=params.OccupancySummerWeekendNight, + # Hour_06=params.OccupancySummerWeekendEarlyMorning, + # Hour_07=params.OccupancySummerWeekendEarlyMorning, + # Hour_08=params.OccupancySummerWeekendEarlyMorning, + # Hour_09=params.OccupancySummerWeekendMorning, + # Hour_10=params.OccupancySummerWeekendMorning, + # Hour_11=params.OccupancySummerWeekendMorning, + # Hour_12=params.OccupancySummerWeekendLunch, + # Hour_13=params.OccupancySummerWeekendLunch, + # Hour_14=params.OccupancySummerWeekendAfternoon, + # Hour_15=params.OccupancySummerWeekendAfternoon, + # Hour_16=params.OccupancySummerWeekendAfternoon, + # Hour_17=params.OccupancySummerWeekendAfternoon, + # Hour_18=params.OccupancySummerWeekendEvening, + # Hour_19=params.OccupancySummerWeekendEvening, + # Hour_20=params.OccupancySummerWeekendEvening, + # Hour_21=params.OccupancySummerWeekendNight, + # Hour_22=params.OccupancySummerWeekendNight, + # Hour_23=params.OccupancySummerWeekendNight, + # ) + + # occ_regular_week = WeekComponent( + # Name=f"Occupancy_Regular_Week{sfx}", + # Monday=occ_regular_workday, + # Tuesday=occ_regular_workday, + # Wednesday=occ_regular_workday, + # Thursday=occ_regular_workday, + # Friday=occ_regular_workday, + # Saturday=occ_regular_weekend, + # Sunday=occ_regular_weekend, + # ) + + # occ_summer_week = WeekComponent( + # Name=f"Occupancy_Summer_Week{sfx}", + # Monday=occ_summer_workday, + # Tuesday=occ_summer_workday, + # Wednesday=occ_summer_workday, + # Thursday=occ_summer_workday, + # Friday=occ_summer_workday, + # Saturday=occ_summer_weekend, + # Sunday=occ_summer_weekend, + # ) + + # occ_year = YearComponent( + # Name=f"Occupancy_Schedule{sfx}", + # Type="Occupancy", + # January=occ_regular_week, + # February=occ_regular_week, + # March=occ_regular_week, + # April=occ_regular_week, + # May=occ_regular_week, + # June=occ_summer_week, + # July=occ_summer_week, + # August=occ_summer_week, + # September=occ_regular_week, + # October=occ_regular_week, + # November=occ_regular_week, + # December=occ_regular_week, + # ) + + # lighting_regular_workday = DayComponent( + # Name=f"Lighting_Regular_Workday{sfx}", + # Type="Fraction", + # Hour_00=params.LightingRegularWeekdayNight, + # Hour_01=params.LightingRegularWeekdayNight, + # Hour_02=params.LightingRegularWeekdayNight, + # Hour_03=params.LightingRegularWeekdayNight, + # Hour_04=params.LightingRegularWeekdayNight, + # Hour_05=params.LightingRegularWeekdayNight, + # Hour_06=params.LightingRegularWeekdayEarlyMorning, + # Hour_07=params.LightingRegularWeekdayEarlyMorning, + # Hour_08=params.LightingRegularWeekdayEarlyMorning, + # Hour_09=params.LightingRegularWeekdayMorning, + # Hour_10=params.LightingRegularWeekdayMorning, + # Hour_11=params.LightingRegularWeekdayMorning, + # Hour_12=params.LightingRegularWeekdayLunch, + # Hour_13=params.LightingRegularWeekdayLunch, + # Hour_14=params.LightingRegularWeekdayAfternoon, + # Hour_15=params.LightingRegularWeekdayAfternoon, + # Hour_16=params.LightingRegularWeekdayAfternoon, + # Hour_17=params.LightingRegularWeekdayAfternoon, + # Hour_18=params.LightingRegularWeekdayEvening, + # Hour_19=params.LightingRegularWeekdayEvening, + # Hour_20=params.LightingRegularWeekdayEvening, + # Hour_21=params.LightingRegularWeekdayNight, + # Hour_22=params.LightingRegularWeekdayNight, + # Hour_23=params.LightingRegularWeekdayNight, + # ) + + # lighting_regular_weekend = DayComponent( + # Name=f"Lighting_Regular_Weekend{sfx}", + # Type="Fraction", + # Hour_00=params.LightingRegularWeekendNight, + # Hour_01=params.LightingRegularWeekendNight, + # Hour_02=params.LightingRegularWeekendNight, + # Hour_03=params.LightingRegularWeekendNight, + # Hour_04=params.LightingRegularWeekendNight, + # Hour_05=params.LightingRegularWeekendNight, + # Hour_06=params.LightingRegularWeekendEarlyMorning, + # Hour_07=params.LightingRegularWeekendEarlyMorning, + # Hour_08=params.LightingRegularWeekendEarlyMorning, + # Hour_09=params.LightingRegularWeekendMorning, + # Hour_10=params.LightingRegularWeekendMorning, + # Hour_11=params.LightingRegularWeekendMorning, + # Hour_12=params.LightingRegularWeekendLunch, + # Hour_13=params.LightingRegularWeekendLunch, + # Hour_14=params.LightingRegularWeekendAfternoon, + # Hour_15=params.LightingRegularWeekendAfternoon, + # Hour_16=params.LightingRegularWeekendAfternoon, + # Hour_17=params.LightingRegularWeekendAfternoon, + # Hour_18=params.LightingRegularWeekendEvening, + # Hour_19=params.LightingRegularWeekendEvening, + # Hour_20=params.LightingRegularWeekendEvening, + # Hour_21=params.LightingRegularWeekendNight, + # Hour_22=params.LightingRegularWeekendNight, + # Hour_23=params.LightingRegularWeekendNight, + # ) + + # lighting_summer_workday = DayComponent( + # Name=f"Lighting_Summer_Workday{sfx}", + # Type="Fraction", + # Hour_00=params.LightingSummerWeekdayNight, + # Hour_01=params.LightingSummerWeekdayNight, + # Hour_02=params.LightingSummerWeekdayNight, + # Hour_03=params.LightingSummerWeekdayNight, + # Hour_04=params.LightingSummerWeekdayNight, + # Hour_05=params.LightingSummerWeekdayNight, + # Hour_06=params.LightingSummerWeekdayEarlyMorning, + # Hour_07=params.LightingSummerWeekdayEarlyMorning, + # Hour_08=params.LightingSummerWeekdayEarlyMorning, + # Hour_09=params.LightingSummerWeekdayMorning, + # Hour_10=params.LightingSummerWeekdayMorning, + # Hour_11=params.LightingSummerWeekdayMorning, + # Hour_12=params.LightingSummerWeekdayLunch, + # Hour_13=params.LightingSummerWeekdayLunch, + # Hour_14=params.LightingSummerWeekdayAfternoon, + # Hour_15=params.LightingSummerWeekdayAfternoon, + # Hour_16=params.LightingSummerWeekdayAfternoon, + # Hour_17=params.LightingSummerWeekdayAfternoon, + # Hour_18=params.LightingSummerWeekdayEvening, + # Hour_19=params.LightingSummerWeekdayEvening, + # Hour_20=params.LightingSummerWeekdayEvening, + # Hour_21=params.LightingSummerWeekdayNight, + # Hour_22=params.LightingSummerWeekdayNight, + # Hour_23=params.LightingSummerWeekdayNight, + # ) + + # lighting_summer_weekend = DayComponent( + # Name=f"Lighting_Summer_Weekend{sfx}", + # Type="Fraction", + # Hour_00=params.LightingSummerWeekendNight, + # Hour_01=params.LightingSummerWeekendNight, + # Hour_02=params.LightingSummerWeekendNight, + # Hour_03=params.LightingSummerWeekendNight, + # Hour_04=params.LightingSummerWeekendNight, + # Hour_05=params.LightingSummerWeekendNight, + # Hour_06=params.LightingSummerWeekendEarlyMorning, + # Hour_07=params.LightingSummerWeekendEarlyMorning, + # Hour_08=params.LightingSummerWeekendEarlyMorning, + # Hour_09=params.LightingSummerWeekendMorning, + # Hour_10=params.LightingSummerWeekendMorning, + # Hour_11=params.LightingSummerWeekendMorning, + # Hour_12=params.LightingSummerWeekendLunch, + # Hour_13=params.LightingSummerWeekendLunch, + # Hour_14=params.LightingSummerWeekendAfternoon, + # Hour_15=params.LightingSummerWeekendAfternoon, + # Hour_16=params.LightingSummerWeekendAfternoon, + # Hour_17=params.LightingSummerWeekendAfternoon, + # Hour_18=params.LightingSummerWeekendEvening, + # Hour_19=params.LightingSummerWeekendEvening, + # Hour_20=params.LightingSummerWeekendEvening, + # Hour_21=params.LightingSummerWeekendNight, + # Hour_22=params.LightingSummerWeekendNight, + # Hour_23=params.LightingSummerWeekendNight, + # ) + + # lighting_regular_week = WeekComponent( + # Name=f"Lighting_Regular_Week{sfx}", + # Monday=lighting_regular_workday, + # Tuesday=lighting_regular_workday, + # Wednesday=lighting_regular_workday, + # Thursday=lighting_regular_workday, + # Friday=lighting_regular_workday, + # Saturday=lighting_regular_weekend, + # Sunday=lighting_regular_weekend, + # ) + + # lighting_summer_week = WeekComponent( + # Name=f"Lighting_Summer_Week{sfx}", + # Monday=lighting_summer_workday, + # Tuesday=lighting_summer_workday, + # Wednesday=lighting_summer_workday, + # Thursday=lighting_summer_workday, + # Friday=lighting_summer_workday, + # Saturday=lighting_summer_weekend, + # Sunday=lighting_summer_weekend, + # ) + + # lighting_year = YearComponent( + # Name=f"Lighting_Schedule{sfx}", + # Type="Lighting", + # January=lighting_regular_week, + # February=lighting_regular_week, + # March=lighting_regular_week, + # April=lighting_regular_week, + # May=lighting_regular_week, + # June=lighting_summer_week, + # July=lighting_summer_week, + # August=lighting_summer_week, + # September=lighting_regular_week, + # October=lighting_regular_week, + # November=lighting_regular_week, + # December=lighting_regular_week, + # ) + + # equipment_regular_workday = DayComponent( + # Name=f"Equipment_Regular_Workday{sfx}", + # Type="Fraction", + # Hour_00=params.EquipmentRegularWeekdayNight, + # Hour_01=params.EquipmentRegularWeekdayNight, + # Hour_02=params.EquipmentRegularWeekdayNight, + # Hour_03=params.EquipmentRegularWeekdayNight, + # Hour_04=params.EquipmentRegularWeekdayNight, + # Hour_05=params.EquipmentRegularWeekdayNight, + # Hour_06=params.EquipmentRegularWeekdayEarlyMorning, + # Hour_07=params.EquipmentRegularWeekdayEarlyMorning, + # Hour_08=params.EquipmentRegularWeekdayEarlyMorning, + # Hour_09=params.EquipmentRegularWeekdayMorning, + # Hour_10=params.EquipmentRegularWeekdayMorning, + # Hour_11=params.EquipmentRegularWeekdayMorning, + # Hour_12=params.EquipmentRegularWeekdayLunch, + # Hour_13=params.EquipmentRegularWeekdayLunch, + # Hour_14=params.EquipmentRegularWeekdayAfternoon, + # Hour_15=params.EquipmentRegularWeekdayAfternoon, + # Hour_16=params.EquipmentRegularWeekdayAfternoon, + # Hour_17=params.EquipmentRegularWeekdayAfternoon, + # Hour_18=params.EquipmentRegularWeekdayEvening, + # Hour_19=params.EquipmentRegularWeekdayEvening, + # Hour_20=params.EquipmentRegularWeekdayEvening, + # Hour_21=params.EquipmentRegularWeekdayNight, + # Hour_22=params.EquipmentRegularWeekdayNight, + # Hour_23=params.EquipmentRegularWeekdayNight, + # ) + + # equipment_regular_weekend = DayComponent( + # Name=f"Equipment_Regular_Weekend{sfx}", + # Type="Fraction", + # Hour_00=params.EquipmentRegularWeekendNight, + # Hour_01=params.EquipmentRegularWeekendNight, + # Hour_02=params.EquipmentRegularWeekendNight, + # Hour_03=params.EquipmentRegularWeekendNight, + # Hour_04=params.EquipmentRegularWeekendNight, + # Hour_05=params.EquipmentRegularWeekendNight, + # Hour_06=params.EquipmentRegularWeekendEarlyMorning, + # Hour_07=params.EquipmentRegularWeekendEarlyMorning, + # Hour_08=params.EquipmentRegularWeekendEarlyMorning, + # Hour_09=params.EquipmentRegularWeekendMorning, + # Hour_10=params.EquipmentRegularWeekendMorning, + # Hour_11=params.EquipmentRegularWeekendMorning, + # Hour_12=params.EquipmentRegularWeekendLunch, + # Hour_13=params.EquipmentRegularWeekendLunch, + # Hour_14=params.EquipmentRegularWeekendAfternoon, + # Hour_15=params.EquipmentRegularWeekendAfternoon, + # Hour_16=params.EquipmentRegularWeekendAfternoon, + # Hour_17=params.EquipmentRegularWeekendAfternoon, + # Hour_18=params.EquipmentRegularWeekendEvening, + # Hour_19=params.EquipmentRegularWeekendEvening, + # Hour_20=params.EquipmentRegularWeekendEvening, + # Hour_21=params.EquipmentRegularWeekendNight, + # Hour_22=params.EquipmentRegularWeekendNight, + # Hour_23=params.EquipmentRegularWeekendNight, + # ) + + # equipment_summer_workday = DayComponent( + # Name=f"Equipment_Summer_Workday{sfx}", + # Type="Fraction", + # Hour_00=params.EquipmentSummerWeekdayNight, + # Hour_01=params.EquipmentSummerWeekdayNight, + # Hour_02=params.EquipmentSummerWeekdayNight, + # Hour_03=params.EquipmentSummerWeekdayNight, + # Hour_04=params.EquipmentSummerWeekdayNight, + # Hour_05=params.EquipmentSummerWeekdayNight, + # Hour_06=params.EquipmentSummerWeekdayEarlyMorning, + # Hour_07=params.EquipmentSummerWeekdayEarlyMorning, + # Hour_08=params.EquipmentSummerWeekdayEarlyMorning, + # Hour_09=params.EquipmentSummerWeekdayMorning, + # Hour_10=params.EquipmentSummerWeekdayMorning, + # Hour_11=params.EquipmentSummerWeekdayMorning, + # Hour_12=params.EquipmentSummerWeekdayLunch, + # Hour_13=params.EquipmentSummerWeekdayLunch, + # Hour_14=params.EquipmentSummerWeekdayAfternoon, + # Hour_15=params.EquipmentSummerWeekdayAfternoon, + # Hour_16=params.EquipmentSummerWeekdayAfternoon, + # Hour_17=params.EquipmentSummerWeekdayAfternoon, + # Hour_18=params.EquipmentSummerWeekdayEvening, + # Hour_19=params.EquipmentSummerWeekdayEvening, + # Hour_20=params.EquipmentSummerWeekdayEvening, + # Hour_21=params.EquipmentSummerWeekdayNight, + # Hour_22=params.EquipmentSummerWeekdayNight, + # Hour_23=params.EquipmentSummerWeekdayNight, + # ) + + # equipment_summer_weekend = DayComponent( + # Name=f"Equipment_Summer_Weekend{sfx}", + # Type="Fraction", + # Hour_00=params.EquipmentSummerWeekendNight, + # Hour_01=params.EquipmentSummerWeekendNight, + # Hour_02=params.EquipmentSummerWeekendNight, + # Hour_03=params.EquipmentSummerWeekendNight, + # Hour_04=params.EquipmentSummerWeekendNight, + # Hour_05=params.EquipmentSummerWeekendNight, + # Hour_06=params.EquipmentSummerWeekendEarlyMorning, + # Hour_07=params.EquipmentSummerWeekendEarlyMorning, + # Hour_08=params.EquipmentSummerWeekendEarlyMorning, + # Hour_09=params.EquipmentSummerWeekendMorning, + # Hour_10=params.EquipmentSummerWeekendMorning, + # Hour_11=params.EquipmentSummerWeekendMorning, + # Hour_12=params.EquipmentSummerWeekendLunch, + # Hour_13=params.EquipmentSummerWeekendLunch, + # Hour_14=params.EquipmentSummerWeekendAfternoon, + # Hour_15=params.EquipmentSummerWeekendAfternoon, + # Hour_16=params.EquipmentSummerWeekendAfternoon, + # Hour_17=params.EquipmentSummerWeekendAfternoon, + # Hour_18=params.EquipmentSummerWeekendEvening, + # Hour_19=params.EquipmentSummerWeekendEvening, + # Hour_20=params.EquipmentSummerWeekendEvening, + # Hour_21=params.EquipmentSummerWeekendNight, + # Hour_22=params.EquipmentSummerWeekendNight, + # Hour_23=params.EquipmentSummerWeekendNight, + # ) + + # equipment_regular_week = WeekComponent( + # Name=f"Equipment_Regular_Week{sfx}", + # Monday=equipment_regular_workday, + # Tuesday=equipment_regular_workday, + # Wednesday=equipment_regular_workday, + # Thursday=equipment_regular_workday, + # Friday=equipment_regular_workday, + # Saturday=equipment_regular_weekend, + # Sunday=equipment_regular_weekend, + # ) + + # equipment_summer_week = WeekComponent( + # Name=f"Equipment_Summer_Week{sfx}", + # Monday=equipment_summer_workday, + # Tuesday=equipment_summer_workday, + # Wednesday=equipment_summer_workday, + # Thursday=equipment_summer_workday, + # Friday=equipment_summer_workday, + # Saturday=equipment_summer_weekend, + # Sunday=equipment_summer_weekend, + # ) + + # equipment_year = YearComponent( + # Name=f"equipment_Schedule{sfx}", + # Type="Equipment", + # January=equipment_regular_week, + # February=equipment_regular_week, + # March=equipment_regular_week, + # April=equipment_regular_week, + # May=equipment_regular_week, + # June=equipment_summer_week, + # July=equipment_summer_week, + # August=equipment_summer_week, + # September=equipment_regular_week, + # October=equipment_regular_week, + # November=lighting_regular_week, + # December=equipment_regular_week, + # ) + + equipment_paramteric = ParametericYear( + Base=params.EquipmentBase, + AMInterp=params.EquipmentAMInterp, + LunchInterp=params.EquipmentLunchInterp, + PMInterp=params.EquipmentPMInterp, + WeekendPeakInterp=params.EquipmentWeekendPeakInterp, + SummerPeakInterp=params.EquipmentSummerPeakInterp, + ) + + lighting_paramteric = ParametericYear( + Base=params.LightingBase, + AMInterp=params.LightingAMInterp, + LunchInterp=params.LightingLunchInterp, + PMInterp=params.LightingPMInterp, + WeekendPeakInterp=params.LightingWeekendPeakInterp, + SummerPeakInterp=params.LightingSummerPeakInterp, + ) + + occupancy_paramteric = ParametericYear( + Base=params.OccupancyBase, + AMInterp=params.OccupancyAMInterp, + LunchInterp=params.OccupancyLunchInterp, + PMInterp=params.OccupancyPMInterp, + WeekendPeakInterp=params.OccupancyWeekendPeakInterp, + SummerPeakInterp=params.OccupancySummerPeakInterp, + ) + + if params.CustomEquipmentWeekdayHourly is not None: + equipment_schedule = year_schedule_from_weekday_hourly( + f"Equipment{sfx}", + "Equipment", + params.CustomEquipmentWeekdayHourly, + ) + else: + equipment_schedule = equipment_paramteric.to_schedule( + name=f"Equipment{sfx}", category="Equipment" + ) + if params.CustomLightingWeekdayHourly is not None: + lighting_schedule = year_schedule_from_weekday_hourly( + f"Lighting{sfx}", + "Lighting", + params.CustomLightingWeekdayHourly, + ) + else: + lighting_schedule = lighting_paramteric.to_schedule( + name=f"Lighting{sfx}", category="Lighting" + ) + if params.CustomOccupancyWeekdayHourly is not None: + occupancy_schedule = year_schedule_from_weekday_hourly( + f"Occupancy{sfx}", + "Occupancy", + params.CustomOccupancyWeekdayHourly, + ) + else: + occupancy_schedule = occupancy_paramteric.to_schedule( + name=f"Occupancy{sfx}", category="Occupancy" + ) + + # hsp_regular_workday = DayComponent( + # Name=f"HeatingSetpoint_Regular_Workday{sfx}", + # Type="Temperature", + # Hour_00=params.HSPRegularWeekdayNight, + # Hour_01=params.HSPRegularWeekdayNight, + # Hour_02=params.HSPRegularWeekdayNight, + # Hour_03=params.HSPRegularWeekdayNight, + # Hour_04=params.HSPRegularWeekdayNight, + # Hour_05=params.HSPRegularWeekdayNight, + # Hour_06=params.HSPRegularWeekdayWorkhours, + # Hour_07=params.HSPRegularWeekdayWorkhours, + # Hour_08=params.HSPRegularWeekdayWorkhours, + # Hour_09=params.HSPRegularWeekdayWorkhours, + # Hour_10=params.HSPRegularWeekdayWorkhours, + # Hour_11=params.HSPRegularWeekdayWorkhours, + # Hour_12=params.HSPRegularWeekdayWorkhours, + # Hour_13=params.HSPRegularWeekdayWorkhours, + # Hour_14=params.HSPRegularWeekdayWorkhours, + # Hour_15=params.HSPRegularWeekdayWorkhours, + # Hour_16=params.HSPRegularWeekdayWorkhours, + # Hour_17=params.HSPRegularWeekdayWorkhours, + # Hour_18=params.HSPRegularWeekdayWorkhours, + # Hour_19=params.HSPRegularWeekdayNight, + # Hour_20=params.HSPRegularWeekdayNight, + # Hour_21=params.HSPRegularWeekdayNight, + # Hour_22=params.HSPRegularWeekdayNight, + # Hour_23=params.HSPRegularWeekdayNight, + # ) + + # hsp_regular_weekend = DayComponent( + # Name=f"HeatingSetpoint_Regular_Weekend{sfx}", + # Type="Temperature", + # Hour_00=params.HSPWeekendNight, + # Hour_01=params.HSPWeekendNight, + # Hour_02=params.HSPWeekendNight, + # Hour_03=params.HSPWeekendNight, + # Hour_04=params.HSPWeekendNight, + # Hour_05=params.HSPWeekendNight, + # Hour_06=params.HSPWeekendWorkhours, + # Hour_07=params.HSPWeekendWorkhours, + # Hour_08=params.HSPWeekendWorkhours, + # Hour_09=params.HSPWeekendWorkhours, + # Hour_10=params.HSPWeekendWorkhours, + # Hour_11=params.HSPWeekendWorkhours, + # Hour_12=params.HSPWeekendWorkhours, + # Hour_13=params.HSPWeekendWorkhours, + # Hour_14=params.HSPWeekendWorkhours, + # Hour_15=params.HSPWeekendWorkhours, + # Hour_16=params.HSPWeekendWorkhours, + # Hour_17=params.HSPWeekendWorkhours, + # Hour_18=params.HSPWeekendWorkhours, + # Hour_19=params.HSPWeekendNight, + # Hour_20=params.HSPWeekendNight, + # Hour_21=params.HSPWeekendNight, + # Hour_22=params.HSPWeekendNight, + # Hour_23=params.HSPWeekendNight, + # ) + + # hsp_summer_workday = DayComponent( + # Name=f"HeatingSetpoint_Summer_Workday{sfx}", + # Type="Temperature", + # Hour_00=params.HSPSummerWeekdayNight, + # Hour_01=params.HSPSummerWeekdayNight, + # Hour_02=params.HSPSummerWeekdayNight, + # Hour_03=params.HSPSummerWeekdayNight, + # Hour_04=params.HSPSummerWeekdayNight, + # Hour_05=params.HSPSummerWeekdayNight, + # Hour_06=params.HSPSummerWeekdayWorkhours, + # Hour_07=params.HSPSummerWeekdayWorkhours, + # Hour_08=params.HSPSummerWeekdayWorkhours, + # Hour_09=params.HSPSummerWeekdayWorkhours, + # Hour_10=params.HSPSummerWeekdayWorkhours, + # Hour_11=params.HSPSummerWeekdayWorkhours, + # Hour_12=params.HSPSummerWeekdayWorkhours, + # Hour_13=params.HSPSummerWeekdayWorkhours, + # Hour_14=params.HSPSummerWeekdayWorkhours, + # Hour_15=params.HSPSummerWeekdayWorkhours, + # Hour_16=params.HSPSummerWeekdayWorkhours, + # Hour_17=params.HSPSummerWeekdayWorkhours, + # Hour_18=params.HSPSummerWeekdayWorkhours, + # Hour_19=params.HSPSummerWeekdayNight, + # Hour_20=params.HSPSummerWeekdayNight, + # Hour_21=params.HSPSummerWeekdayNight, + # Hour_22=params.HSPSummerWeekdayNight, + # Hour_23=params.HSPSummerWeekdayNight, + # ) + + # hsp_regular_week = WeekComponent( + # Name=f"HeatingSetpoint_Regular_Week{sfx}", + # Monday=hsp_regular_workday, + # Tuesday=hsp_regular_workday, + # Wednesday=hsp_regular_workday, + # Thursday=hsp_regular_workday, + # Friday=hsp_regular_workday, + # Saturday=hsp_regular_weekend, + # Sunday=hsp_regular_weekend, + # ) + + # hsp_summer_week = WeekComponent( + # Name=f"HeatingSetpoint_Summer_Week{sfx}", + # Monday=hsp_summer_workday, + # Tuesday=hsp_summer_workday, + # Wednesday=hsp_summer_workday, + # Thursday=hsp_summer_workday, + # Friday=hsp_summer_workday, + # Saturday=hsp_regular_weekend, + # Sunday=hsp_regular_weekend, + # ) + + # hsp_year = YearComponent( + # Name=f"HeatingSetpoint_Schedule{sfx}", + # Type="Setpoint", + # January=hsp_regular_week, + # February=hsp_regular_week, + # March=hsp_regular_week, + # April=hsp_regular_week, + # May=hsp_regular_week, + # June=hsp_summer_week, + # July=hsp_summer_week, + # August=hsp_summer_week, + # September=hsp_regular_week, + # October=hsp_regular_week, + # November=hsp_regular_week, + # December=hsp_regular_week, + # ) + + # csp_regular_workday = DayComponent( + # Name=f"CoolingSetpoint_Regular_Workday{sfx}", + # Type="Temperature", + # Hour_00=params.CSPRegularWeekdayNight, + # Hour_01=params.CSPRegularWeekdayNight, + # Hour_02=params.CSPRegularWeekdayNight, + # Hour_03=params.CSPRegularWeekdayNight, + # Hour_04=params.CSPRegularWeekdayNight, + # Hour_05=params.CSPRegularWeekdayNight, + # Hour_06=params.CSPRegularWeekdayWorkhours, + # Hour_07=params.CSPRegularWeekdayWorkhours, + # Hour_08=params.CSPRegularWeekdayWorkhours, + # Hour_09=params.CSPRegularWeekdayWorkhours, + # Hour_10=params.CSPRegularWeekdayWorkhours, + # Hour_11=params.CSPRegularWeekdayWorkhours, + # Hour_12=params.CSPRegularWeekdayWorkhours, + # Hour_13=params.CSPRegularWeekdayWorkhours, + # Hour_14=params.CSPRegularWeekdayWorkhours, + # Hour_15=params.CSPRegularWeekdayWorkhours, + # Hour_16=params.CSPRegularWeekdayWorkhours, + # Hour_17=params.CSPRegularWeekdayWorkhours, + # Hour_18=params.CSPRegularWeekdayWorkhours, + # Hour_19=params.CSPRegularWeekdayNight, + # Hour_20=params.CSPRegularWeekdayNight, + # Hour_21=params.CSPRegularWeekdayNight, + # Hour_22=params.CSPRegularWeekdayNight, + # Hour_23=params.CSPRegularWeekdayNight, + # ) + + # csp_regular_weekend = DayComponent( + # Name=f"CoolingSetpoint_Regular_Weekend{sfx}", + # Type="Temperature", + # Hour_00=params.CSPWeekendNight, + # Hour_01=params.CSPWeekendNight, + # Hour_02=params.CSPWeekendNight, + # Hour_03=params.CSPWeekendNight, + # Hour_04=params.CSPWeekendNight, + # Hour_05=params.CSPWeekendNight, + # Hour_06=params.CSPWeekendWorkhours, + # Hour_07=params.CSPWeekendWorkhours, + # Hour_08=params.CSPWeekendWorkhours, + # Hour_09=params.CSPWeekendWorkhours, + # Hour_10=params.CSPWeekendWorkhours, + # Hour_11=params.CSPWeekendWorkhours, + # Hour_12=params.CSPWeekendWorkhours, + # Hour_13=params.CSPWeekendWorkhours, + # Hour_14=params.CSPWeekendWorkhours, + # Hour_15=params.CSPWeekendWorkhours, + # Hour_16=params.CSPWeekendWorkhours, + # Hour_17=params.CSPWeekendWorkhours, + # Hour_18=params.CSPWeekendWorkhours, + # Hour_19=params.CSPWeekendNight, + # Hour_20=params.CSPWeekendNight, + # Hour_21=params.CSPWeekendNight, + # Hour_22=params.CSPWeekendNight, + # Hour_23=params.CSPWeekendNight, + # ) + + # csp_summer_workday = DayComponent( + # Name=f"CoolingSetpoint_Summer_Workday{sfx}", + # Type="Temperature", + # Hour_00=params.CSPSummerWeekdayNight, + # Hour_01=params.CSPSummerWeekdayNight, + # Hour_02=params.CSPSummerWeekdayNight, + # Hour_03=params.CSPSummerWeekdayNight, + # Hour_04=params.CSPSummerWeekdayNight, + # Hour_05=params.CSPSummerWeekdayNight, + # Hour_06=params.CSPSummerWeekdayWorkhours, + # Hour_07=params.CSPSummerWeekdayWorkhours, + # Hour_08=params.CSPSummerWeekdayWorkhours, + # Hour_09=params.CSPSummerWeekdayWorkhours, + # Hour_10=params.CSPSummerWeekdayWorkhours, + # Hour_11=params.CSPSummerWeekdayWorkhours, + # Hour_12=params.CSPSummerWeekdayWorkhours, + # Hour_13=params.CSPSummerWeekdayWorkhours, + # Hour_14=params.CSPSummerWeekdayWorkhours, + # Hour_15=params.CSPSummerWeekdayWorkhours, + # Hour_16=params.CSPSummerWeekdayWorkhours, + # Hour_17=params.CSPSummerWeekdayWorkhours, + # Hour_18=params.CSPSummerWeekdayWorkhours, + # Hour_19=params.CSPSummerWeekdayNight, + # Hour_20=params.CSPSummerWeekdayNight, + # Hour_21=params.CSPSummerWeekdayNight, + # Hour_22=params.CSPSummerWeekdayNight, + # Hour_23=params.CSPSummerWeekdayNight, + # ) + + # csp_regular_week = WeekComponent( + # Name=f"CoolingSetpoint_Regular_Week{sfx}", + # Monday=csp_regular_workday, + # Tuesday=csp_regular_workday, + # Wednesday=csp_regular_workday, + # Thursday=csp_regular_workday, + # Friday=csp_regular_workday, + # Saturday=csp_regular_weekend, + # Sunday=csp_regular_weekend, + # ) + + # csp_summer_week = WeekComponent( + # Name=f"CoolingSetpoint_Summer_Week{sfx}", + # Monday=csp_summer_workday, + # Tuesday=csp_summer_workday, + # Wednesday=csp_summer_workday, + # Thursday=csp_summer_workday, + # Friday=csp_summer_workday, + # Saturday=csp_regular_weekend, + # Sunday=csp_regular_weekend, + # ) + + # csp_year = YearComponent( + # Name=f"CoolingSetpoint_Schedule{sfx}", + # Type="Setpoint", + # January=csp_regular_week, + # February=csp_regular_week, + # March=csp_regular_week, + # April=csp_regular_week, + # May=csp_regular_week, + # June=csp_summer_week, + # July=csp_summer_week, + # August=csp_summer_week, + # September=csp_regular_week, + # October=csp_regular_week, + # November=csp_regular_week, + # December=csp_regular_week, + # ) + + setpoint_parametric = ParametricSetpoints( + HeatingSetpoint=params.HeatingSetpointBase, + DeadBand=params.SetpointDeadband, + HeatingSetback=params.HeatingSetpointSetback, + CoolingSetback=params.CoolingSetpointSetback, + NightSetback=params.NightSetback, + WeekendSetback=params.WeekendSetback, + SummerSetback=params.SummerSetback, + ) + + hsp_year, csp_year = setpoint_parametric.to_schedules(name_suffix=sfx) + + thermostat = ThermostatComponent( + Name=f"Thermostat{sfx}", + IsOn=True, + HeatingSetpoint=hsp_year.January.Monday.Hour_12, + CoolingSetpoint=csp_year.January.Monday.Hour_12, + HeatingSchedule=hsp_year, + CoolingSchedule=csp_year, + ) + + equipment = EquipmentComponent( + Name=f"Equipment{sfx}", + PowerDensity=params.EquipmentPowerDensity, + Schedule=equipment_schedule, + IsOn=True, + ) + + lighting = LightingComponent( + Name=f"Lighting{sfx}", + PowerDensity=params.LightingPowerDensity, + Schedule=lighting_schedule, + IsOn=True, + DimmingType="Off", + ) + + occupancy = OccupancyComponent( + Name=f"Occupancy{sfx}", + PeopleDensity=params.OccupantDensity, + Schedule=occupancy_schedule, + IsOn=True, + ) + + water_use = WaterUseComponent( + Name=f"WaterUse{sfx}", + FlowRatePerPerson=params.DHWFlowRatePerPerson, + Schedule=occupancy_schedule, + ) + + space_use = ZoneSpaceUseComponent( + Name=f"SpaceUse{sfx}", + Occupancy=occupancy, + Lighting=lighting, + Equipment=equipment, + Thermostat=thermostat, + WaterUse=water_use, + ) + + heating_system = ( + ThermalSystemComponent( + Name=f"HeatingSystem{sfx}", + ConditioningType="Heating", + Fuel=params.HeatingFuel, + SystemCOP=params.HeatingSystemCOP, + DistributionCOP=params.HeatingDistributionCOP, + ) + if params.IdealLoadsHeatingOn + else None + ) + + cooling_system = ( + ThermalSystemComponent( + Name=f"CoolingSystem{sfx}", + ConditioningType="Cooling", + Fuel=params.CoolingFuel, + SystemCOP=params.CoolingSystemCOP, + DistributionCOP=params.CoolingDistributionCOP, + ) + if params.IdealLoadsCoolingOn + else None + ) + + conditioning_system = ConditioningSystemsComponent( + Name=f"ConditioningSystem{sfx}", + Heating=heating_system, + Cooling=cooling_system, + ) + + all_off_day = DayComponent( + Name=f"AllOffDayComponent{sfx}", + Type="Fraction", + Hour_00=0.0, + Hour_01=0.0, + Hour_02=0.0, + Hour_03=0.0, + Hour_04=0.0, + Hour_05=0.0, + Hour_06=0.0, + Hour_07=0.0, + Hour_08=0.0, + Hour_09=0.0, + Hour_10=0.0, + Hour_11=0.0, + Hour_12=0.0, + Hour_13=0.0, + Hour_14=0.0, + Hour_15=0.0, + Hour_16=0.0, + Hour_17=0.0, + Hour_18=0.0, + Hour_19=0.0, + Hour_20=0.0, + Hour_21=0.0, + Hour_22=0.0, + Hour_23=0.0, + ) + + all_off_week = WeekComponent( + Name=f"AllOffWeekComponent{sfx}", + Monday=all_off_day, + Tuesday=all_off_day, + Wednesday=all_off_day, + Thursday=all_off_day, + Friday=all_off_day, + Saturday=all_off_day, + Sunday=all_off_day, + ) + + all_off_year = YearComponent( + Name=f"AllOffYearComponent{sfx}", + Type="Window", + January=all_off_week, + February=all_off_week, + March=all_off_week, + April=all_off_week, + May=all_off_week, + June=all_off_week, + July=all_off_week, + August=all_off_week, + September=all_off_week, + October=all_off_week, + November=all_off_week, + December=all_off_week, + ) + + ventilation_system = VentilationComponent( + Name=f"VentilationSystem{sfx}", + FreshAirPerFloorArea=params.VentFlowRatePerArea, + FreshAirPerPerson=params.VentFlowRatePerPerson, + Provider=params.VentProvider, + # TODO: should hrv sensible/latent efficiency be configurable? (e.g. high/medium/low) + HRV=params.VentHRV, + Economizer=params.VentEconomizer, + DCV=params.VentDCV, + # TODO: this controls natvnent and should + # be configurable for residential models + Schedule=all_off_year, + ) + + hvac = ZoneHVACComponent( + Name=f"HVAC{sfx}", + ConditioningSystems=conditioning_system, + Ventilation=ventilation_system, + ) + + dhw = DHWComponent( + Name=f"DHW{sfx}", + SystemCOP=params.DHWSystemCOP, + DistributionCOP=params.DHWDistributionCOP, + # TODO: should these be configurable? + WaterTemperatureInlet=10, + WaterSupplyTemperature=55, + IsOn=True, + FuelType=params.DHWFuel, + ) + + operations = ZoneOperationsComponent( + Name=f"Operations{sfx}", + SpaceUse=space_use, + HVAC=hvac, + DHW=dhw, + ) + + window_assembly = GlazingConstructionSimpleComponent( + Name=f"WindowAssembly{sfx}", + UValue=params.WindowUValue, + SHGF=params.WindowSHGF, + TVis=params.WindowTVis, + Type="Single", + ) + + infiltration = InfiltrationComponent( + Name=f"Infiltration{sfx}", + IsOn=True, + CalculationMethod="AirChanges/Hour", + AirChangesPerHour=params.InfiltrationACH, + ConstantCoefficient=0.0, + TemperatureCoefficient=0.0, + WindVelocityCoefficient=0.0, + WindVelocitySquaredCoefficient=0.0, + AFNAirMassFlowCoefficientCrack=0.0, + FlowPerExteriorSurfaceArea=0.0, + ) + + # TODO: verify interior/exterior + # TODO: are we okaky with mass assumptions? + facade = ConstructionAssemblyComponent( + Name=f"Facade{sfx}", + Type="Facade", + Layers=[ + ConstructionLayerComponent( + ConstructionMaterial=clay_brick, + Thickness=0.002, + LayerOrder=0, + ), + ConstructionLayerComponent( + ConstructionMaterial=concrete_block_h, + Thickness=0.15, + LayerOrder=1, + ), + ConstructionLayerComponent( + ConstructionMaterial=fiberglass_batts, + Thickness=0.05, + LayerOrder=2, + ), + ConstructionLayerComponent( + ConstructionMaterial=gypsum_board, + Thickness=0.015, + LayerOrder=3, + ), + ], + ) + + facade_r_value_without_fiberglass = facade.r_value - facade.sorted_layers[2].r_value + + facade_r_value_delta = params.FacadeRValue - facade_r_value_without_fiberglass + required_fiberglass_thickness = fiberglass_batts.Conductivity * facade_r_value_delta + + if required_fiberglass_thickness < 0.003: + msg = f"Required Facade Fiberglass thickness is less than 3mm because the desired total facade R-value is {params.FacadeRValue} m²K/W but the concrete and gypsum layers already have a total R-value of {facade_r_value_without_fiberglass} m²K/W." + raise ValueError(msg) + + facade.sorted_layers[2].Thickness = required_fiberglass_thickness + + roof = ConstructionAssemblyComponent( + Name=f"Roof{sfx}", + Type="FlatRoof", + Layers=[ + ConstructionLayerComponent( + ConstructionMaterial=xps_board, + Thickness=0.1, + LayerOrder=0, + ), + ConstructionLayerComponent( + ConstructionMaterial=concrete_mc_light, + Thickness=0.15, + LayerOrder=1, + ), + ConstructionLayerComponent( + ConstructionMaterial=concrete_rc_dense, + Thickness=0.2, + LayerOrder=2, + ), + ConstructionLayerComponent( + ConstructionMaterial=gypsum_board, + Thickness=0.02, + LayerOrder=3, + ), + ], + ) + + roof_r_value_without_xps = roof.r_value - roof.sorted_layers[0].r_value + roof_r_value_delta = params.RoofRValue - roof_r_value_without_xps + required_xps_thickness = xps_board.Conductivity * roof_r_value_delta + if required_xps_thickness < 0.003: + msg = f"Required Roof XPS thickness is less than 3mm because the desired total roof R-value is {params.RoofRValue} m²K/W but the concrete layers already have a total R-value of {roof_r_value_without_xps} m²K/W." + raise ValueError(msg) + + roof.sorted_layers[0].Thickness = required_xps_thickness + + partition = ConstructionAssemblyComponent( + Name=f"Partition{sfx}", + Type="Partition", + Layers=[ + ConstructionLayerComponent( + ConstructionMaterial=gypsum_plaster, + Thickness=0.02, + LayerOrder=0, + ), + ConstructionLayerComponent( + ConstructionMaterial=softwood_general, + Thickness=0.02, + LayerOrder=1, + ), + ConstructionLayerComponent( + ConstructionMaterial=gypsum_plaster, + Thickness=0.02, + LayerOrder=2, + ), + ], + ) + + floor_ceiling = ConstructionAssemblyComponent( + Name=f"FloorCeiling{sfx}", + Type="FloorCeiling", + Layers=[ + ConstructionLayerComponent( + ConstructionMaterial=urethane_carpet, + Thickness=0.02, + LayerOrder=0, + ), + ConstructionLayerComponent( + ConstructionMaterial=cement_mortar, + Thickness=0.02, + LayerOrder=1, + ), + ConstructionLayerComponent( + ConstructionMaterial=concrete_rc_dense, + Thickness=0.15, + LayerOrder=2, + ), + ConstructionLayerComponent( + ConstructionMaterial=gypsum_board, + Thickness=0.02, + LayerOrder=3, + ), + ], + ) + + ground_slab_assembly = ConstructionAssemblyComponent( + Name=f"GroundSlabAssembly{sfx}", + Type="GroundSlab", + Layers=[ + ConstructionLayerComponent( + ConstructionMaterial=xps_board, + Thickness=0.02, + LayerOrder=0, + ), + ConstructionLayerComponent( + ConstructionMaterial=concrete_rc_dense, + Thickness=0.15, + LayerOrder=1, + ), + ConstructionLayerComponent( + ConstructionMaterial=concrete_mc_light, + Thickness=0.04, + LayerOrder=2, + ), + ConstructionLayerComponent( + ConstructionMaterial=cement_mortar, + Thickness=0.03, + LayerOrder=3, + ), + ConstructionLayerComponent( + ConstructionMaterial=ceramic_tile, + Thickness=0.02, + LayerOrder=4, + ), + ], + ) + + ground_slab_r_value_without_xps = ( + ground_slab_assembly.r_value - ground_slab_assembly.sorted_layers[0].r_value + ) + ground_slab_r_value_delta = params.SlabRValue - ground_slab_r_value_without_xps + required_xps_thickness = xps_board.Conductivity * ground_slab_r_value_delta + if required_xps_thickness < 0.003: + msg = f"Required Ground Slab XPS thickness is less than 3mm because the desired total slab R-value is {params.SlabRValue} m²K/W but the concrete layers already have a total R-value of {ground_slab_r_value_without_xps} m²K/W." + raise ValueError(msg) + + ground_slab_assembly.sorted_layers[0].Thickness = required_xps_thickness + + assemblies = EnvelopeAssemblyComponent( + Name=f"EnvelopeAssemblies{sfx}", + FacadeAssembly=facade, + FlatRoofAssembly=roof, + AtticRoofAssembly=roof, + PartitionAssembly=partition, + FloorCeilingAssembly=floor_ceiling, + AtticFloorAssembly=floor_ceiling, + BasementCeilingAssembly=floor_ceiling, + GroundSlabAssembly=ground_slab_assembly, + GroundWallAssembly=ground_slab_assembly, + ExternalFloorAssembly=ground_slab_assembly, + ) + + basement_infiltration = infiltration.model_copy(deep=True) + envelope = ZoneEnvelopeComponent( + Name=f"Envelope{sfx}", + AtticInfiltration=infiltration, + BasementInfiltration=basement_infiltration, + Window=window_assembly, + Infiltration=infiltration, + Assemblies=assemblies, + ) + + zone = ZoneComponent( + Name=f"Zone{sfx}", + Operations=operations, + Envelope=envelope, + ) + + return zone + + +def _eppy_lights_zone_name(lg: Any) -> str: + for attr in ( + "Zone_or_ZoneList_Name", + "Zone_or_ZoneList_or_Space_or_SpaceList_Name", + "Zone_Name", + ): + zn = getattr(lg, attr, None) + if zn not in (None, ""): + return str(zn) + msg = "lights object has no zone reference field" + raise ValueError(msg) + + +def _eppy_lights_watts_per_area(lg: Any) -> float: + v = getattr(lg, "Watts_per_Zone_Floor_Area", None) + if v not in (None, ""): + return float(v) + wpf = getattr(lg, "Watts_per_Floor_Area", None) + if wpf in (None, ""): + msg = "lights object has no watts-per-area field" + raise ValueError(msg) + return float(wpf) + + +class FlatModel(ZoneParams): """A flattened set of parameters for invoking building energy models more conveniently.""" # EquipmentSummerWeekdayNight: float = Field(ge=0, le=1) @@ -843,1251 +2145,60 @@ class FlatModel(BaseModel): # OccupancyRegularWeekendAfternoon: float = Field(ge=0, le=1) # OccupancyRegularWeekendEvening: float = Field(ge=0, le=1) - EquipmentBase: float = Field(ge=0, le=1) - EquipmentAMInterp: float = Field(ge=0, le=1) - EquipmentLunchInterp: float = Field(ge=0, le=1) - EquipmentPMInterp: float = Field(ge=0, le=1) - EquipmentWeekendPeakInterp: float = Field(ge=0, le=1) - EquipmentSummerPeakInterp: float = Field(ge=0, le=1) - - LightingBase: float = Field(ge=0, le=1) - LightingAMInterp: float = Field(ge=0, le=1) - LightingLunchInterp: float = Field(ge=0, le=1) - LightingPMInterp: float = Field(ge=0, le=1) - LightingWeekendPeakInterp: float = Field(ge=0, le=1) - LightingSummerPeakInterp: float = Field(ge=0, le=1) - - OccupancyBase: float = Field(ge=0, le=1) - OccupancyAMInterp: float = Field(ge=0, le=1) - OccupancyLunchInterp: float = Field(ge=0, le=1) - OccupancyPMInterp: float = Field(ge=0, le=1) - OccupancyWeekendPeakInterp: float = Field(ge=0, le=1) - OccupancySummerPeakInterp: float = Field(ge=0, le=1) - - # HSPRegularWeekdayWorkhours: float = Field(ge=0, le=23) - # HSPRegularWeekdayNight: float = Field(ge=0, le=23) - # HSPSummerWeekdayWorkhours: float = Field(ge=0, le=23) - # HSPSummerWeekdayNight: float = Field(ge=0, le=23) - # HSPWeekendWorkhours: float = Field(ge=0, le=23) - # HSPWeekendNight: float = Field(ge=0, le=23) - - # CSPRegularWeekdayWorkhours: float = Field(ge=20, le=30) - # CSPRegularWeekdayNight: float = Field(ge=20, le=30) - # CSPSummerWeekdayWorkhours: float = Field(ge=20, le=30) - # CSPSummerWeekdayNight: float = Field(ge=20, le=30) - # CSPWeekendWorkhours: float = Field(ge=20, le=30) - # CSPWeekendNight: float = Field(ge=20, le=30) - - HeatingSetpointBase: float = Field(ge=0, le=23) - SetpointDeadband: float = Field(ge=0, le=10) - HeatingSetpointSetback: float = Field(ge=0, le=10) - CoolingSetpointSetback: float = Field(ge=0, le=10) - NightSetback: float = Field(ge=0, le=1) - WeekendSetback: float = Field(ge=0, le=1) - SummerSetback: float = Field(ge=0, le=1) - - HeatingFuel: FuelType - CoolingFuel: FuelType - HeatingSystemCOP: float - CoolingSystemCOP: float - HeatingDistributionCOP: float - CoolingDistributionCOP: float - - EquipmentPowerDensity: float = Field(ge=0, le=200) - LightingPowerDensity: float = Field(ge=0, le=100) - OccupantDensity: float = Field(ge=0, le=50) - - VentFlowRatePerPerson: float - VentFlowRatePerArea: float - VentProvider: VentilationProvider - VentHRV: HRVMethod - VentEconomizer: EconomizerMethod - VentDCV: DCVMethod - - DHWFlowRatePerPerson: float - DHWFuel: DHWFuelType - DHWSystemCOP: float - DHWDistributionCOP: float - - InfiltrationACH: float - - WindowUValue: float - WindowSHGF: float - WindowTVis: float - - FacadeRValue: float - RoofRValue: float - SlabRValue: float - - WWR: float - F2FHeight: float - NFloors: int - Width: float - Depth: float - Rotation: float - EPWURI: WeatherUrl | Path + zoning: ZoningType = Field(default="core/perim") + zone_overrides: dict[int, dict[str, dict[str, Any]]] = Field(default_factory=dict) - def to_zone(self) -> ZoneComponent: - """Convert the flat model to a full zone.""" - # occ_regular_workday = DayComponent( - # Name="Occupancy_Regular_Workday", - # Type="Fraction", - # Hour_00=self.OccupancyRegularWeekdayNight, - # Hour_01=self.OccupancyRegularWeekdayNight, - # Hour_02=self.OccupancyRegularWeekdayNight, - # Hour_03=self.OccupancyRegularWeekdayNight, - # Hour_04=self.OccupancyRegularWeekdayNight, - # Hour_05=self.OccupancyRegularWeekdayNight, - # Hour_06=self.OccupancyRegularWeekdayEarlyMorning, - # Hour_07=self.OccupancyRegularWeekdayEarlyMorning, - # Hour_08=self.OccupancyRegularWeekdayEarlyMorning, - # Hour_09=self.OccupancyRegularWeekdayMorning, - # Hour_10=self.OccupancyRegularWeekdayMorning, - # Hour_11=self.OccupancyRegularWeekdayMorning, - # Hour_12=self.OccupancyRegularWeekdayLunch, - # Hour_13=self.OccupancyRegularWeekdayLunch, - # Hour_14=self.OccupancyRegularWeekdayAfternoon, - # Hour_15=self.OccupancyRegularWeekdayAfternoon, - # Hour_16=self.OccupancyRegularWeekdayAfternoon, - # Hour_17=self.OccupancyRegularWeekdayAfternoon, - # Hour_18=self.OccupancyRegularWeekdayEvening, - # Hour_19=self.OccupancyRegularWeekdayEvening, - # Hour_20=self.OccupancyRegularWeekdayEvening, - # Hour_21=self.OccupancyRegularWeekdayNight, - # Hour_22=self.OccupancyRegularWeekdayNight, - # Hour_23=self.OccupancyRegularWeekdayNight, - # ) - - # occ_regular_weekend = DayComponent( - # Name="Occupancy_Regular_Weekend", - # Type="Fraction", - # Hour_00=self.OccupancyRegularWeekendNight, - # Hour_01=self.OccupancyRegularWeekendNight, - # Hour_02=self.OccupancyRegularWeekendNight, - # Hour_03=self.OccupancyRegularWeekendNight, - # Hour_04=self.OccupancyRegularWeekendNight, - # Hour_05=self.OccupancyRegularWeekendNight, - # Hour_06=self.OccupancyRegularWeekendEarlyMorning, - # Hour_07=self.OccupancyRegularWeekendEarlyMorning, - # Hour_08=self.OccupancyRegularWeekendEarlyMorning, - # Hour_09=self.OccupancyRegularWeekendMorning, - # Hour_10=self.OccupancyRegularWeekendMorning, - # Hour_11=self.OccupancyRegularWeekendMorning, - # Hour_12=self.OccupancyRegularWeekendLunch, - # Hour_13=self.OccupancyRegularWeekendLunch, - # Hour_14=self.OccupancyRegularWeekendAfternoon, - # Hour_15=self.OccupancyRegularWeekendAfternoon, - # Hour_16=self.OccupancyRegularWeekendAfternoon, - # Hour_17=self.OccupancyRegularWeekendAfternoon, - # Hour_18=self.OccupancyRegularWeekendEvening, - # Hour_19=self.OccupancyRegularWeekendEvening, - # Hour_20=self.OccupancyRegularWeekendEvening, - # Hour_21=self.OccupancyRegularWeekendNight, - # Hour_22=self.OccupancyRegularWeekendNight, - # Hour_23=self.OccupancyRegularWeekendNight, - # ) - - # occ_summer_workday = DayComponent( - # Name="Occupancy_Summer_Workday", - # Type="Fraction", - # Hour_00=self.OccupancySummerWeekdayNight, - # Hour_01=self.OccupancySummerWeekdayNight, - # Hour_02=self.OccupancySummerWeekdayNight, - # Hour_03=self.OccupancySummerWeekdayNight, - # Hour_04=self.OccupancySummerWeekdayNight, - # Hour_05=self.OccupancySummerWeekdayNight, - # Hour_06=self.OccupancySummerWeekdayEarlyMorning, - # Hour_07=self.OccupancySummerWeekdayEarlyMorning, - # Hour_08=self.OccupancySummerWeekdayEarlyMorning, - # Hour_09=self.OccupancySummerWeekdayMorning, - # Hour_10=self.OccupancySummerWeekdayMorning, - # Hour_11=self.OccupancySummerWeekdayMorning, - # Hour_12=self.OccupancySummerWeekdayLunch, - # Hour_13=self.OccupancySummerWeekdayLunch, - # Hour_14=self.OccupancySummerWeekdayAfternoon, - # Hour_15=self.OccupancySummerWeekdayAfternoon, - # Hour_16=self.OccupancySummerWeekdayAfternoon, - # Hour_17=self.OccupancySummerWeekdayAfternoon, - # Hour_18=self.OccupancySummerWeekdayEvening, - # Hour_19=self.OccupancySummerWeekdayEvening, - # Hour_20=self.OccupancySummerWeekdayEvening, - # Hour_21=self.OccupancySummerWeekdayNight, - # Hour_22=self.OccupancySummerWeekdayNight, - # Hour_23=self.OccupancySummerWeekdayNight, - # ) - - # occ_summer_weekend = DayComponent( - # Name="Occupancy_Summer_Weekend", - # Type="Fraction", - # Hour_00=self.OccupancySummerWeekendNight, - # Hour_01=self.OccupancySummerWeekendNight, - # Hour_02=self.OccupancySummerWeekendNight, - # Hour_03=self.OccupancySummerWeekendNight, - # Hour_04=self.OccupancySummerWeekendNight, - # Hour_05=self.OccupancySummerWeekendNight, - # Hour_06=self.OccupancySummerWeekendEarlyMorning, - # Hour_07=self.OccupancySummerWeekendEarlyMorning, - # Hour_08=self.OccupancySummerWeekendEarlyMorning, - # Hour_09=self.OccupancySummerWeekendMorning, - # Hour_10=self.OccupancySummerWeekendMorning, - # Hour_11=self.OccupancySummerWeekendMorning, - # Hour_12=self.OccupancySummerWeekendLunch, - # Hour_13=self.OccupancySummerWeekendLunch, - # Hour_14=self.OccupancySummerWeekendAfternoon, - # Hour_15=self.OccupancySummerWeekendAfternoon, - # Hour_16=self.OccupancySummerWeekendAfternoon, - # Hour_17=self.OccupancySummerWeekendAfternoon, - # Hour_18=self.OccupancySummerWeekendEvening, - # Hour_19=self.OccupancySummerWeekendEvening, - # Hour_20=self.OccupancySummerWeekendEvening, - # Hour_21=self.OccupancySummerWeekendNight, - # Hour_22=self.OccupancySummerWeekendNight, - # Hour_23=self.OccupancySummerWeekendNight, - # ) - - # occ_regular_week = WeekComponent( - # Name="Occupancy_Regular_Week", - # Monday=occ_regular_workday, - # Tuesday=occ_regular_workday, - # Wednesday=occ_regular_workday, - # Thursday=occ_regular_workday, - # Friday=occ_regular_workday, - # Saturday=occ_regular_weekend, - # Sunday=occ_regular_weekend, - # ) - - # occ_summer_week = WeekComponent( - # Name="Occupancy_Summer_Week", - # Monday=occ_summer_workday, - # Tuesday=occ_summer_workday, - # Wednesday=occ_summer_workday, - # Thursday=occ_summer_workday, - # Friday=occ_summer_workday, - # Saturday=occ_summer_weekend, - # Sunday=occ_summer_weekend, - # ) - - # occ_year = YearComponent( - # Name="Occupancy_Schedule", - # Type="Occupancy", - # January=occ_regular_week, - # February=occ_regular_week, - # March=occ_regular_week, - # April=occ_regular_week, - # May=occ_regular_week, - # June=occ_summer_week, - # July=occ_summer_week, - # August=occ_summer_week, - # September=occ_regular_week, - # October=occ_regular_week, - # November=occ_regular_week, - # December=occ_regular_week, - # ) - - # lighting_regular_workday = DayComponent( - # Name="Lighting_Regular_Workday", - # Type="Fraction", - # Hour_00=self.LightingRegularWeekdayNight, - # Hour_01=self.LightingRegularWeekdayNight, - # Hour_02=self.LightingRegularWeekdayNight, - # Hour_03=self.LightingRegularWeekdayNight, - # Hour_04=self.LightingRegularWeekdayNight, - # Hour_05=self.LightingRegularWeekdayNight, - # Hour_06=self.LightingRegularWeekdayEarlyMorning, - # Hour_07=self.LightingRegularWeekdayEarlyMorning, - # Hour_08=self.LightingRegularWeekdayEarlyMorning, - # Hour_09=self.LightingRegularWeekdayMorning, - # Hour_10=self.LightingRegularWeekdayMorning, - # Hour_11=self.LightingRegularWeekdayMorning, - # Hour_12=self.LightingRegularWeekdayLunch, - # Hour_13=self.LightingRegularWeekdayLunch, - # Hour_14=self.LightingRegularWeekdayAfternoon, - # Hour_15=self.LightingRegularWeekdayAfternoon, - # Hour_16=self.LightingRegularWeekdayAfternoon, - # Hour_17=self.LightingRegularWeekdayAfternoon, - # Hour_18=self.LightingRegularWeekdayEvening, - # Hour_19=self.LightingRegularWeekdayEvening, - # Hour_20=self.LightingRegularWeekdayEvening, - # Hour_21=self.LightingRegularWeekdayNight, - # Hour_22=self.LightingRegularWeekdayNight, - # Hour_23=self.LightingRegularWeekdayNight, - # ) - - # lighting_regular_weekend = DayComponent( - # Name="Lighting_Regular_Weekend", - # Type="Fraction", - # Hour_00=self.LightingRegularWeekendNight, - # Hour_01=self.LightingRegularWeekendNight, - # Hour_02=self.LightingRegularWeekendNight, - # Hour_03=self.LightingRegularWeekendNight, - # Hour_04=self.LightingRegularWeekendNight, - # Hour_05=self.LightingRegularWeekendNight, - # Hour_06=self.LightingRegularWeekendEarlyMorning, - # Hour_07=self.LightingRegularWeekendEarlyMorning, - # Hour_08=self.LightingRegularWeekendEarlyMorning, - # Hour_09=self.LightingRegularWeekendMorning, - # Hour_10=self.LightingRegularWeekendMorning, - # Hour_11=self.LightingRegularWeekendMorning, - # Hour_12=self.LightingRegularWeekendLunch, - # Hour_13=self.LightingRegularWeekendLunch, - # Hour_14=self.LightingRegularWeekendAfternoon, - # Hour_15=self.LightingRegularWeekendAfternoon, - # Hour_16=self.LightingRegularWeekendAfternoon, - # Hour_17=self.LightingRegularWeekendAfternoon, - # Hour_18=self.LightingRegularWeekendEvening, - # Hour_19=self.LightingRegularWeekendEvening, - # Hour_20=self.LightingRegularWeekendEvening, - # Hour_21=self.LightingRegularWeekendNight, - # Hour_22=self.LightingRegularWeekendNight, - # Hour_23=self.LightingRegularWeekendNight, - # ) - - # lighting_summer_workday = DayComponent( - # Name="Lighting_Summer_Workday", - # Type="Fraction", - # Hour_00=self.LightingSummerWeekdayNight, - # Hour_01=self.LightingSummerWeekdayNight, - # Hour_02=self.LightingSummerWeekdayNight, - # Hour_03=self.LightingSummerWeekdayNight, - # Hour_04=self.LightingSummerWeekdayNight, - # Hour_05=self.LightingSummerWeekdayNight, - # Hour_06=self.LightingSummerWeekdayEarlyMorning, - # Hour_07=self.LightingSummerWeekdayEarlyMorning, - # Hour_08=self.LightingSummerWeekdayEarlyMorning, - # Hour_09=self.LightingSummerWeekdayMorning, - # Hour_10=self.LightingSummerWeekdayMorning, - # Hour_11=self.LightingSummerWeekdayMorning, - # Hour_12=self.LightingSummerWeekdayLunch, - # Hour_13=self.LightingSummerWeekdayLunch, - # Hour_14=self.LightingSummerWeekdayAfternoon, - # Hour_15=self.LightingSummerWeekdayAfternoon, - # Hour_16=self.LightingSummerWeekdayAfternoon, - # Hour_17=self.LightingSummerWeekdayAfternoon, - # Hour_18=self.LightingSummerWeekdayEvening, - # Hour_19=self.LightingSummerWeekdayEvening, - # Hour_20=self.LightingSummerWeekdayEvening, - # Hour_21=self.LightingSummerWeekdayNight, - # Hour_22=self.LightingSummerWeekdayNight, - # Hour_23=self.LightingSummerWeekdayNight, - # ) - - # lighting_summer_weekend = DayComponent( - # Name="Lighting_Summer_Weekend", - # Type="Fraction", - # Hour_00=self.LightingSummerWeekendNight, - # Hour_01=self.LightingSummerWeekendNight, - # Hour_02=self.LightingSummerWeekendNight, - # Hour_03=self.LightingSummerWeekendNight, - # Hour_04=self.LightingSummerWeekendNight, - # Hour_05=self.LightingSummerWeekendNight, - # Hour_06=self.LightingSummerWeekendEarlyMorning, - # Hour_07=self.LightingSummerWeekendEarlyMorning, - # Hour_08=self.LightingSummerWeekendEarlyMorning, - # Hour_09=self.LightingSummerWeekendMorning, - # Hour_10=self.LightingSummerWeekendMorning, - # Hour_11=self.LightingSummerWeekendMorning, - # Hour_12=self.LightingSummerWeekendLunch, - # Hour_13=self.LightingSummerWeekendLunch, - # Hour_14=self.LightingSummerWeekendAfternoon, - # Hour_15=self.LightingSummerWeekendAfternoon, - # Hour_16=self.LightingSummerWeekendAfternoon, - # Hour_17=self.LightingSummerWeekendAfternoon, - # Hour_18=self.LightingSummerWeekendEvening, - # Hour_19=self.LightingSummerWeekendEvening, - # Hour_20=self.LightingSummerWeekendEvening, - # Hour_21=self.LightingSummerWeekendNight, - # Hour_22=self.LightingSummerWeekendNight, - # Hour_23=self.LightingSummerWeekendNight, - # ) - - # lighting_regular_week = WeekComponent( - # Name="Lighting_Regular_Week", - # Monday=lighting_regular_workday, - # Tuesday=lighting_regular_workday, - # Wednesday=lighting_regular_workday, - # Thursday=lighting_regular_workday, - # Friday=lighting_regular_workday, - # Saturday=lighting_regular_weekend, - # Sunday=lighting_regular_weekend, - # ) - - # lighting_summer_week = WeekComponent( - # Name="Lighting_Summer_Week", - # Monday=lighting_summer_workday, - # Tuesday=lighting_summer_workday, - # Wednesday=lighting_summer_workday, - # Thursday=lighting_summer_workday, - # Friday=lighting_summer_workday, - # Saturday=lighting_summer_weekend, - # Sunday=lighting_summer_weekend, - # ) - - # lighting_year = YearComponent( - # Name="Lighting_Schedule", - # Type="Lighting", - # January=lighting_regular_week, - # February=lighting_regular_week, - # March=lighting_regular_week, - # April=lighting_regular_week, - # May=lighting_regular_week, - # June=lighting_summer_week, - # July=lighting_summer_week, - # August=lighting_summer_week, - # September=lighting_regular_week, - # October=lighting_regular_week, - # November=lighting_regular_week, - # December=lighting_regular_week, - # ) - - # equipment_regular_workday = DayComponent( - # Name="Equipment_Regular_Workday", - # Type="Fraction", - # Hour_00=self.EquipmentRegularWeekdayNight, - # Hour_01=self.EquipmentRegularWeekdayNight, - # Hour_02=self.EquipmentRegularWeekdayNight, - # Hour_03=self.EquipmentRegularWeekdayNight, - # Hour_04=self.EquipmentRegularWeekdayNight, - # Hour_05=self.EquipmentRegularWeekdayNight, - # Hour_06=self.EquipmentRegularWeekdayEarlyMorning, - # Hour_07=self.EquipmentRegularWeekdayEarlyMorning, - # Hour_08=self.EquipmentRegularWeekdayEarlyMorning, - # Hour_09=self.EquipmentRegularWeekdayMorning, - # Hour_10=self.EquipmentRegularWeekdayMorning, - # Hour_11=self.EquipmentRegularWeekdayMorning, - # Hour_12=self.EquipmentRegularWeekdayLunch, - # Hour_13=self.EquipmentRegularWeekdayLunch, - # Hour_14=self.EquipmentRegularWeekdayAfternoon, - # Hour_15=self.EquipmentRegularWeekdayAfternoon, - # Hour_16=self.EquipmentRegularWeekdayAfternoon, - # Hour_17=self.EquipmentRegularWeekdayAfternoon, - # Hour_18=self.EquipmentRegularWeekdayEvening, - # Hour_19=self.EquipmentRegularWeekdayEvening, - # Hour_20=self.EquipmentRegularWeekdayEvening, - # Hour_21=self.EquipmentRegularWeekdayNight, - # Hour_22=self.EquipmentRegularWeekdayNight, - # Hour_23=self.EquipmentRegularWeekdayNight, - # ) - - # equipment_regular_weekend = DayComponent( - # Name="Equipment_Regular_Weekend", - # Type="Fraction", - # Hour_00=self.EquipmentRegularWeekendNight, - # Hour_01=self.EquipmentRegularWeekendNight, - # Hour_02=self.EquipmentRegularWeekendNight, - # Hour_03=self.EquipmentRegularWeekendNight, - # Hour_04=self.EquipmentRegularWeekendNight, - # Hour_05=self.EquipmentRegularWeekendNight, - # Hour_06=self.EquipmentRegularWeekendEarlyMorning, - # Hour_07=self.EquipmentRegularWeekendEarlyMorning, - # Hour_08=self.EquipmentRegularWeekendEarlyMorning, - # Hour_09=self.EquipmentRegularWeekendMorning, - # Hour_10=self.EquipmentRegularWeekendMorning, - # Hour_11=self.EquipmentRegularWeekendMorning, - # Hour_12=self.EquipmentRegularWeekendLunch, - # Hour_13=self.EquipmentRegularWeekendLunch, - # Hour_14=self.EquipmentRegularWeekendAfternoon, - # Hour_15=self.EquipmentRegularWeekendAfternoon, - # Hour_16=self.EquipmentRegularWeekendAfternoon, - # Hour_17=self.EquipmentRegularWeekendAfternoon, - # Hour_18=self.EquipmentRegularWeekendEvening, - # Hour_19=self.EquipmentRegularWeekendEvening, - # Hour_20=self.EquipmentRegularWeekendEvening, - # Hour_21=self.EquipmentRegularWeekendNight, - # Hour_22=self.EquipmentRegularWeekendNight, - # Hour_23=self.EquipmentRegularWeekendNight, - # ) - - # equipment_summer_workday = DayComponent( - # Name="Equipment_Summer_Workday", - # Type="Fraction", - # Hour_00=self.EquipmentSummerWeekdayNight, - # Hour_01=self.EquipmentSummerWeekdayNight, - # Hour_02=self.EquipmentSummerWeekdayNight, - # Hour_03=self.EquipmentSummerWeekdayNight, - # Hour_04=self.EquipmentSummerWeekdayNight, - # Hour_05=self.EquipmentSummerWeekdayNight, - # Hour_06=self.EquipmentSummerWeekdayEarlyMorning, - # Hour_07=self.EquipmentSummerWeekdayEarlyMorning, - # Hour_08=self.EquipmentSummerWeekdayEarlyMorning, - # Hour_09=self.EquipmentSummerWeekdayMorning, - # Hour_10=self.EquipmentSummerWeekdayMorning, - # Hour_11=self.EquipmentSummerWeekdayMorning, - # Hour_12=self.EquipmentSummerWeekdayLunch, - # Hour_13=self.EquipmentSummerWeekdayLunch, - # Hour_14=self.EquipmentSummerWeekdayAfternoon, - # Hour_15=self.EquipmentSummerWeekdayAfternoon, - # Hour_16=self.EquipmentSummerWeekdayAfternoon, - # Hour_17=self.EquipmentSummerWeekdayAfternoon, - # Hour_18=self.EquipmentSummerWeekdayEvening, - # Hour_19=self.EquipmentSummerWeekdayEvening, - # Hour_20=self.EquipmentSummerWeekdayEvening, - # Hour_21=self.EquipmentSummerWeekdayNight, - # Hour_22=self.EquipmentSummerWeekdayNight, - # Hour_23=self.EquipmentSummerWeekdayNight, - # ) - - # equipment_summer_weekend = DayComponent( - # Name="Equipment_Summer_Weekend", - # Type="Fraction", - # Hour_00=self.EquipmentSummerWeekendNight, - # Hour_01=self.EquipmentSummerWeekendNight, - # Hour_02=self.EquipmentSummerWeekendNight, - # Hour_03=self.EquipmentSummerWeekendNight, - # Hour_04=self.EquipmentSummerWeekendNight, - # Hour_05=self.EquipmentSummerWeekendNight, - # Hour_06=self.EquipmentSummerWeekendEarlyMorning, - # Hour_07=self.EquipmentSummerWeekendEarlyMorning, - # Hour_08=self.EquipmentSummerWeekendEarlyMorning, - # Hour_09=self.EquipmentSummerWeekendMorning, - # Hour_10=self.EquipmentSummerWeekendMorning, - # Hour_11=self.EquipmentSummerWeekendMorning, - # Hour_12=self.EquipmentSummerWeekendLunch, - # Hour_13=self.EquipmentSummerWeekendLunch, - # Hour_14=self.EquipmentSummerWeekendAfternoon, - # Hour_15=self.EquipmentSummerWeekendAfternoon, - # Hour_16=self.EquipmentSummerWeekendAfternoon, - # Hour_17=self.EquipmentSummerWeekendAfternoon, - # Hour_18=self.EquipmentSummerWeekendEvening, - # Hour_19=self.EquipmentSummerWeekendEvening, - # Hour_20=self.EquipmentSummerWeekendEvening, - # Hour_21=self.EquipmentSummerWeekendNight, - # Hour_22=self.EquipmentSummerWeekendNight, - # Hour_23=self.EquipmentSummerWeekendNight, - # ) - - # equipment_regular_week = WeekComponent( - # Name="Equipment_Regular_Week", - # Monday=equipment_regular_workday, - # Tuesday=equipment_regular_workday, - # Wednesday=equipment_regular_workday, - # Thursday=equipment_regular_workday, - # Friday=equipment_regular_workday, - # Saturday=equipment_regular_weekend, - # Sunday=equipment_regular_weekend, - # ) - - # equipment_summer_week = WeekComponent( - # Name="Equipment_Summer_Week", - # Monday=equipment_summer_workday, - # Tuesday=equipment_summer_workday, - # Wednesday=equipment_summer_workday, - # Thursday=equipment_summer_workday, - # Friday=equipment_summer_workday, - # Saturday=equipment_summer_weekend, - # Sunday=equipment_summer_weekend, - # ) - - # equipment_year = YearComponent( - # Name="equipment_Schedule", - # Type="Equipment", - # January=equipment_regular_week, - # February=equipment_regular_week, - # March=equipment_regular_week, - # April=equipment_regular_week, - # May=equipment_regular_week, - # June=equipment_summer_week, - # July=equipment_summer_week, - # August=equipment_summer_week, - # September=equipment_regular_week, - # October=equipment_regular_week, - # November=lighting_regular_week, - # December=equipment_regular_week, - # ) - - equipment_paramteric = ParametericYear( - Base=self.EquipmentBase, - AMInterp=self.EquipmentAMInterp, - LunchInterp=self.EquipmentLunchInterp, - PMInterp=self.EquipmentPMInterp, - WeekendPeakInterp=self.EquipmentWeekendPeakInterp, - SummerPeakInterp=self.EquipmentSummerPeakInterp, - ) - - lighting_paramteric = ParametericYear( - Base=self.LightingBase, - AMInterp=self.LightingAMInterp, - LunchInterp=self.LightingLunchInterp, - PMInterp=self.LightingPMInterp, - WeekendPeakInterp=self.LightingWeekendPeakInterp, - SummerPeakInterp=self.LightingSummerPeakInterp, - ) - - occupancy_paramteric = ParametericYear( - Base=self.OccupancyBase, - AMInterp=self.OccupancyAMInterp, - LunchInterp=self.OccupancyLunchInterp, - PMInterp=self.OccupancyPMInterp, - WeekendPeakInterp=self.OccupancyWeekendPeakInterp, - SummerPeakInterp=self.OccupancySummerPeakInterp, - ) - - equipment_schedule = equipment_paramteric.to_schedule( - name="Equipment", category="Equipment" - ) - lighting_schedule = lighting_paramteric.to_schedule( - name="Lighting", category="Lighting" - ) - occupancy_schedule = occupancy_paramteric.to_schedule( - name="Occupancy", category="Occupancy" - ) - - # hsp_regular_workday = DayComponent( - # Name="HeatingSetpoint_Regular_Workday", - # Type="Temperature", - # Hour_00=self.HSPRegularWeekdayNight, - # Hour_01=self.HSPRegularWeekdayNight, - # Hour_02=self.HSPRegularWeekdayNight, - # Hour_03=self.HSPRegularWeekdayNight, - # Hour_04=self.HSPRegularWeekdayNight, - # Hour_05=self.HSPRegularWeekdayNight, - # Hour_06=self.HSPRegularWeekdayWorkhours, - # Hour_07=self.HSPRegularWeekdayWorkhours, - # Hour_08=self.HSPRegularWeekdayWorkhours, - # Hour_09=self.HSPRegularWeekdayWorkhours, - # Hour_10=self.HSPRegularWeekdayWorkhours, - # Hour_11=self.HSPRegularWeekdayWorkhours, - # Hour_12=self.HSPRegularWeekdayWorkhours, - # Hour_13=self.HSPRegularWeekdayWorkhours, - # Hour_14=self.HSPRegularWeekdayWorkhours, - # Hour_15=self.HSPRegularWeekdayWorkhours, - # Hour_16=self.HSPRegularWeekdayWorkhours, - # Hour_17=self.HSPRegularWeekdayWorkhours, - # Hour_18=self.HSPRegularWeekdayWorkhours, - # Hour_19=self.HSPRegularWeekdayNight, - # Hour_20=self.HSPRegularWeekdayNight, - # Hour_21=self.HSPRegularWeekdayNight, - # Hour_22=self.HSPRegularWeekdayNight, - # Hour_23=self.HSPRegularWeekdayNight, - # ) - - # hsp_regular_weekend = DayComponent( - # Name="HeatingSetpoint_Regular_Weekend", - # Type="Temperature", - # Hour_00=self.HSPWeekendNight, - # Hour_01=self.HSPWeekendNight, - # Hour_02=self.HSPWeekendNight, - # Hour_03=self.HSPWeekendNight, - # Hour_04=self.HSPWeekendNight, - # Hour_05=self.HSPWeekendNight, - # Hour_06=self.HSPWeekendWorkhours, - # Hour_07=self.HSPWeekendWorkhours, - # Hour_08=self.HSPWeekendWorkhours, - # Hour_09=self.HSPWeekendWorkhours, - # Hour_10=self.HSPWeekendWorkhours, - # Hour_11=self.HSPWeekendWorkhours, - # Hour_12=self.HSPWeekendWorkhours, - # Hour_13=self.HSPWeekendWorkhours, - # Hour_14=self.HSPWeekendWorkhours, - # Hour_15=self.HSPWeekendWorkhours, - # Hour_16=self.HSPWeekendWorkhours, - # Hour_17=self.HSPWeekendWorkhours, - # Hour_18=self.HSPWeekendWorkhours, - # Hour_19=self.HSPWeekendNight, - # Hour_20=self.HSPWeekendNight, - # Hour_21=self.HSPWeekendNight, - # Hour_22=self.HSPWeekendNight, - # Hour_23=self.HSPWeekendNight, - # ) - - # hsp_summer_workday = DayComponent( - # Name="HeatingSetpoint_Summer_Workday", - # Type="Temperature", - # Hour_00=self.HSPSummerWeekdayNight, - # Hour_01=self.HSPSummerWeekdayNight, - # Hour_02=self.HSPSummerWeekdayNight, - # Hour_03=self.HSPSummerWeekdayNight, - # Hour_04=self.HSPSummerWeekdayNight, - # Hour_05=self.HSPSummerWeekdayNight, - # Hour_06=self.HSPSummerWeekdayWorkhours, - # Hour_07=self.HSPSummerWeekdayWorkhours, - # Hour_08=self.HSPSummerWeekdayWorkhours, - # Hour_09=self.HSPSummerWeekdayWorkhours, - # Hour_10=self.HSPSummerWeekdayWorkhours, - # Hour_11=self.HSPSummerWeekdayWorkhours, - # Hour_12=self.HSPSummerWeekdayWorkhours, - # Hour_13=self.HSPSummerWeekdayWorkhours, - # Hour_14=self.HSPSummerWeekdayWorkhours, - # Hour_15=self.HSPSummerWeekdayWorkhours, - # Hour_16=self.HSPSummerWeekdayWorkhours, - # Hour_17=self.HSPSummerWeekdayWorkhours, - # Hour_18=self.HSPSummerWeekdayWorkhours, - # Hour_19=self.HSPSummerWeekdayNight, - # Hour_20=self.HSPSummerWeekdayNight, - # Hour_21=self.HSPSummerWeekdayNight, - # Hour_22=self.HSPSummerWeekdayNight, - # Hour_23=self.HSPSummerWeekdayNight, - # ) - - # hsp_regular_week = WeekComponent( - # Name="HeatingSetpoint_Regular_Week", - # Monday=hsp_regular_workday, - # Tuesday=hsp_regular_workday, - # Wednesday=hsp_regular_workday, - # Thursday=hsp_regular_workday, - # Friday=hsp_regular_workday, - # Saturday=hsp_regular_weekend, - # Sunday=hsp_regular_weekend, - # ) - - # hsp_summer_week = WeekComponent( - # Name="HeatingSetpoint_Summer_Week", - # Monday=hsp_summer_workday, - # Tuesday=hsp_summer_workday, - # Wednesday=hsp_summer_workday, - # Thursday=hsp_summer_workday, - # Friday=hsp_summer_workday, - # Saturday=hsp_regular_weekend, - # Sunday=hsp_regular_weekend, - # ) - - # hsp_year = YearComponent( - # Name="HeatingSetpoint_Schedule", - # Type="Setpoint", - # January=hsp_regular_week, - # February=hsp_regular_week, - # March=hsp_regular_week, - # April=hsp_regular_week, - # May=hsp_regular_week, - # June=hsp_summer_week, - # July=hsp_summer_week, - # August=hsp_summer_week, - # September=hsp_regular_week, - # October=hsp_regular_week, - # November=hsp_regular_week, - # December=hsp_regular_week, - # ) - - # csp_regular_workday = DayComponent( - # Name="CoolingSetpoint_Regular_Workday", - # Type="Temperature", - # Hour_00=self.CSPRegularWeekdayNight, - # Hour_01=self.CSPRegularWeekdayNight, - # Hour_02=self.CSPRegularWeekdayNight, - # Hour_03=self.CSPRegularWeekdayNight, - # Hour_04=self.CSPRegularWeekdayNight, - # Hour_05=self.CSPRegularWeekdayNight, - # Hour_06=self.CSPRegularWeekdayWorkhours, - # Hour_07=self.CSPRegularWeekdayWorkhours, - # Hour_08=self.CSPRegularWeekdayWorkhours, - # Hour_09=self.CSPRegularWeekdayWorkhours, - # Hour_10=self.CSPRegularWeekdayWorkhours, - # Hour_11=self.CSPRegularWeekdayWorkhours, - # Hour_12=self.CSPRegularWeekdayWorkhours, - # Hour_13=self.CSPRegularWeekdayWorkhours, - # Hour_14=self.CSPRegularWeekdayWorkhours, - # Hour_15=self.CSPRegularWeekdayWorkhours, - # Hour_16=self.CSPRegularWeekdayWorkhours, - # Hour_17=self.CSPRegularWeekdayWorkhours, - # Hour_18=self.CSPRegularWeekdayWorkhours, - # Hour_19=self.CSPRegularWeekdayNight, - # Hour_20=self.CSPRegularWeekdayNight, - # Hour_21=self.CSPRegularWeekdayNight, - # Hour_22=self.CSPRegularWeekdayNight, - # Hour_23=self.CSPRegularWeekdayNight, - # ) - - # csp_regular_weekend = DayComponent( - # Name="CoolingSetpoint_Regular_Weekend", - # Type="Temperature", - # Hour_00=self.CSPWeekendNight, - # Hour_01=self.CSPWeekendNight, - # Hour_02=self.CSPWeekendNight, - # Hour_03=self.CSPWeekendNight, - # Hour_04=self.CSPWeekendNight, - # Hour_05=self.CSPWeekendNight, - # Hour_06=self.CSPWeekendWorkhours, - # Hour_07=self.CSPWeekendWorkhours, - # Hour_08=self.CSPWeekendWorkhours, - # Hour_09=self.CSPWeekendWorkhours, - # Hour_10=self.CSPWeekendWorkhours, - # Hour_11=self.CSPWeekendWorkhours, - # Hour_12=self.CSPWeekendWorkhours, - # Hour_13=self.CSPWeekendWorkhours, - # Hour_14=self.CSPWeekendWorkhours, - # Hour_15=self.CSPWeekendWorkhours, - # Hour_16=self.CSPWeekendWorkhours, - # Hour_17=self.CSPWeekendWorkhours, - # Hour_18=self.CSPWeekendWorkhours, - # Hour_19=self.CSPWeekendNight, - # Hour_20=self.CSPWeekendNight, - # Hour_21=self.CSPWeekendNight, - # Hour_22=self.CSPWeekendNight, - # Hour_23=self.CSPWeekendNight, - # ) - - # csp_summer_workday = DayComponent( - # Name="CoolingSetpoint_Summer_Workday", - # Type="Temperature", - # Hour_00=self.CSPSummerWeekdayNight, - # Hour_01=self.CSPSummerWeekdayNight, - # Hour_02=self.CSPSummerWeekdayNight, - # Hour_03=self.CSPSummerWeekdayNight, - # Hour_04=self.CSPSummerWeekdayNight, - # Hour_05=self.CSPSummerWeekdayNight, - # Hour_06=self.CSPSummerWeekdayWorkhours, - # Hour_07=self.CSPSummerWeekdayWorkhours, - # Hour_08=self.CSPSummerWeekdayWorkhours, - # Hour_09=self.CSPSummerWeekdayWorkhours, - # Hour_10=self.CSPSummerWeekdayWorkhours, - # Hour_11=self.CSPSummerWeekdayWorkhours, - # Hour_12=self.CSPSummerWeekdayWorkhours, - # Hour_13=self.CSPSummerWeekdayWorkhours, - # Hour_14=self.CSPSummerWeekdayWorkhours, - # Hour_15=self.CSPSummerWeekdayWorkhours, - # Hour_16=self.CSPSummerWeekdayWorkhours, - # Hour_17=self.CSPSummerWeekdayWorkhours, - # Hour_18=self.CSPSummerWeekdayWorkhours, - # Hour_19=self.CSPSummerWeekdayNight, - # Hour_20=self.CSPSummerWeekdayNight, - # Hour_21=self.CSPSummerWeekdayNight, - # Hour_22=self.CSPSummerWeekdayNight, - # Hour_23=self.CSPSummerWeekdayNight, - # ) - - # csp_regular_week = WeekComponent( - # Name="CoolingSetpoint_Regular_Week", - # Monday=csp_regular_workday, - # Tuesday=csp_regular_workday, - # Wednesday=csp_regular_workday, - # Thursday=csp_regular_workday, - # Friday=csp_regular_workday, - # Saturday=csp_regular_weekend, - # Sunday=csp_regular_weekend, - # ) - - # csp_summer_week = WeekComponent( - # Name="CoolingSetpoint_Summer_Week", - # Monday=csp_summer_workday, - # Tuesday=csp_summer_workday, - # Wednesday=csp_summer_workday, - # Thursday=csp_summer_workday, - # Friday=csp_summer_workday, - # Saturday=csp_regular_weekend, - # Sunday=csp_regular_weekend, - # ) - - # csp_year = YearComponent( - # Name="CoolingSetpoint_Schedule", - # Type="Setpoint", - # January=csp_regular_week, - # February=csp_regular_week, - # March=csp_regular_week, - # April=csp_regular_week, - # May=csp_regular_week, - # June=csp_summer_week, - # July=csp_summer_week, - # August=csp_summer_week, - # September=csp_regular_week, - # October=csp_regular_week, - # November=csp_regular_week, - # December=csp_regular_week, - # ) - - setpoint_parametric = ParametricSetpoints( - HeatingSetpoint=self.HeatingSetpointBase, - DeadBand=self.SetpointDeadband, - HeatingSetback=self.HeatingSetpointSetback, - CoolingSetback=self.CoolingSetpointSetback, - NightSetback=self.NightSetback, - WeekendSetback=self.WeekendSetback, - SummerSetback=self.SummerSetback, - ) - - hsp_year, csp_year = setpoint_parametric.to_schedules() - - thermostat = ThermostatComponent( - Name="Thermostat", - IsOn=True, - HeatingSetpoint=hsp_year.January.Monday.Hour_12, - CoolingSetpoint=csp_year.January.Monday.Hour_12, - HeatingSchedule=hsp_year, - CoolingSchedule=csp_year, - ) - - equipment = EquipmentComponent( - Name="Equipment", - PowerDensity=self.EquipmentPowerDensity, - Schedule=equipment_schedule, - IsOn=True, - ) - - lighting = LightingComponent( - Name="Lighting", - PowerDensity=self.LightingPowerDensity, - Schedule=lighting_schedule, - IsOn=True, - DimmingType="Off", - ) - - occupancy = OccupancyComponent( - Name="Occupancy", - PeopleDensity=self.OccupantDensity, - Schedule=occupancy_schedule, - IsOn=True, - ) - - water_use = WaterUseComponent( - Name="WaterUse", - FlowRatePerPerson=self.DHWFlowRatePerPerson, - Schedule=occupancy_schedule, - ) - - space_use = ZoneSpaceUseComponent( - Name="SpaceUse", - Occupancy=occupancy, - Lighting=lighting, - Equipment=equipment, - Thermostat=thermostat, - WaterUse=water_use, - ) - - heating_system = ThermalSystemComponent( - Name="HeatingSystem", - ConditioningType="Heating", - Fuel=self.HeatingFuel, - SystemCOP=self.HeatingSystemCOP, - DistributionCOP=self.HeatingDistributionCOP, - ) - - cooling_system = ThermalSystemComponent( - Name="CoolingSystem", - ConditioningType="Cooling", - Fuel=self.CoolingFuel, - SystemCOP=self.CoolingSystemCOP, - DistributionCOP=self.CoolingDistributionCOP, - ) - - conditioning_system = ConditioningSystemsComponent( - Name="ConditioningSystem", - Heating=heating_system, - Cooling=cooling_system, - ) - - all_off_day = DayComponent( - Name="AllOffDayComponent", - Type="Fraction", - Hour_00=0.0, - Hour_01=0.0, - Hour_02=0.0, - Hour_03=0.0, - Hour_04=0.0, - Hour_05=0.0, - Hour_06=0.0, - Hour_07=0.0, - Hour_08=0.0, - Hour_09=0.0, - Hour_10=0.0, - Hour_11=0.0, - Hour_12=0.0, - Hour_13=0.0, - Hour_14=0.0, - Hour_15=0.0, - Hour_16=0.0, - Hour_17=0.0, - Hour_18=0.0, - Hour_19=0.0, - Hour_20=0.0, - Hour_21=0.0, - Hour_22=0.0, - Hour_23=0.0, - ) - - all_off_week = WeekComponent( - Name="AllOffWeekComponent", - Monday=all_off_day, - Tuesday=all_off_day, - Wednesday=all_off_day, - Thursday=all_off_day, - Friday=all_off_day, - Saturday=all_off_day, - Sunday=all_off_day, - ) - - all_off_year = YearComponent( - Name="AllOffYearComponent", - Type="Window", - January=all_off_week, - February=all_off_week, - March=all_off_week, - April=all_off_week, - May=all_off_week, - June=all_off_week, - July=all_off_week, - August=all_off_week, - September=all_off_week, - October=all_off_week, - November=all_off_week, - December=all_off_week, - ) - - ventilation_system = VentilationComponent( - Name="VentilationSystem", - FreshAirPerFloorArea=self.VentFlowRatePerArea, - FreshAirPerPerson=self.VentFlowRatePerPerson, - Provider=self.VentProvider, - # TODO: should hrv sensible/latent efficiency be configurable? (e.g. high/medium/low) - HRV=self.VentHRV, - Economizer=self.VentEconomizer, - DCV=self.VentDCV, - # TODO: this controls natvnent and should - # be configurable for residential models - Schedule=all_off_year, - ) - - hvac = ZoneHVACComponent( - Name="HVAC", - ConditioningSystems=conditioning_system, - Ventilation=ventilation_system, - ) - - dhw = DHWComponent( - Name="DHW", - SystemCOP=self.DHWSystemCOP, - DistributionCOP=self.DHWDistributionCOP, - # TODO: should these be configurable? - WaterTemperatureInlet=10, - WaterSupplyTemperature=55, - IsOn=True, - FuelType=self.DHWFuel, - ) - - operations = ZoneOperationsComponent( - Name="Operations", - SpaceUse=space_use, - HVAC=hvac, - DHW=dhw, - ) - - window_assembly = GlazingConstructionSimpleComponent( - Name="WindowAssembly", - UValue=self.WindowUValue, - SHGF=self.WindowSHGF, - TVis=self.WindowTVis, - Type="Single", - ) - - infiltration = InfiltrationComponent( - Name="Infiltration", - IsOn=True, - CalculationMethod="AirChanges/Hour", - AirChangesPerHour=self.InfiltrationACH, - ConstantCoefficient=0.0, - TemperatureCoefficient=0.0, - WindVelocityCoefficient=0.0, - WindVelocitySquaredCoefficient=0.0, - AFNAirMassFlowCoefficientCrack=0.0, - FlowPerExteriorSurfaceArea=0.0, - ) - - # TODO: verify interior/exterior - # TODO: are we okaky with mass assumptions? - facade = ConstructionAssemblyComponent( - Name="Facade", - Type="Facade", - Layers=[ - ConstructionLayerComponent( - ConstructionMaterial=clay_brick, - Thickness=0.002, - LayerOrder=0, - ), - ConstructionLayerComponent( - ConstructionMaterial=concrete_block_h, - Thickness=0.15, - LayerOrder=1, - ), - ConstructionLayerComponent( - ConstructionMaterial=fiberglass_batts, - Thickness=0.05, - LayerOrder=2, - ), - ConstructionLayerComponent( - ConstructionMaterial=gypsum_board, - Thickness=0.015, - LayerOrder=3, - ), - ], - ) - - facade_r_value_without_fiberglass = ( - facade.r_value - facade.sorted_layers[2].r_value - ) - - facade_r_value_delta = self.FacadeRValue - facade_r_value_without_fiberglass - required_fiberglass_thickness = ( - fiberglass_batts.Conductivity * facade_r_value_delta - ) - - if required_fiberglass_thickness < 0.003: - msg = f"Required Facade Fiberglass thickness is less than 3mm because the desired total facade R-value is {self.FacadeRValue} m²K/W but the concrete and gypsum layers already have a total R-value of {facade_r_value_without_fiberglass} m²K/W." - raise ValueError(msg) - - facade.sorted_layers[2].Thickness = required_fiberglass_thickness - - roof = ConstructionAssemblyComponent( - Name="Roof", - Type="FlatRoof", - Layers=[ - ConstructionLayerComponent( - ConstructionMaterial=xps_board, - Thickness=0.1, - LayerOrder=0, - ), - ConstructionLayerComponent( - ConstructionMaterial=concrete_mc_light, - Thickness=0.15, - LayerOrder=1, - ), - ConstructionLayerComponent( - ConstructionMaterial=concrete_rc_dense, - Thickness=0.2, - LayerOrder=2, - ), - ConstructionLayerComponent( - ConstructionMaterial=gypsum_board, - Thickness=0.02, - LayerOrder=3, - ), - ], - ) - - roof_r_value_without_xps = roof.r_value - roof.sorted_layers[0].r_value - roof_r_value_delta = self.RoofRValue - roof_r_value_without_xps - required_xps_thickness = xps_board.Conductivity * roof_r_value_delta - if required_xps_thickness < 0.003: - msg = f"Required Roof XPS thickness is less than 3mm because the desired total roof R-value is {self.RoofRValue} m²K/W but the concrete layers already have a total R-value of {roof_r_value_without_xps} m²K/W." - raise ValueError(msg) - - roof.sorted_layers[0].Thickness = required_xps_thickness - - partition = ConstructionAssemblyComponent( - Name="Partition", - Type="Partition", - Layers=[ - ConstructionLayerComponent( - ConstructionMaterial=gypsum_plaster, - Thickness=0.02, - LayerOrder=0, - ), - ConstructionLayerComponent( - ConstructionMaterial=softwood_general, - Thickness=0.02, - LayerOrder=1, - ), - ConstructionLayerComponent( - ConstructionMaterial=gypsum_plaster, - Thickness=0.02, - LayerOrder=2, - ), - ], + def zone_params_defaults(self) -> ZoneParams: + """Scalars used when building ZoneParams / ZoneComponents.""" + return ZoneParams.model_validate( + self.model_dump(include=set(ZoneParams.model_fields)) ) - floor_ceiling = ConstructionAssemblyComponent( - Name="FloorCeiling", - Type="FloorCeiling", - Layers=[ - ConstructionLayerComponent( - ConstructionMaterial=urethane_carpet, - Thickness=0.02, - LayerOrder=0, - ), - ConstructionLayerComponent( - ConstructionMaterial=cement_mortar, - Thickness=0.02, - LayerOrder=1, - ), - ConstructionLayerComponent( - ConstructionMaterial=concrete_rc_dense, - Thickness=0.15, - LayerOrder=2, - ), - ConstructionLayerComponent( - ConstructionMaterial=gypsum_board, - Thickness=0.02, - LayerOrder=3, - ), - ], + def zone_assignment_table(self) -> ZoneAssignmentTable: + """Building defaults plus sparse overrides for per-zone assignment.""" + return ZoneAssignmentTable( + defaults=self.zone_params_defaults(), + overrides=self.zone_overrides, ) - ground_slab_assembly = ConstructionAssemblyComponent( - Name="GroundSlabAssembly", - Type="GroundSlab", - Layers=[ - ConstructionLayerComponent( - ConstructionMaterial=xps_board, - Thickness=0.02, - LayerOrder=0, - ), - ConstructionLayerComponent( - ConstructionMaterial=concrete_rc_dense, - Thickness=0.15, - LayerOrder=1, - ), - ConstructionLayerComponent( - ConstructionMaterial=concrete_mc_light, - Thickness=0.04, - LayerOrder=2, - ), - ConstructionLayerComponent( - ConstructionMaterial=cement_mortar, - Thickness=0.03, - LayerOrder=3, - ), - ConstructionLayerComponent( - ConstructionMaterial=ceramic_tile, - Thickness=0.02, - LayerOrder=4, - ), - ], - ) + def to_building(self) -> BuildingFlatModel: + """Convert to structured building + floor zone assignment model.""" + from epinterface.sbem.building_flat_model import BuildingFlatModel - ground_slab_r_value_without_xps = ( - ground_slab_assembly.r_value - ground_slab_assembly.sorted_layers[0].r_value - ) - ground_slab_r_value_delta = self.SlabRValue - ground_slab_r_value_without_xps - required_xps_thickness = xps_board.Conductivity * ground_slab_r_value_delta - if required_xps_thickness < 0.003: - msg = f"Required Ground Slab XPS thickness is less than 3mm because the desired total slab R-value is {self.SlabRValue} m²K/W but the concrete layers already have a total R-value of {ground_slab_r_value_without_xps} m²K/W." - raise ValueError(msg) - - ground_slab_assembly.sorted_layers[0].Thickness = required_xps_thickness - - assemblies = EnvelopeAssemblyComponent( - Name="EnvelopeAssemblies", - FacadeAssembly=facade, - FlatRoofAssembly=roof, - AtticRoofAssembly=roof, - PartitionAssembly=partition, - FloorCeilingAssembly=floor_ceiling, - AtticFloorAssembly=floor_ceiling, - BasementCeilingAssembly=floor_ceiling, - GroundSlabAssembly=ground_slab_assembly, - GroundWallAssembly=ground_slab_assembly, - ExternalFloorAssembly=ground_slab_assembly, - ) + return BuildingFlatModel.from_flat_model(self) - basement_infiltration = infiltration.model_copy(deep=True) - envelope = ZoneEnvelopeComponent( - Name="Envelope", - AtticInfiltration=infiltration, - BasementInfiltration=basement_infiltration, - Window=window_assembly, - Infiltration=infiltration, - Assemblies=assemblies, - ) + @classmethod + def from_building(cls, building: BuildingFlatModel) -> FlatModel: + """Build legacy flat model from building + floor structure.""" + return building.to_flat_model() - zone = ZoneComponent( - Name="Zone", - Operations=operations, - Envelope=envelope, + def resolve_params( + self, + key: ParsedZoneKey, + *, + geometry: ShoeboxGeometry, + attic_use_fraction: float | None = None, + attic_conditioned: bool = False, + basement_use_fraction: float | None = None, + basement_conditioned: bool = False, + ) -> ZoneParams: + """Merge defaults and overrides for one zone key (incl. attic/basement scaling).""" + return self.zone_assignment_table().resolve( + key, + attic_use_fraction=attic_use_fraction, + attic_conditioned=attic_conditioned, + basement_use_fraction=basement_use_fraction, + basement_conditioned=basement_conditioned, + geometry=geometry, ) - return zone + def to_zone(self) -> ZoneComponent: + """Convert the flat model defaults to a single zone component.""" + return zone_params_to_zone_component(self.zone_params_defaults()) def to_model(self) -> tuple[Model, Callable[[IDF], IDF]]: """Returns a tuple of a Model and a post-geometry callback.""" - zone = self.to_zone() - # TODO: add in a shading mask geometry = ShoeboxGeometry( x=0, y=0, @@ -2095,8 +2206,7 @@ def to_model(self) -> tuple[Model, Callable[[IDF], IDF]]: d=self.Depth, h=self.F2FHeight, num_stories=self.NFloors, - # TODO: should core/perim be dependent on width, depth > 9m? - zoning="core/perim", + zoning=self.zoning, roof_height=None, wwr=self.WWR, basement=False, @@ -2109,7 +2219,8 @@ def post_geometry_callback(idf: IDF) -> IDF: return ( Model( geometry=geometry, - Zone=zone, + Zone=None, + zone_assignments=self.zone_assignment_table(), Attic=AtticAssumptions( UseFraction=None, Conditioned=False, @@ -2123,6 +2234,90 @@ def post_geometry_callback(idf: IDF) -> IDF: post_geometry_callback, ) + def build_idf( + self, + *, + output_dir: Path | None = None, + weather_dir: Path | None = None, + ) -> IDF: + """Build an IDF without running EnergyPlus.""" + model, cb = self.to_model() + out = ( + Path(tempfile.mkdtemp(prefix="flat_build_")) + if output_dir is None + else Path(output_dir) + ) + out.mkdir(parents=True, exist_ok=True) + cfg = ( + SimulationPathConfig(output_dir=out, weather_dir=weather_dir) + if weather_dir is not None + else SimulationPathConfig(output_dir=out) + ) + return model.build(cfg, post_geometry_callback=cb) + + def zone_assignment_summary(self, idf: IDF | None = None) -> pd.DataFrame: + """Resolved zone params plus optional cross-check against built IDF.""" + model, _ = self.to_model() + geom = model.geometry + tbl = self.zone_assignment_table() + idf_built = idf or self.build_idf() + lights_by_zone: dict[str, float] = {} + for lg in idf_built.idfobjects["LIGHTS"]: + zn = _eppy_lights_zone_name(lg) + lights_by_zone[zn] = _eppy_lights_watts_per_area(lg) + + facade_by_zone: dict[str, str] = {} + for srf in idf_built.idfobjects["BUILDINGSURFACE:DETAILED"]: + if ( + str(srf.Surface_Type).lower() == "wall" + and str(srf.Outside_Boundary_Condition).lower() == "outdoors" + ): + zn = str(srf.Zone_Name) + facade_by_zone.setdefault(zn, str(srf.Construction_Name)) + + rows = [] + for z in idf_built.idfobjects["ZONE"]: + zn = z.Name + key = parse_zone_key(zn, geom) + params = tbl.resolve( + key, + attic_use_fraction=model.Attic.UseFraction, + attic_conditioned=model.Attic.Conditioned, + basement_use_fraction=model.Basement.UseFraction, + basement_conditioned=model.Basement.Conditioned, + geometry=geom, + ) + rows.append({ + "ep_zone_name": zn, + "storey_index": key.storey_index, + "role": key.role.value, + "category": key.category, + "LightingPowerDensity": params.LightingPowerDensity, + "EquipmentPowerDensity": params.EquipmentPowerDensity, + "FacadeRValue": params.FacadeRValue, + "idf_lighting_w_per_m2": lights_by_zone.get(zn), + "idf_facade_construction": facade_by_zone.get(zn), + }) + return pd.DataFrame(rows) + + def zone_simulation_summary(self, results: ModelRunResults) -> pd.DataFrame: + """Assignment summary joined with per-zone simulated energy from a model run.""" + from epinterface.analysis.zone_energy import ( + merge_assignment_and_energy, + zone_energy_summary, + ) + + assign = self.zone_assignment_summary(idf=results.idf) + energy = zone_energy_summary(results.sql, results.idf) + return merge_assignment_and_energy(assign, energy) + + @classmethod + def example_with_zone_overrides(cls) -> FlatModel: + """Two-storey core/perim shoebox with contrasting zone overrides.""" + from epinterface.sbem.building_flat_model import BuildingFlatModel + + return BuildingFlatModel.example_with_zone_overrides().to_flat_model() + def simulate( self, overheating_config: OverheatingAnalysisConfig | None = None, @@ -2141,81 +2336,34 @@ def simulate( if __name__ == "__main__": - flat_model = FlatModel( - F2FHeight=3.25, - Width=40, - Depth=40, - Rotation=45, - WWR=0.3, - NFloors=2, - FacadeRValue=3.0, - RoofRValue=3.0, - SlabRValue=3.0, - WindowUValue=3.0, - WindowSHGF=0.7, - WindowTVis=0.5, - InfiltrationACH=0.5, - VentFlowRatePerArea=0.001, - VentFlowRatePerPerson=0.0085, - VentProvider="Mechanical", - VentHRV="NoHRV", - VentEconomizer="NoEconomizer", - VentDCV="NoDCV", - DHWFlowRatePerPerson=0.010, - DHWFuel="Electricity", - DHWSystemCOP=1.0, - DHWDistributionCOP=1.0, - EquipmentPowerDensity=25, - LightingPowerDensity=10, - OccupantDensity=0.01, - EquipmentBase=0.4, - EquipmentAMInterp=0.5, - EquipmentLunchInterp=0.8, - EquipmentPMInterp=0.5, - EquipmentWeekendPeakInterp=0.25, - EquipmentSummerPeakInterp=0.5, - LightingBase=0.3, - LightingAMInterp=0.75, - LightingLunchInterp=0.75, - LightingPMInterp=0.9, - LightingWeekendPeakInterp=0.75, - LightingSummerPeakInterp=0.9, - OccupancyBase=0.05, - OccupancyAMInterp=0.25, - OccupancyLunchInterp=0.9, - OccupancyPMInterp=0.5, - OccupancyWeekendPeakInterp=0.15, - OccupancySummerPeakInterp=0.85, - # HSPRegularWeekdayWorkhours=21, - # HSPRegularWeekdayNight=21, - # HSPSummerWeekdayWorkhours=21, - # HSPSummerWeekdayNight=21, - # HSPWeekendWorkhours=21, - # HSPWeekendNight=21, - # CSPRegularWeekdayWorkhours=23, - # CSPRegularWeekdayNight=23, - # CSPSummerWeekdayWorkhours=23, - # CSPSummerWeekdayNight=23, - # CSPWeekendWorkhours=23, - # CSPWeekendNight=23, - HeatingSetpointBase=21, - SetpointDeadband=2, - HeatingSetpointSetback=2, - CoolingSetpointSetback=2, - NightSetback=0.5, - WeekendSetback=0.5, - SummerSetback=0.5, - HeatingFuel="Electricity", - CoolingFuel="Electricity", - HeatingSystemCOP=1.0, - CoolingSystemCOP=1.0, - HeatingDistributionCOP=1.0, - CoolingDistributionCOP=1.0, - EPWURI=WeatherUrl( # pyright: ignore [reportCallIssue] - "https://climate.onebuilding.org/WMO_Region_4_North_and_Central_America/USA_United_States_of_America/MA_Massachusetts/USA_MA_Bedford-Hanscom.Field.AP.744900_TMYx.2009-2023.zip" - ), + run_flat = Path("run_flat") + run_flat.mkdir(exist_ok=True) + fm = FlatModel.example_with_zone_overrides() + print("running energyplus simulation (uses bundled weather zip)...") + run_results = fm.simulate(eplus_parent_dir=run_flat) + merged = fm.zone_simulation_summary(run_results) + cols = [ + "storey_index", + "role", + "LightingPowerDensity", + "sim_lighting_kwh_per_m2", + "sim_total_kwh_per_m2", + ] + print( + merged[cols] + .drop_duplicates() + .sort_values(["storey_index", "role"]) + .to_string(index=False) ) + try: + from epinterface.analysis.zone_assignment_viz import plot_assignment_and_energy - r = flat_model.simulate() - - print(r.energy_and_peak.groupby(level=["Measurement", "Aggregation"]).sum()) + _assign_fig, energy_fig, _merged = plot_assignment_and_energy( + fm.zone_assignment_summary(idf=run_results.idf), + results=run_results, + ) + out_png = run_flat / "zone_assignment_energy.png" + cast(Any, energy_fig).savefig(out_png, bbox_inches="tight") + print(f"wrote {out_png}") + except ImportError as err: + print("matplotlib not installed (uv sync --group dev):", err) diff --git a/epinterface/sbem/zone_assignment.py b/epinterface/sbem/zone_assignment.py new file mode 100644 index 0000000..56b0664 --- /dev/null +++ b/epinterface/sbem/zone_assignment.py @@ -0,0 +1,157 @@ +"""Zone name parsing and sparse overrides merged into ZoneParams.""" + +from __future__ import annotations + +import re +from dataclasses import dataclass +from enum import StrEnum +from typing import Any, Literal + +from pydantic import BaseModel, Field + +from epinterface.geometry import ShoeboxGeometry +from epinterface.sbem.zone_params import ZoneParams + + +class ZoneRole(StrEnum): + """Thermal-zone role within a storey (matches geomeppy shoebox naming).""" + + core = "core" + perim_1 = "perim_1" + perim_2 = "perim_2" + perim_3 = "perim_3" + perim_4 = "perim_4" + floor = "floor" + attic = "attic" + + +@dataclass(frozen=True) +class ParsedZoneKey: + """Parsed EP zone name -> assignment indices.""" + + storey_index: int + role: ZoneRole + category: Literal["main", "attic", "basement"] + + +_STOREY_RE = re.compile(r"Storey\s+(-?\d+)\s*$", re.IGNORECASE) +_PERIM_RE = re.compile(r"Perimeter_Zone_(\d+)", re.IGNORECASE) + + +def parse_zone_key(zone_name: str, geometry: ShoeboxGeometry) -> ParsedZoneKey: + """Map EnergyPlus zone name to storey index and role.""" + zn = zone_name.strip() + low = zn.lower() + if "attic" in low: + return ParsedZoneKey(storey_index=-1, role=ZoneRole.attic, category="attic") + + m = _STOREY_RE.search(zn) + if not m: + msg = f"cannot parse storey index from zone name: {zone_name!r}" + raise ValueError(msg) + storey_index = int(m.group(1)) + + if geometry.basement: + if geometry.zoning == "by_storey": + category: Literal["main", "attic", "basement"] = ( + "basement" if storey_index == -1 else "main" + ) + else: + category = "basement" if storey_index == 0 else "main" + else: + category = "main" + + if geometry.zoning == "by_storey": + return ParsedZoneKey( + storey_index=storey_index, role=ZoneRole.floor, category=category + ) + + if "core_zone" in low: + return ParsedZoneKey( + storey_index=storey_index, role=ZoneRole.core, category=category + ) + + pm = _PERIM_RE.search(zn) + if pm: + idx = int(pm.group(1)) + if idx not in range(1, 5): + msg = f"unexpected perimeter index {idx} in {zone_name!r}" + raise ValueError(msg) + role = ZoneRole(f"perim_{idx}") + return ParsedZoneKey(storey_index=storey_index, role=role, category=category) + + msg = f"cannot classify core/perimeter zone name: {zone_name!r}" + raise ValueError(msg) + + +class ZoneAssignmentTable(BaseModel): + """Building defaults plus sparse (storey, role) patches.""" + + defaults: ZoneParams + overrides: dict[int, dict[str, dict[str, Any]]] = Field(default_factory=dict) + + def merged_patch(self, storey_index: int, role: ZoneRole) -> dict[str, Any]: + """Return override dict for one zone key.""" + return dict(self.overrides.get(storey_index, {}).get(role.value, {})) + + def resolve_base_params(self, key: ParsedZoneKey) -> ZoneParams: + """Apply YAML-style overrides only (no attic/basement occupancy scaling).""" + data = self.defaults.model_dump() + patch = self.merged_patch(key.storey_index, key.role) + data.update({k: v for k, v in patch.items() if v is not None}) + return ZoneParams.model_validate(data) + + def resolve( + self, + key: ParsedZoneKey, + *, + attic_use_fraction: float | None, + attic_conditioned: bool, + basement_use_fraction: float | None, + basement_conditioned: bool, + geometry: ShoeboxGeometry, + ) -> ZoneParams: + """Resolve patch plus attic/basement behaviour matching legacy Model.build.""" + params = self.resolve_base_params(key) + + if key.category == "attic": + frac = attic_use_fraction or 0 + params = params.model_copy( + update={ + "EquipmentPowerDensity": frac * params.EquipmentPowerDensity, + "LightingPowerDensity": frac * params.LightingPowerDensity, + "OccupantDensity": frac * params.OccupantDensity, + } + ) + if not attic_conditioned: + params = params.model_copy( + update={ + "VentProvider": "None", + "IdealLoadsHeatingOn": False, + "IdealLoadsCoolingOn": False, + } + ) + return params + + if key.category == "basement": + frac = basement_use_fraction or 0 + params = params.model_copy( + update={ + "EquipmentPowerDensity": frac * params.EquipmentPowerDensity, + "LightingPowerDensity": frac * params.LightingPowerDensity, + "OccupantDensity": frac * params.OccupantDensity, + } + ) + if not basement_conditioned: + params = params.model_copy( + update={ + "VentProvider": "None", + "IdealLoadsHeatingOn": False, + "IdealLoadsCoolingOn": False, + } + ) + else: + params = params.model_copy(update={"IdealLoadsCoolingOn": False}) + return params + + return params diff --git a/epinterface/sbem/zone_params.py b/epinterface/sbem/zone_params.py new file mode 100644 index 0000000..5d7fc5b --- /dev/null +++ b/epinterface/sbem/zone_params.py @@ -0,0 +1,113 @@ +"""Scalar zone inputs shared by FlatModel and per-zone assignment.""" + +from __future__ import annotations + +from pydantic import BaseModel, Field, field_validator + +from epinterface.sbem.components.systems import ( + DCVMethod, + DHWFuelType, + EconomizerMethod, + FuelType, + HRVMethod, + VentilationProvider, +) + + +class ZoneParams(BaseModel): + """Building-zone calibration parameters used to construct a ZoneComponent.""" + + EquipmentBase: float = Field(ge=0, le=1) + EquipmentAMInterp: float = Field(ge=0, le=1) + EquipmentLunchInterp: float = Field(ge=0, le=1) + EquipmentPMInterp: float = Field(ge=0, le=1) + EquipmentWeekendPeakInterp: float = Field(ge=0, le=1) + EquipmentSummerPeakInterp: float = Field(ge=0, le=1) + + LightingBase: float = Field(ge=0, le=1) + LightingAMInterp: float = Field(ge=0, le=1) + LightingLunchInterp: float = Field(ge=0, le=1) + LightingPMInterp: float = Field(ge=0, le=1) + LightingWeekendPeakInterp: float = Field(ge=0, le=1) + LightingSummerPeakInterp: float = Field(ge=0, le=1) + + OccupancyBase: float = Field(ge=0, le=1) + OccupancyAMInterp: float = Field(ge=0, le=1) + OccupancyLunchInterp: float = Field(ge=0, le=1) + OccupancyPMInterp: float = Field(ge=0, le=1) + OccupancyWeekendPeakInterp: float = Field(ge=0, le=1) + OccupancySummerPeakInterp: float = Field(ge=0, le=1) + + HeatingSetpointBase: float = Field(ge=0, le=23) + SetpointDeadband: float = Field(ge=0, le=10) + HeatingSetpointSetback: float = Field(ge=0, le=10) + CoolingSetpointSetback: float = Field(ge=0, le=10) + NightSetback: float = Field(ge=0, le=1) + WeekendSetback: float = Field(ge=0, le=1) + SummerSetback: float = Field(ge=0, le=1) + + HeatingFuel: FuelType + CoolingFuel: FuelType + HeatingSystemCOP: float + CoolingSystemCOP: float + HeatingDistributionCOP: float + CoolingDistributionCOP: float + + EquipmentPowerDensity: float = Field(ge=0, le=200) + LightingPowerDensity: float = Field(ge=0, le=100) + OccupantDensity: float = Field(ge=0, le=50) + + VentFlowRatePerPerson: float + VentFlowRatePerArea: float + VentProvider: VentilationProvider + VentHRV: HRVMethod + VentEconomizer: EconomizerMethod + VentDCV: DCVMethod + + DHWFlowRatePerPerson: float + DHWFuel: DHWFuelType + DHWSystemCOP: float + DHWDistributionCOP: float + + InfiltrationACH: float + + WindowUValue: float + WindowSHGF: float + WindowTVis: float + + FacadeRValue: float + RoofRValue: float + SlabRValue: float + + WWR: float + F2FHeight: float + NFloors: int + Width: float + Depth: float + Rotation: float + + IdealLoadsHeatingOn: bool = True + IdealLoadsCoolingOn: bool = True + + # optional flat 24h weekday profiles (0-1); when set, used instead of parametric + CustomLightingWeekdayHourly: list[float] | None = None + CustomEquipmentWeekdayHourly: list[float] | None = None + CustomOccupancyWeekdayHourly: list[float] | None = None + + @field_validator( + "CustomLightingWeekdayHourly", + "CustomEquipmentWeekdayHourly", + "CustomOccupancyWeekdayHourly", + ) + @classmethod + def _validate_hourly_profile(cls, value: list[float] | None) -> list[float] | None: + if value is None: + return None + if len(value) != 24: + msg = "hourly profile must have exactly 24 values" + raise ValueError(msg) + for hour, fraction in enumerate(value): + if not 0 <= fraction <= 1: + msg = f"hour {hour} value {fraction} out of range [0, 1]" + raise ValueError(msg) + return value diff --git a/pyproject.toml b/pyproject.toml index 48ce9e1..dc6bc46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,7 @@ epi = "epinterface.cli:cli" [dependency-groups] dev = [ "jupyter~=1.1.1", + "matplotlib>=3.8,<4", "pandas-stubs>=2.2,<2.3", "pre-commit~=4.2.0", "pyarrow~=20.0.0", diff --git a/uv.lock b/uv.lock index 1adfa13..c6cb6c6 100644 --- a/uv.lock +++ b/uv.lock @@ -592,6 +592,7 @@ dependencies = [ [package.dev-dependencies] dev = [ { name = "jupyter" }, + { name = "matplotlib" }, { name = "pandas-stubs" }, { name = "pre-commit" }, { name = "pyarrow" }, @@ -627,6 +628,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ { name = "jupyter", specifier = "~=1.1.1" }, + { name = "matplotlib", specifier = ">=3.8,<4" }, { name = "pandas-stubs", specifier = ">=2.2,<2.3" }, { name = "pre-commit", specifier = "~=4.2.0" }, { name = "pyarrow", specifier = "~=20.0.0" },