Skip to content

Components

Components for the SBEM model.

Space Use#

Space use components for the SBEM library..

EquipmentComponent #

Bases: NamedObject, MetadataMixin

An equipment object in the SBEM library.

Source code in epinterface/sbem/components/space_use.py
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
class EquipmentComponent(NamedObject, MetadataMixin, extra="forbid"):
    """An equipment object in the SBEM library."""

    PowerDensity: float = Field(..., title="Equipment density of the object [W/m2]")
    Schedule: YearComponent = Field(
        ..., title="Equipment schedule of the object [frac]"
    )
    IsOn: BoolStr = Field(..., title="Equipment is on")

    def add_equipment_to_idf_zone(
        self, idf: IDF, target_zone_or_zone_list_name: str
    ) -> IDF:
        """Add equipment to an IDF zone.

        Args:
            idf (IDF): The IDF object to add the equipment to.
            target_zone_or_zone_list_name (str): The name of the zone or zone list to add the equipment to.

        Returns:
            IDF: The updated IDF object.
        """
        if not self.IsOn:
            return idf

        name_prefix = f"{target_zone_or_zone_list_name}_{self.safe_name}_EQUIPMENT"
        idf, year_name = self.Schedule.add_year_to_idf(
            idf,
            name_prefix=None,
            summer_design_day_sch_name="d_AllOn_00",
            winter_design_day_sch_name="d_AllOn_00",
        )
        equipment = ElectricEquipment(
            Name=name_prefix,
            Zone_or_ZoneList_Name=target_zone_or_zone_list_name,
            Schedule_Name=year_name,
            Design_Level_Calculation_Method="Watts/Area",
            Watts_per_Zone_Floor_Area=self.PowerDensity,
            Watts_per_Person=None,
            Fraction_Latent=assumed_constants.FractionLatentEquipment,
            Fraction_Radiant=assumed_constants.FractionRadiantEquipment,
            Fraction_Lost=assumed_constants.FractionLostEquipment,
            EndUse_Subcategory=None,
        )
        idf = equipment.add(idf)
        return idf

add_equipment_to_idf_zone(idf, target_zone_or_zone_list_name) #

Add equipment to an IDF zone.

Parameters:

Name Type Description Default
idf IDF

The IDF object to add the equipment to.

required
target_zone_or_zone_list_name str

The name of the zone or zone list to add the equipment to.

required

Returns:

Name Type Description
IDF IDF

The updated IDF object.

Source code in epinterface/sbem/components/space_use.py
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
def add_equipment_to_idf_zone(
    self, idf: IDF, target_zone_or_zone_list_name: str
) -> IDF:
    """Add equipment to an IDF zone.

    Args:
        idf (IDF): The IDF object to add the equipment to.
        target_zone_or_zone_list_name (str): The name of the zone or zone list to add the equipment to.

    Returns:
        IDF: The updated IDF object.
    """
    if not self.IsOn:
        return idf

    name_prefix = f"{target_zone_or_zone_list_name}_{self.safe_name}_EQUIPMENT"
    idf, year_name = self.Schedule.add_year_to_idf(
        idf,
        name_prefix=None,
        summer_design_day_sch_name="d_AllOn_00",
        winter_design_day_sch_name="d_AllOn_00",
    )
    equipment = ElectricEquipment(
        Name=name_prefix,
        Zone_or_ZoneList_Name=target_zone_or_zone_list_name,
        Schedule_Name=year_name,
        Design_Level_Calculation_Method="Watts/Area",
        Watts_per_Zone_Floor_Area=self.PowerDensity,
        Watts_per_Person=None,
        Fraction_Latent=assumed_constants.FractionLatentEquipment,
        Fraction_Radiant=assumed_constants.FractionRadiantEquipment,
        Fraction_Lost=assumed_constants.FractionLostEquipment,
        EndUse_Subcategory=None,
    )
    idf = equipment.add(idf)
    return idf

LightingComponent #

Bases: NamedObject, MetadataMixin

A lighting object in the SBEM library.

Source code in epinterface/sbem/components/space_use.py
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
class LightingComponent(NamedObject, MetadataMixin, extra="forbid"):
    """A lighting object in the SBEM library."""

    PowerDensity: float = Field(
        ...,
        title="Lighting density of the object [W/m2]",
        ge=0,
    )

    DimmingType: DimmingTypeType = Field(
        ...,
        title="Dimming type",
    )
    Schedule: YearComponent = Field(..., title="Lighting schedule of the object [frac]")
    IsOn: BoolStr = Field(..., title="Lights are on")

    def add_lights_to_idf_zone(
        self, idf: IDF, target_zone_or_zone_list_name: str
    ) -> IDF:
        """Add lights to an IDF zone.

        Note that this makes some assumptions about the fraction visible/radiant/replaceable.

        Args:
            idf (IDF): The IDF object to add the lights to.
            target_zone_or_zone_list_name (str): The name of the zone or zone list to add the lights to.

        Returns:
            IDF: The updated IDF object.
        """
        if not self.IsOn:
            return idf

        if self.DimmingType != "Off":
            raise NotImplementedParameter("DimmingType:On", self.Name, "Lights")

        name_prefix = f"{target_zone_or_zone_list_name}_{self.safe_name}_LIGHTS"
        idf, year_name = self.Schedule.add_year_to_idf(
            idf,
            name_prefix=None,
            summer_design_day_sch_name="d_AllOn_00",
            winter_design_day_sch_name="d_AllOn_00",
        )
        lights = Lights(
            Name=name_prefix,
            Zone_or_ZoneList_Name=target_zone_or_zone_list_name,
            Schedule_Name=year_name,
            Design_Level_Calculation_Method="Watts/Area",
            Watts_per_Zone_Floor_Area=self.PowerDensity,
            Watts_per_Person=None,
            Lighting_Level=None,
            Return_Air_Fraction=assumed_constants.ReturnAirFractionLights,
            Fraction_Radiant=assumed_constants.FractionRadiantLights,
            Fraction_Visible=assumed_constants.FractionVisibleLights,
            Fraction_Replaceable=assumed_constants.FractionReplaceableLights,
            EndUse_Subcategory=None,
        )
        idf = lights.add(idf)
        return idf

add_lights_to_idf_zone(idf, target_zone_or_zone_list_name) #

Add lights to an IDF zone.

Note that this makes some assumptions about the fraction visible/radiant/replaceable.

Parameters:

Name Type Description Default
idf IDF

The IDF object to add the lights to.

required
target_zone_or_zone_list_name str

The name of the zone or zone list to add the lights to.

required

Returns:

Name Type Description
IDF IDF

The updated IDF object.

Source code in epinterface/sbem/components/space_use.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
def add_lights_to_idf_zone(
    self, idf: IDF, target_zone_or_zone_list_name: str
) -> IDF:
    """Add lights to an IDF zone.

    Note that this makes some assumptions about the fraction visible/radiant/replaceable.

    Args:
        idf (IDF): The IDF object to add the lights to.
        target_zone_or_zone_list_name (str): The name of the zone or zone list to add the lights to.

    Returns:
        IDF: The updated IDF object.
    """
    if not self.IsOn:
        return idf

    if self.DimmingType != "Off":
        raise NotImplementedParameter("DimmingType:On", self.Name, "Lights")

    name_prefix = f"{target_zone_or_zone_list_name}_{self.safe_name}_LIGHTS"
    idf, year_name = self.Schedule.add_year_to_idf(
        idf,
        name_prefix=None,
        summer_design_day_sch_name="d_AllOn_00",
        winter_design_day_sch_name="d_AllOn_00",
    )
    lights = Lights(
        Name=name_prefix,
        Zone_or_ZoneList_Name=target_zone_or_zone_list_name,
        Schedule_Name=year_name,
        Design_Level_Calculation_Method="Watts/Area",
        Watts_per_Zone_Floor_Area=self.PowerDensity,
        Watts_per_Person=None,
        Lighting_Level=None,
        Return_Air_Fraction=assumed_constants.ReturnAirFractionLights,
        Fraction_Radiant=assumed_constants.FractionRadiantLights,
        Fraction_Visible=assumed_constants.FractionVisibleLights,
        Fraction_Replaceable=assumed_constants.FractionReplaceableLights,
        EndUse_Subcategory=None,
    )
    idf = lights.add(idf)
    return idf

OccupancyComponent #

Bases: NamedObject, MetadataMixin

An occupancy object in the SBEM library.

Source code in epinterface/sbem/components/space_use.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
class OccupancyComponent(NamedObject, MetadataMixin, extra="forbid"):
    """An occupancy object in the SBEM library."""

    PeopleDensity: float = Field(
        ...,
        title="Occupancy density of the object [ppl/m2]",
        ge=0,
    )
    Schedule: YearComponent = Field(
        ..., title="Occupancy schedule of the object [frac]"
    )
    IsOn: BoolStr = Field(..., title="People are on")
    MetabolicRate: float = assumed_constants.MetabolicRate_met

    @property
    def MetabolicRate_met_to_W(self):
        """Get the metabolic rate in Watts."""
        avg_human_weight_kg = assumed_constants.AvgHumanWeight_kg
        conversion_factor = physical_constants.ConversionFactor_W_per_kg
        # mets * kg * W/kg = W
        return self.MetabolicRate * avg_human_weight_kg * conversion_factor

    def add_people_to_idf_zone(
        self, idf: IDF, target_zone_or_zone_list_name: str
    ) -> IDF:
        """Add people to an IDF zone.

        Args:
            idf (IDF): The IDF object to add the people to.
            target_zone_or_zone_list_name (str): The name of the zone or zone list to add the people to.

        Returns:
            IDF: The updated IDF object.
        """
        if not self.IsOn:
            return idf

        activity_sch_name = (
            f"{target_zone_or_zone_list_name}_{self.safe_name}_PEOPLE_Activity_Schedule"
        )
        lim = ScheduleTypeLimits(
            Name="AnyNumber",
            LowerLimit=None,
            UpperLimit=None,
        )
        if not idf.getobject("SCHEDULETYPELIMITS", lim.Name):
            lim.to_epbunch(idf)
        activity_sch = Schedule.from_values(
            Values=[self.MetabolicRate_met_to_W] * 8760,
            Name=activity_sch_name,
            Type=lim,  # pyright: ignore [reportArgumentType]
        )
        activity_sch_year, *_ = activity_sch.to_year_week_day()
        activity_sch_year.to_epbunch(idf)

        name_prefix = f"{target_zone_or_zone_list_name}_{self.safe_name}_PEOPLE"
        idf, year_name = self.Schedule.add_year_to_idf(
            idf,
            name_prefix=None,
            summer_design_day_sch_name="d_AllOn_00",
            winter_design_day_sch_name="d_AllOn_00",
        )
        people = People(
            Name=name_prefix,
            Zone_or_ZoneList_Name=target_zone_or_zone_list_name,
            Number_of_People_Schedule_Name=year_name,
            Number_of_People_Calculation_Method="People/Area",
            Number_of_People=None,
            Floor_Area_per_Person=None,
            People_per_Floor_Area=self.PeopleDensity,
            Fraction_Radiant=assumed_constants.FractionRadiantPeople,
            Sensible_Heat_Fraction="autocalculate",
            Activity_Level_Schedule_Name=activity_sch_year.Name,
        )

        idf = people.add(idf)
        return idf

MetabolicRate_met_to_W property #

Get the metabolic rate in Watts.

add_people_to_idf_zone(idf, target_zone_or_zone_list_name) #

Add people to an IDF zone.

Parameters:

Name Type Description Default
idf IDF

The IDF object to add the people to.

required
target_zone_or_zone_list_name str

The name of the zone or zone list to add the people to.

required

Returns:

Name Type Description
IDF IDF

The updated IDF object.

Source code in epinterface/sbem/components/space_use.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
def add_people_to_idf_zone(
    self, idf: IDF, target_zone_or_zone_list_name: str
) -> IDF:
    """Add people to an IDF zone.

    Args:
        idf (IDF): The IDF object to add the people to.
        target_zone_or_zone_list_name (str): The name of the zone or zone list to add the people to.

    Returns:
        IDF: The updated IDF object.
    """
    if not self.IsOn:
        return idf

    activity_sch_name = (
        f"{target_zone_or_zone_list_name}_{self.safe_name}_PEOPLE_Activity_Schedule"
    )
    lim = ScheduleTypeLimits(
        Name="AnyNumber",
        LowerLimit=None,
        UpperLimit=None,
    )
    if not idf.getobject("SCHEDULETYPELIMITS", lim.Name):
        lim.to_epbunch(idf)
    activity_sch = Schedule.from_values(
        Values=[self.MetabolicRate_met_to_W] * 8760,
        Name=activity_sch_name,
        Type=lim,  # pyright: ignore [reportArgumentType]
    )
    activity_sch_year, *_ = activity_sch.to_year_week_day()
    activity_sch_year.to_epbunch(idf)

    name_prefix = f"{target_zone_or_zone_list_name}_{self.safe_name}_PEOPLE"
    idf, year_name = self.Schedule.add_year_to_idf(
        idf,
        name_prefix=None,
        summer_design_day_sch_name="d_AllOn_00",
        winter_design_day_sch_name="d_AllOn_00",
    )
    people = People(
        Name=name_prefix,
        Zone_or_ZoneList_Name=target_zone_or_zone_list_name,
        Number_of_People_Schedule_Name=year_name,
        Number_of_People_Calculation_Method="People/Area",
        Number_of_People=None,
        Floor_Area_per_Person=None,
        People_per_Floor_Area=self.PeopleDensity,
        Fraction_Radiant=assumed_constants.FractionRadiantPeople,
        Sensible_Heat_Fraction="autocalculate",
        Activity_Level_Schedule_Name=activity_sch_year.Name,
    )

    idf = people.add(idf)
    return idf

ThermostatComponent #

Bases: NamedObject, MetadataMixin

A thermostat object in the SBEM library.

Source code in epinterface/sbem/components/space_use.py
212
213
214
215
216
217
218
219
220
221
222
223
224
225
class ThermostatComponent(NamedObject, MetadataMixin, extra="forbid"):
    """A thermostat object in the SBEM library."""

    IsOn: BoolStr = Field(..., title="Thermostat is on")
    HeatingSetpoint: float = Field(
        ...,
        title="Heating setpoint of the object; ignored if schedule is present, except also used for determining natural ventilation boundaries.",
    )
    HeatingSchedule: YearComponent = Field(..., title="Heating schedule of the object")
    CoolingSetpoint: float = Field(
        ...,
        title="Cooling setpoint of the object; ignored if schedule is present, except also used for determining natural ventilation boundaries.",
    )
    CoolingSchedule: YearComponent = Field(..., title="Cooling schedule of the object")

WaterUseComponent #

Bases: NamedObject, MetadataMixin

A water use object in the SBEM library.

Source code in epinterface/sbem/components/space_use.py
228
229
230
231
232
233
234
235
236
237
238
239
class WaterUseComponent(NamedObject, MetadataMixin, extra="forbid"):
    """A water use object in the SBEM library."""

    FlowRatePerPerson: float = Field(
        ...,
        title="Flow rate per person [m3/day/p]",
        ge=0,
        le=10,
        description="This is the AVERAGE total amount of water per person per day; "
        "the peak flow rate will be computed dynamically based on the schedule.",
    )
    Schedule: YearComponent = Field(..., title="Water schedule")

ZoneSpaceUseComponent #

Bases: NamedObject, MetadataMixin

Space use object.

Source code in epinterface/sbem/components/space_use.py
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
class ZoneSpaceUseComponent(NamedObject, MetadataMixin, extra="forbid"):
    """Space use object."""

    Occupancy: OccupancyComponent
    Lighting: LightingComponent
    Equipment: EquipmentComponent
    Thermostat: ThermostatComponent
    WaterUse: WaterUseComponent

    # TODO: Is this really necessary, or should it be lifted up to the ZoneComponent?
    # it currently is acting as a very bare bones passthrough.
    def add_loads_to_idf_zone(self, idf: IDF, target_zone_name: str) -> IDF:
        """Add the loads to an IDF zone.

        This will add the people, equipment, and lights to the zone.

        Args:
            idf (IDF): The IDF object to add the loads to.
            target_zone_name (str): The name of the zone to add the loads to.

        Returns:
            IDF: The updated IDF object.
        """
        idf = self.Lighting.add_lights_to_idf_zone(idf, target_zone_name)
        idf = self.Occupancy.add_people_to_idf_zone(idf, target_zone_name)
        idf = self.Equipment.add_equipment_to_idf_zone(idf, target_zone_name)
        # idf = self.Thermostat.add_thermostat_to_idf_zone(idf, target_zone_name)
        # raise NotImplementedError
        return idf

add_loads_to_idf_zone(idf, target_zone_name) #

Add the loads to an IDF zone.

This will add the people, equipment, and lights to the zone.

Parameters:

Name Type Description Default
idf IDF

The IDF object to add the loads to.

required
target_zone_name str

The name of the zone to add the loads to.

required

Returns:

Name Type Description
IDF IDF

The updated IDF object.

Source code in epinterface/sbem/components/space_use.py
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
def add_loads_to_idf_zone(self, idf: IDF, target_zone_name: str) -> IDF:
    """Add the loads to an IDF zone.

    This will add the people, equipment, and lights to the zone.

    Args:
        idf (IDF): The IDF object to add the loads to.
        target_zone_name (str): The name of the zone to add the loads to.

    Returns:
        IDF: The updated IDF object.
    """
    idf = self.Lighting.add_lights_to_idf_zone(idf, target_zone_name)
    idf = self.Occupancy.add_people_to_idf_zone(idf, target_zone_name)
    idf = self.Equipment.add_equipment_to_idf_zone(idf, target_zone_name)
    # idf = self.Thermostat.add_thermostat_to_idf_zone(idf, target_zone_name)
    # raise NotImplementedError
    return idf

Systems#

Systems components for the SBEM library.

ConditioningSystemsComponent #

Bases: NamedObject, MetadataMixin

A conditioning system object in the SBEM library.

Source code in epinterface/sbem/components/systems.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
class ConditioningSystemsComponent(NamedObject, MetadataMixin, extra="forbid"):
    """A conditioning system object in the SBEM library."""

    Heating: ThermalSystemComponent | None
    Cooling: ThermalSystemComponent | None

    @model_validator(mode="after")
    def validate_conditioning_types(self):
        """Validate that the conditioning types are correct.

        Cannot have a heating system assigned to a cooling system and vice versa.
        """
        if self.Heating and "heating" not in self.Heating.ConditioningType.lower():
            msg = "Heating system type is only applicable to heating systems."
            raise ValueError(msg)
        if self.Cooling and "cooling" not in self.Cooling.ConditioningType.lower():
            msg = "Cooling system type is only applicable to cooling systems."
            raise ValueError(msg)

        return self

validate_conditioning_types() #

Validate that the conditioning types are correct.

Cannot have a heating system assigned to a cooling system and vice versa.

Source code in epinterface/sbem/components/systems.py
107
108
109
110
111
112
113
114
115
116
117
118
119
120
@model_validator(mode="after")
def validate_conditioning_types(self):
    """Validate that the conditioning types are correct.

    Cannot have a heating system assigned to a cooling system and vice versa.
    """
    if self.Heating and "heating" not in self.Heating.ConditioningType.lower():
        msg = "Heating system type is only applicable to heating systems."
        raise ValueError(msg)
    if self.Cooling and "cooling" not in self.Cooling.ConditioningType.lower():
        msg = "Cooling system type is only applicable to cooling systems."
        raise ValueError(msg)

    return self

DHWComponent #

Bases: NamedObject, MetadataMixin

Domestic Hot Water object.

Source code in epinterface/sbem/components/systems.py
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
class DHWComponent(
    NamedObject,
    MetadataMixin,
    extra="forbid",
):
    """Domestic Hot Water object."""

    SystemCOP: float = Field(
        ...,
        title="Domestic hot water coefficient of performance",
        ge=0,
    )
    WaterTemperatureInlet: float = Field(
        ...,
        title="Water temperature inlet [°C]",
        ge=0,
        le=100,
    )  # TODO:remove this or just set as constant. Leaving for flexibility here

    DistributionCOP: float = Field(
        ...,
        title="Distribution coefficient of performance",
        ge=0,
        le=1,
    )

    WaterSupplyTemperature: float = Field(
        ...,
        title="Water supply temperature [°C]",
        ge=0,
        le=100,
    )
    IsOn: BoolStr = Field(..., title="Is on")
    FuelType: DHWFuelType = Field(..., title="Hot water fuel type")

    @model_validator(mode="after")
    def validate_supply_greater_than_inlet(self):
        """Validate that the supply temperature is greater than the inlet temperature."""
        if self.WaterSupplyTemperature <= self.WaterTemperatureInlet:
            msg = "Water supply temperature must be greater than the inlet temperature."
            raise ValueError(msg)
        return self

    @property
    def effective_system_cop(self) -> float:
        """Compute the effective system COP based on the system and distribution COPs.

        Returns:
            cop (float): The effective system COP.
        """
        return self.SystemCOP * self.DistributionCOP

effective_system_cop property #

Compute the effective system COP based on the system and distribution COPs.

Returns:

Name Type Description
cop float

The effective system COP.

validate_supply_greater_than_inlet() #

Validate that the supply temperature is greater than the inlet temperature.

Source code in epinterface/sbem/components/systems.py
255
256
257
258
259
260
261
@model_validator(mode="after")
def validate_supply_greater_than_inlet(self):
    """Validate that the supply temperature is greater than the inlet temperature."""
    if self.WaterSupplyTemperature <= self.WaterTemperatureInlet:
        msg = "Water supply temperature must be greater than the inlet temperature."
        raise ValueError(msg)
    return self

ThermalSystemComponent #

Bases: NamedObject, MetadataMixin

A thermal system object in the SBEM library.

Source code in epinterface/sbem/components/systems.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
class ThermalSystemComponent(NamedObject, MetadataMixin, extra="forbid"):
    """A thermal system object in the SBEM library."""

    ConditioningType: Literal["Heating", "Cooling", "HeatingAndCooling"]
    Fuel: FuelType
    SystemCOP: float = Field(
        ...,
        title="System COP",
        ge=0,
    )
    DistributionCOP: float = Field(..., title="Distribution COP", ge=0)

    @property
    def effective_system_cop(self) -> float:
        """Compute the effective system COP based on the system and distribution COPs.

        Returns:
            cop (float): The effective system COP.
        """
        return self.SystemCOP * self.DistributionCOP

    @property
    def HeatingSystemType(self) -> HeatingSystemType:
        """Compute the heating system type based on the system COP.

        Returns:
            heating_system_type (HeatingSystemType): The heating system type.
        """
        if (
            self.ConditioningType != "Heating"
            and self.ConditioningType != "HeatingAndCooling"
        ):
            msg = "Heating system type is only applicable to heating systems."
            raise ValueError(msg)
        # TODO: compute based off of CoP
        msg = "Heating system type is not implemented."
        raise NotImplementedError(msg)
        return "ElectricResistance"

    @property
    def CoolingSystemType(self) -> CoolingSystemType:
        """Compute the cooling system type based on the system COP.

        Returns:
            cooling_system_type (CoolingSystemType): The cooling system type.
        """
        if (
            self.ConditioningType != "Cooling"
            and self.ConditioningType != "HeatingAndCooling"
        ):
            msg = "Cooling system type is only applicable to cooling systems."
            raise ValueError(msg)
        # TODO: compute based off of CoP
        msg = "Cooling system type is not implemented."
        raise NotImplementedError(msg)
        return "DX"

    @property
    def DistributionType(self) -> DistributionType:
        """Compute the distribution type based on the system COP.

        Returns:
            distribution_type (DistributionType): The distribution type.
        """
        # TODO: compute based off of CoP
        msg = "Distribution type is not implemented."
        raise NotImplementedError(msg)
        return "Hydronic"

CoolingSystemType property #

Compute the cooling system type based on the system COP.

Returns:

Name Type Description
cooling_system_type CoolingSystemType

The cooling system type.

DistributionType property #

Compute the distribution type based on the system COP.

Returns:

Name Type Description
distribution_type DistributionType

The distribution type.

HeatingSystemType property #

Compute the heating system type based on the system COP.

Returns:

Name Type Description
heating_system_type HeatingSystemType

The heating system type.

effective_system_cop property #

Compute the effective system COP based on the system and distribution COPs.

Returns:

Name Type Description
cop float

The effective system COP.

VentilationComponent #

Bases: NamedObject, MetadataMixin

A ventilation object in the SBEM library.

Source code in epinterface/sbem/components/systems.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
class VentilationComponent(NamedObject, MetadataMixin, extra="forbid"):
    """A ventilation object in the SBEM library."""

    # TODO: add unit notes in field descriptions
    FreshAirPerFloorArea: float = Field(
        ...,
        title="Fresh air per m2 of the object [m³/s/m²]",
        ge=0,
        le=0.05,
    )
    FreshAirPerPerson: float = Field(
        ...,
        title="Fresh air per person of the object [m³/s/p]",
        ge=0,
        le=0.05,
    )
    Schedule: YearComponent = Field(..., title="Ventilation schedule of the object")
    Provider: VentilationProvider = Field(..., title="Type of the object")
    HRV: HRVMethod = Field(..., title="HRV type of the object")
    Economizer: EconomizerMethod = Field(..., title="Economizer type of the object")
    DCV: DCVMethod = Field(..., title="DCV type of the object")

    @model_validator(mode="after")
    def validate_ventilation_systems(self):
        """Validate that the ventilation systems are correct.

        If the ventilation type is natural, then the zone cannot have variants of mechanical ventilation systems (e.g, HRV, DCV, Economizer).
        """
        if self.Provider == "Natural":
            if self.HRV != "NoHRV":
                msg = "Natural ventilation systems can't have HRV."
                raise ValueError(msg)
            if self.DCV != "NoDCV":
                msg = "Natural ventilation systems can't have DCV."
                raise ValueError(msg)
            if self.Economizer != "NoEconomizer":
                msg = "Natural ventilation systems can't have an Economizer."
                raise ValueError(msg)
        if self.Provider == "None":
            if self.HRV != "NoHRV":
                msg = "None ventilation systems can't have HRV."
                raise ValueError(msg)
            if self.DCV != "NoDCV":
                msg = "None ventilation systems can't have DCV."
                raise ValueError(msg)
            if self.Economizer != "NoEconomizer":
                msg = "None ventilation systems can't have an Economizer."
                raise ValueError(msg)
        return self

validate_ventilation_systems() #

Validate that the ventilation systems are correct.

If the ventilation type is natural, then the zone cannot have variants of mechanical ventilation systems (e.g, HRV, DCV, Economizer).

Source code in epinterface/sbem/components/systems.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
@model_validator(mode="after")
def validate_ventilation_systems(self):
    """Validate that the ventilation systems are correct.

    If the ventilation type is natural, then the zone cannot have variants of mechanical ventilation systems (e.g, HRV, DCV, Economizer).
    """
    if self.Provider == "Natural":
        if self.HRV != "NoHRV":
            msg = "Natural ventilation systems can't have HRV."
            raise ValueError(msg)
        if self.DCV != "NoDCV":
            msg = "Natural ventilation systems can't have DCV."
            raise ValueError(msg)
        if self.Economizer != "NoEconomizer":
            msg = "Natural ventilation systems can't have an Economizer."
            raise ValueError(msg)
    if self.Provider == "None":
        if self.HRV != "NoHRV":
            msg = "None ventilation systems can't have HRV."
            raise ValueError(msg)
        if self.DCV != "NoDCV":
            msg = "None ventilation systems can't have DCV."
            raise ValueError(msg)
        if self.Economizer != "NoEconomizer":
            msg = "None ventilation systems can't have an Economizer."
            raise ValueError(msg)
    return self

ZoneHVACComponent #

Bases: NamedObject, MetadataMixin

Conditioning object in the SBEM library.

Source code in epinterface/sbem/components/systems.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
class ZoneHVACComponent(
    NamedObject,
    MetadataMixin,
    extra="forbid",
):
    """Conditioning object in the SBEM library."""

    ConditioningSystems: ConditioningSystemsComponent
    Ventilation: VentilationComponent
    # TODO: change the structure like ZoneSpaceUse
    """Zone conditioning object."""

    def add_conditioning_to_idf_zone(self, idf: IDF, target_zone_name: str) -> IDF:
        """Add conditioning to an IDF zone.

        This constructs HVAC template objects which get assigned to the zone.

        NB: currently, many of the climate studio parameters are ignored -
        particularly the ones related to humidity control.

        Args:
            idf (IDF): The IDF object to add the conditioning to.
            target_zone_name (str): The name of the zone to add the conditioning to.

        Returns:
            IDF: The updated IDF object.
        """
        # TODO: add the idf conversion functions

        return idf

Ventilation instance-attribute #

Zone conditioning object.

add_conditioning_to_idf_zone(idf, target_zone_name) #

Add conditioning to an IDF zone.

This constructs HVAC template objects which get assigned to the zone.

NB: currently, many of the climate studio parameters are ignored - particularly the ones related to humidity control.

Parameters:

Name Type Description Default
idf IDF

The IDF object to add the conditioning to.

required
target_zone_name str

The name of the zone to add the conditioning to.

required

Returns:

Name Type Description
IDF IDF

The updated IDF object.

Source code in epinterface/sbem/components/systems.py
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
def add_conditioning_to_idf_zone(self, idf: IDF, target_zone_name: str) -> IDF:
    """Add conditioning to an IDF zone.

    This constructs HVAC template objects which get assigned to the zone.

    NB: currently, many of the climate studio parameters are ignored -
    particularly the ones related to humidity control.

    Args:
        idf (IDF): The IDF object to add the conditioning to.
        target_zone_name (str): The name of the zone to add the conditioning to.

    Returns:
        IDF: The updated IDF object.
    """
    # TODO: add the idf conversion functions

    return idf

Operations#

Operations components for the SBEM library.

ZoneOperationsComponent #

Bases: NamedObject, MetadataMixin

Zone use consolidation across space use, HVAC, DHW.

Source code in epinterface/sbem/components/operations.py
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
class ZoneOperationsComponent(
    NamedObject,
    MetadataMixin,
    extra="forbid",
):
    """Zone use consolidation across space use, HVAC, DHW."""

    SpaceUse: ZoneSpaceUseComponent
    HVAC: ZoneHVACComponent
    DHW: DHWComponent

    def add_water_use_to_idf_zone(self, idf: IDF, target_zone_name: str) -> IDF:
        """Handle adding water use to the zone based on both DHW and Operations.SpaceUse.WaterUse.

        We choose to execute this from the zone component interface because it requires context from both
        zone.Operations.SpaceUse.WaterUse and zone.Operations.

        Note: the water use component's flow rate represents the total amount of water used per person per day;
        in order to calculate a peak flow rate, we will need to ensure that the product of the schedule and the peak
        flow rate would resolve to the same daily average value per person.

        This will be responsible for:
            1. Extracting the total area of the zone by finding the matching surfaces.
            2. Computing the total number of people in the zone based on the occupancy density and the total area.
            3. Computing the average flow rate of water use/s in the zone based on the flow rate per person/day and the total number of people.
            4. Computing the peak flow rate of water use/s in the zone based on the average flow rate and the average fractional value of the schedule.
            5. Adding the water use equipment to the zone.


        # TODO: major problem - this assumes the zone has already been correctly sized!
        # this method should only actually be called AFTER
        # the building has been properly rescaled, which currently is executed at the very end.
        # that hook should probably be moved up.

        Args:
            idf (IDF): The IDF object to add the operations to.
            target_zone_name (str): The name of the zone to add the operations to.

        Returns:
            idf (IDF): The updated IDF object.
        """
        # TODO: should IsOn definitely live in DHW? should dhw be nullable?
        # should it live in spaceuse.wateruse for consistency?
        if not self.DHW.IsOn:
            return idf

        # Acquire the relevant data fields
        flow_rate_per_person_per_day = (
            self.SpaceUse.WaterUse.FlowRatePerPerson
        )  # m3/person/day
        occupant_density = self.SpaceUse.Occupancy.PeopleDensity  # ppl/m2
        water_supply_temperature = self.DHW.WaterSupplyTemperature  # degC
        water_temperature_inlet = self.DHW.WaterTemperatureInlet  # degC
        water_use_frac_sched = self.SpaceUse.WaterUse.Schedule
        water_use_name = f"{target_zone_name}_{self.SpaceUse.WaterUse.safe_name}_{self.DHW.safe_name}_WATER"

        # determine zone area to then compute total people
        # we will do this by finding all matching surfaces and computing their areas.
        zone = next(
            (x for x in idf.idfobjects["ZONE"] if x.Name == target_zone_name), None
        )
        if zone is None:
            raise ValueError(f"NO_ZONE:{target_zone_name}")
        area = get_zone_floor_area(idf, zone.Name)
        total_ppl = occupant_density * area

        # Compute final flow rates.
        total_flow_per_day = flow_rate_per_person_per_day * total_ppl  # m3/day
        avg_flow_per_s = total_flow_per_day / (3600 * 24)  # m3/s
        # TODO: Update this rather than being constant rate

        lim = "Temperature"
        if not idf.getobject("SCHEDULETYPELIMITS", lim):
            lim = ScheduleTypeLimits(
                Name="Temperature",
                LowerLimit=-60,
                UpperLimit=200,
            )
            lim.to_epbunch(idf)

        # TODO: should we be using time-varying temperatures?
        target_temperature_schedule = Schedule.constant_schedule(
            value=water_supply_temperature,  # pyright: ignore [reportArgumentType]
            Name=f"{water_use_name}_TargetWaterTemperatureSch",
            Type="Temperature",
        )
        inlet_temperature_schedule = Schedule.constant_schedule(
            value=water_temperature_inlet,  # pyright: ignore [reportArgumentType]
            Name=f"{water_use_name}_InletWaterTemperatureSch",
            Type="Temperature",
        )

        target_temperature_yr_schedule, *_ = (
            target_temperature_schedule.to_year_week_day()
        )
        inlet_temperature_yr_schedule, *_ = (
            inlet_temperature_schedule.to_year_week_day()
        )

        target_temperature_yr_schedule.to_epbunch(idf)
        inlet_temperature_yr_schedule.to_epbunch(idf)

        if water_use_frac_sched.schedule_type_limits != "Fraction":
            msg = f"WATER_USE_FRACTION_SCHEDULE_TYPE_NOT_FRACTION:{water_use_name}"
            raise ValueError(msg)

        idf, water_use_frac_sch_name = water_use_frac_sched.add_year_to_idf(
            idf, name_prefix=None
        )
        # sch_obj = idf.getobject("SCHEDULE:YEAR", water_use_frac_sch_name)
        # arch_sch = Schedule.from_epbunch(sch_obj)
        # values = np.array(arch_sch.Values)
        # avg_fractional_value = np.sum(values) / 8760
        logger.warning("USING HARDCODED 2018 to compute avg fractional value")
        avg_fractional_value = water_use_frac_sched.fractional_year_sum(2018)
        # total_fractional_value * peak_flow_rate_per_s = avg_float_per_s
        peak_flow_rate_per_s = avg_flow_per_s / avg_fractional_value

        hot_water = WaterUseEquipment(
            Name=water_use_name,
            EndUse_Subcategory="Domestic Hot Water",
            Peak_Flow_Rate=peak_flow_rate_per_s,
            Flow_Rate_Fraction_Schedule_Name=water_use_frac_sch_name,
            Zone_Name=target_zone_name,
            Target_Temperature_Schedule_Name=target_temperature_yr_schedule.Name,
            Hot_Water_Supply_Temperature_Schedule_Name=target_temperature_schedule.Name,
            Cold_Water_Supply_Temperature_Schedule_Name=inlet_temperature_schedule.Name,
            Sensible_Fraction_Schedule_Name=None,
            Latent_Fraction_Schedule_Name=None,
        )
        idf = hot_water.add(idf)
        return idf

    def add_thermostat_to_idf_zone(
        self, idf: IDF, target_zone_name: str
    ) -> HVACTemplateThermostat:
        """Add a thermostat to the zone.

        Args:
            idf (IDF): The IDF object to add the thermostat to.
            target_zone_name (str): The name of the zone to add the thermostat to.

        Returns:
            idf (IDF): The updated IDF object.
        """
        thermostat_name = (
            f"{target_zone_name}_{self.SpaceUse.Thermostat.safe_name}_THERMOSTAT"
        )
        heating_schedule = self.SpaceUse.Thermostat.HeatingSchedule
        cooling_schedule = self.SpaceUse.Thermostat.CoolingSchedule

        heating_schedule_name = None
        cooling_schedule_name = None
        if heating_schedule is not None:
            _, heat_high = self.SpaceUse.Thermostat.HeatingSchedule.bounds
            heating_design_day = ScheduleDayHourly(
                Name=f"{thermostat_name}_HeatingDesignDay",
                Schedule_Type_Limits_Name="Temperature",
                Hour_1=heat_high,
                Hour_2=heat_high,
                Hour_3=heat_high,
                Hour_4=heat_high,
                Hour_5=heat_high,
                Hour_6=heat_high,
                Hour_7=heat_high,
                Hour_8=heat_high,
                Hour_9=heat_high,
                Hour_10=heat_high,
                Hour_11=heat_high,
                Hour_12=heat_high,
                Hour_13=heat_high,
                Hour_14=heat_high,
                Hour_15=heat_high,
                Hour_16=heat_high,
                Hour_17=heat_high,
                Hour_18=heat_high,
                Hour_19=heat_high,
                Hour_20=heat_high,
                Hour_21=heat_high,
                Hour_22=heat_high,
                Hour_23=heat_high,
                Hour_24=heat_high,
            )
            idf = heating_design_day.add(idf)
            idf, heating_schedule_name = heating_schedule.add_year_to_idf(
                idf,
                name_prefix=None,
                winter_design_day_sch_name=heating_design_day.Name,
                summer_design_day_sch_name=heating_design_day.Name,
            )
        if cooling_schedule is not None:
            cool_low, _ = self.SpaceUse.Thermostat.CoolingSchedule.bounds
            cooling_design_day = ScheduleDayHourly(
                Name=f"{thermostat_name}_CoolingDesignDay",
                Schedule_Type_Limits_Name="Temperature",
                Hour_1=cool_low,
                Hour_2=cool_low,
                Hour_3=cool_low,
                Hour_4=cool_low,
                Hour_5=cool_low,
                Hour_6=cool_low,
                Hour_7=cool_low,
                Hour_8=cool_low,
                Hour_9=cool_low,
                Hour_10=cool_low,
                Hour_11=cool_low,
                Hour_12=cool_low,
                Hour_13=cool_low,
                Hour_14=cool_low,
                Hour_15=cool_low,
                Hour_16=cool_low,
                Hour_17=cool_low,
                Hour_18=cool_low,
                Hour_19=cool_low,
                Hour_20=cool_low,
                Hour_21=cool_low,
                Hour_22=cool_low,
                Hour_23=cool_low,
                Hour_24=cool_low,
            )

            idf = cooling_design_day.add(idf)
            idf, cooling_schedule_name = cooling_schedule.add_year_to_idf(
                idf,
                name_prefix=None,
                winter_design_day_sch_name=cooling_design_day.Name,
                summer_design_day_sch_name=cooling_design_day.Name,
            )

        thermostat = HVACTemplateThermostat(
            Name=thermostat_name,
            Heating_Setpoint_Schedule_Name=heating_schedule_name,
            Constant_Heating_Setpoint=self.SpaceUse.Thermostat.HeatingSetpoint
            if self.SpaceUse.Thermostat.HeatingSchedule is None
            else None,
            Cooling_Setpoint_Schedule_Name=cooling_schedule_name,
            Constant_Cooling_Setpoint=self.SpaceUse.Thermostat.CoolingSetpoint
            if self.SpaceUse.Thermostat.CoolingSchedule is None
            else None,
        )

        idf = thermostat.add(idf)

        return thermostat

    def add_conditioning_to_idf_zone(self, idf: IDF, target_zone_name: str) -> IDF:
        """Add conditioning to an IDF zone."""
        thermostat = self.add_thermostat_to_idf_zone(idf, target_zone_name)
        if self.HVAC.Ventilation.DCV != "NoDCV":
            # check the design spec outdoor air for the DCV
            raise NotImplementedError("DCV not implemented.")
        hvac_template = HVACTemplateZoneIdealLoadsAirSystem(
            Zone_Name=target_zone_name,
            Template_Thermostat_Name=thermostat.Name,
            Heating_Availability_Schedule_Name=None
            if self.HVAC.ConditioningSystems.Heating
            else "AlwaysOff",
            Cooling_Availability_Schedule_Name=None
            if self.HVAC.ConditioningSystems.Cooling
            else "AlwaysOff",
            Heating_Limit="LimitFlowRateAndCapacity",
            Maximum_Heating_Air_Flow_Rate="autosize",
            Maximum_Sensible_Heating_Capacity="autosize",
            Cooling_Limit="LimitFlowRateAndCapacity",
            Maximum_Cooling_Air_Flow_Rate="autosize",
            Maximum_Total_Cooling_Capacity="autosize",
            Minimum_Cooling_Supply_Air_Temperature=13,
            Humidification_Control_Type="None",
            Outdoor_Air_Flow_Rate_per_Person=self.HVAC.Ventilation.FreshAirPerPerson,
            Outdoor_Air_Flow_Rate_per_Zone_Floor_Area=self.HVAC.Ventilation.FreshAirPerFloorArea,
            Outdoor_Air_Flow_Rate_per_Zone=0,
            Demand_Controlled_Ventilation_Type="None"
            if self.HVAC.Ventilation.DCV == "NoDCV"
            else self.HVAC.Ventilation.DCV,
            Outdoor_Air_Economizer_Type=self.HVAC.Ventilation.Economizer,
            Heat_Recovery_Type="None"
            if self.HVAC.Ventilation.HRV == "NoHRV"
            else self.HVAC.Ventilation.HRV,
            Sensible_Heat_Recovery_Effectiveness=assumed_constants.Sensible_Heat_Recovery_Effectiveness,
            Latent_Heat_Recovery_Effectiveness=assumed_constants.Latent_Heat_Recovery_Effectiveness,
            Outdoor_Air_Method="Sum"
            if self.HVAC.Ventilation.Provider == "Mechanical"
            or self.HVAC.Ventilation.Provider == "Both"
            else "None",
        )
        idf = hvac_template.add(idf)

        if self.HVAC.Ventilation.Provider == "Natural":
            # total_window_area = calculate_window_area_for_zone(idf, target_zone_name)
            total_window_area = get_zone_glazed_area(idf, target_zone_name)

            if total_window_area == 0:
                logger.warning(
                    f"No windows found for natural ventilation in zone {target_zone_name}"
                )
                return idf
            vent_wind_stack_name = f"{target_zone_name}_{self.SpaceUse.Thermostat.safe_name}_{self.HVAC.Ventilation.safe_name}_VENTILATION_WIND_AND_STACK_OPEN_AREA"

            idf, vent_wind_stack_name_sch = (
                self.HVAC.Ventilation.Schedule.add_year_to_idf(
                    idf,
                    name_prefix=None,
                    summer_design_day_sch_name="d_AllOff_00",
                    winter_design_day_sch_name="d_AllOff_00",
                )
            )
            ventilation_wind_and_stack_open_area = ZoneVentilationWindAndStackOpenArea(
                Name=vent_wind_stack_name,
                Zone_or_Space_Name=target_zone_name,
                Minimum_Outdoor_Temperature=12,
                Height_Difference=0,
                Delta_Temperature=2,
                Maximum_Outdoor_Temperature=25,
                Opening_Area=total_window_area,
                Opening_Area_Fraction_Schedule_Name=vent_wind_stack_name_sch,
            )
            idf = ventilation_wind_and_stack_open_area.add(idf)

        return idf

add_conditioning_to_idf_zone(idf, target_zone_name) #

Add conditioning to an IDF zone.

Source code in epinterface/sbem/components/operations.py
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
def add_conditioning_to_idf_zone(self, idf: IDF, target_zone_name: str) -> IDF:
    """Add conditioning to an IDF zone."""
    thermostat = self.add_thermostat_to_idf_zone(idf, target_zone_name)
    if self.HVAC.Ventilation.DCV != "NoDCV":
        # check the design spec outdoor air for the DCV
        raise NotImplementedError("DCV not implemented.")
    hvac_template = HVACTemplateZoneIdealLoadsAirSystem(
        Zone_Name=target_zone_name,
        Template_Thermostat_Name=thermostat.Name,
        Heating_Availability_Schedule_Name=None
        if self.HVAC.ConditioningSystems.Heating
        else "AlwaysOff",
        Cooling_Availability_Schedule_Name=None
        if self.HVAC.ConditioningSystems.Cooling
        else "AlwaysOff",
        Heating_Limit="LimitFlowRateAndCapacity",
        Maximum_Heating_Air_Flow_Rate="autosize",
        Maximum_Sensible_Heating_Capacity="autosize",
        Cooling_Limit="LimitFlowRateAndCapacity",
        Maximum_Cooling_Air_Flow_Rate="autosize",
        Maximum_Total_Cooling_Capacity="autosize",
        Minimum_Cooling_Supply_Air_Temperature=13,
        Humidification_Control_Type="None",
        Outdoor_Air_Flow_Rate_per_Person=self.HVAC.Ventilation.FreshAirPerPerson,
        Outdoor_Air_Flow_Rate_per_Zone_Floor_Area=self.HVAC.Ventilation.FreshAirPerFloorArea,
        Outdoor_Air_Flow_Rate_per_Zone=0,
        Demand_Controlled_Ventilation_Type="None"
        if self.HVAC.Ventilation.DCV == "NoDCV"
        else self.HVAC.Ventilation.DCV,
        Outdoor_Air_Economizer_Type=self.HVAC.Ventilation.Economizer,
        Heat_Recovery_Type="None"
        if self.HVAC.Ventilation.HRV == "NoHRV"
        else self.HVAC.Ventilation.HRV,
        Sensible_Heat_Recovery_Effectiveness=assumed_constants.Sensible_Heat_Recovery_Effectiveness,
        Latent_Heat_Recovery_Effectiveness=assumed_constants.Latent_Heat_Recovery_Effectiveness,
        Outdoor_Air_Method="Sum"
        if self.HVAC.Ventilation.Provider == "Mechanical"
        or self.HVAC.Ventilation.Provider == "Both"
        else "None",
    )
    idf = hvac_template.add(idf)

    if self.HVAC.Ventilation.Provider == "Natural":
        # total_window_area = calculate_window_area_for_zone(idf, target_zone_name)
        total_window_area = get_zone_glazed_area(idf, target_zone_name)

        if total_window_area == 0:
            logger.warning(
                f"No windows found for natural ventilation in zone {target_zone_name}"
            )
            return idf
        vent_wind_stack_name = f"{target_zone_name}_{self.SpaceUse.Thermostat.safe_name}_{self.HVAC.Ventilation.safe_name}_VENTILATION_WIND_AND_STACK_OPEN_AREA"

        idf, vent_wind_stack_name_sch = (
            self.HVAC.Ventilation.Schedule.add_year_to_idf(
                idf,
                name_prefix=None,
                summer_design_day_sch_name="d_AllOff_00",
                winter_design_day_sch_name="d_AllOff_00",
            )
        )
        ventilation_wind_and_stack_open_area = ZoneVentilationWindAndStackOpenArea(
            Name=vent_wind_stack_name,
            Zone_or_Space_Name=target_zone_name,
            Minimum_Outdoor_Temperature=12,
            Height_Difference=0,
            Delta_Temperature=2,
            Maximum_Outdoor_Temperature=25,
            Opening_Area=total_window_area,
            Opening_Area_Fraction_Schedule_Name=vent_wind_stack_name_sch,
        )
        idf = ventilation_wind_and_stack_open_area.add(idf)

    return idf

add_thermostat_to_idf_zone(idf, target_zone_name) #

Add a thermostat to the zone.

Parameters:

Name Type Description Default
idf IDF

The IDF object to add the thermostat to.

required
target_zone_name str

The name of the zone to add the thermostat to.

required

Returns:

Name Type Description
idf IDF

The updated IDF object.

Source code in epinterface/sbem/components/operations.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
def add_thermostat_to_idf_zone(
    self, idf: IDF, target_zone_name: str
) -> HVACTemplateThermostat:
    """Add a thermostat to the zone.

    Args:
        idf (IDF): The IDF object to add the thermostat to.
        target_zone_name (str): The name of the zone to add the thermostat to.

    Returns:
        idf (IDF): The updated IDF object.
    """
    thermostat_name = (
        f"{target_zone_name}_{self.SpaceUse.Thermostat.safe_name}_THERMOSTAT"
    )
    heating_schedule = self.SpaceUse.Thermostat.HeatingSchedule
    cooling_schedule = self.SpaceUse.Thermostat.CoolingSchedule

    heating_schedule_name = None
    cooling_schedule_name = None
    if heating_schedule is not None:
        _, heat_high = self.SpaceUse.Thermostat.HeatingSchedule.bounds
        heating_design_day = ScheduleDayHourly(
            Name=f"{thermostat_name}_HeatingDesignDay",
            Schedule_Type_Limits_Name="Temperature",
            Hour_1=heat_high,
            Hour_2=heat_high,
            Hour_3=heat_high,
            Hour_4=heat_high,
            Hour_5=heat_high,
            Hour_6=heat_high,
            Hour_7=heat_high,
            Hour_8=heat_high,
            Hour_9=heat_high,
            Hour_10=heat_high,
            Hour_11=heat_high,
            Hour_12=heat_high,
            Hour_13=heat_high,
            Hour_14=heat_high,
            Hour_15=heat_high,
            Hour_16=heat_high,
            Hour_17=heat_high,
            Hour_18=heat_high,
            Hour_19=heat_high,
            Hour_20=heat_high,
            Hour_21=heat_high,
            Hour_22=heat_high,
            Hour_23=heat_high,
            Hour_24=heat_high,
        )
        idf = heating_design_day.add(idf)
        idf, heating_schedule_name = heating_schedule.add_year_to_idf(
            idf,
            name_prefix=None,
            winter_design_day_sch_name=heating_design_day.Name,
            summer_design_day_sch_name=heating_design_day.Name,
        )
    if cooling_schedule is not None:
        cool_low, _ = self.SpaceUse.Thermostat.CoolingSchedule.bounds
        cooling_design_day = ScheduleDayHourly(
            Name=f"{thermostat_name}_CoolingDesignDay",
            Schedule_Type_Limits_Name="Temperature",
            Hour_1=cool_low,
            Hour_2=cool_low,
            Hour_3=cool_low,
            Hour_4=cool_low,
            Hour_5=cool_low,
            Hour_6=cool_low,
            Hour_7=cool_low,
            Hour_8=cool_low,
            Hour_9=cool_low,
            Hour_10=cool_low,
            Hour_11=cool_low,
            Hour_12=cool_low,
            Hour_13=cool_low,
            Hour_14=cool_low,
            Hour_15=cool_low,
            Hour_16=cool_low,
            Hour_17=cool_low,
            Hour_18=cool_low,
            Hour_19=cool_low,
            Hour_20=cool_low,
            Hour_21=cool_low,
            Hour_22=cool_low,
            Hour_23=cool_low,
            Hour_24=cool_low,
        )

        idf = cooling_design_day.add(idf)
        idf, cooling_schedule_name = cooling_schedule.add_year_to_idf(
            idf,
            name_prefix=None,
            winter_design_day_sch_name=cooling_design_day.Name,
            summer_design_day_sch_name=cooling_design_day.Name,
        )

    thermostat = HVACTemplateThermostat(
        Name=thermostat_name,
        Heating_Setpoint_Schedule_Name=heating_schedule_name,
        Constant_Heating_Setpoint=self.SpaceUse.Thermostat.HeatingSetpoint
        if self.SpaceUse.Thermostat.HeatingSchedule is None
        else None,
        Cooling_Setpoint_Schedule_Name=cooling_schedule_name,
        Constant_Cooling_Setpoint=self.SpaceUse.Thermostat.CoolingSetpoint
        if self.SpaceUse.Thermostat.CoolingSchedule is None
        else None,
    )

    idf = thermostat.add(idf)

    return thermostat

add_water_use_to_idf_zone(idf, target_zone_name) #

Handle adding water use to the zone based on both DHW and Operations.SpaceUse.WaterUse.

We choose to execute this from the zone component interface because it requires context from both zone.Operations.SpaceUse.WaterUse and zone.Operations.

Note: the water use component's flow rate represents the total amount of water used per person per day; in order to calculate a peak flow rate, we will need to ensure that the product of the schedule and the peak flow rate would resolve to the same daily average value per person.

This will be responsible for
  1. Extracting the total area of the zone by finding the matching surfaces.
  2. Computing the total number of people in the zone based on the occupancy density and the total area.
  3. Computing the average flow rate of water use/s in the zone based on the flow rate per person/day and the total number of people.
  4. Computing the peak flow rate of water use/s in the zone based on the average flow rate and the average fractional value of the schedule.
  5. Adding the water use equipment to the zone.

TODO: major problem - this assumes the zone has already been correctly sized!#

this method should only actually be called AFTER#

the building has been properly rescaled, which currently is executed at the very end.#

that hook should probably be moved up.#

Parameters:

Name Type Description Default
idf IDF

The IDF object to add the operations to.

required
target_zone_name str

The name of the zone to add the operations to.

required

Returns:

Name Type Description
idf IDF

The updated IDF object.

Source code in epinterface/sbem/components/operations.py
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
def add_water_use_to_idf_zone(self, idf: IDF, target_zone_name: str) -> IDF:
    """Handle adding water use to the zone based on both DHW and Operations.SpaceUse.WaterUse.

    We choose to execute this from the zone component interface because it requires context from both
    zone.Operations.SpaceUse.WaterUse and zone.Operations.

    Note: the water use component's flow rate represents the total amount of water used per person per day;
    in order to calculate a peak flow rate, we will need to ensure that the product of the schedule and the peak
    flow rate would resolve to the same daily average value per person.

    This will be responsible for:
        1. Extracting the total area of the zone by finding the matching surfaces.
        2. Computing the total number of people in the zone based on the occupancy density and the total area.
        3. Computing the average flow rate of water use/s in the zone based on the flow rate per person/day and the total number of people.
        4. Computing the peak flow rate of water use/s in the zone based on the average flow rate and the average fractional value of the schedule.
        5. Adding the water use equipment to the zone.


    # TODO: major problem - this assumes the zone has already been correctly sized!
    # this method should only actually be called AFTER
    # the building has been properly rescaled, which currently is executed at the very end.
    # that hook should probably be moved up.

    Args:
        idf (IDF): The IDF object to add the operations to.
        target_zone_name (str): The name of the zone to add the operations to.

    Returns:
        idf (IDF): The updated IDF object.
    """
    # TODO: should IsOn definitely live in DHW? should dhw be nullable?
    # should it live in spaceuse.wateruse for consistency?
    if not self.DHW.IsOn:
        return idf

    # Acquire the relevant data fields
    flow_rate_per_person_per_day = (
        self.SpaceUse.WaterUse.FlowRatePerPerson
    )  # m3/person/day
    occupant_density = self.SpaceUse.Occupancy.PeopleDensity  # ppl/m2
    water_supply_temperature = self.DHW.WaterSupplyTemperature  # degC
    water_temperature_inlet = self.DHW.WaterTemperatureInlet  # degC
    water_use_frac_sched = self.SpaceUse.WaterUse.Schedule
    water_use_name = f"{target_zone_name}_{self.SpaceUse.WaterUse.safe_name}_{self.DHW.safe_name}_WATER"

    # determine zone area to then compute total people
    # we will do this by finding all matching surfaces and computing their areas.
    zone = next(
        (x for x in idf.idfobjects["ZONE"] if x.Name == target_zone_name), None
    )
    if zone is None:
        raise ValueError(f"NO_ZONE:{target_zone_name}")
    area = get_zone_floor_area(idf, zone.Name)
    total_ppl = occupant_density * area

    # Compute final flow rates.
    total_flow_per_day = flow_rate_per_person_per_day * total_ppl  # m3/day
    avg_flow_per_s = total_flow_per_day / (3600 * 24)  # m3/s
    # TODO: Update this rather than being constant rate

    lim = "Temperature"
    if not idf.getobject("SCHEDULETYPELIMITS", lim):
        lim = ScheduleTypeLimits(
            Name="Temperature",
            LowerLimit=-60,
            UpperLimit=200,
        )
        lim.to_epbunch(idf)

    # TODO: should we be using time-varying temperatures?
    target_temperature_schedule = Schedule.constant_schedule(
        value=water_supply_temperature,  # pyright: ignore [reportArgumentType]
        Name=f"{water_use_name}_TargetWaterTemperatureSch",
        Type="Temperature",
    )
    inlet_temperature_schedule = Schedule.constant_schedule(
        value=water_temperature_inlet,  # pyright: ignore [reportArgumentType]
        Name=f"{water_use_name}_InletWaterTemperatureSch",
        Type="Temperature",
    )

    target_temperature_yr_schedule, *_ = (
        target_temperature_schedule.to_year_week_day()
    )
    inlet_temperature_yr_schedule, *_ = (
        inlet_temperature_schedule.to_year_week_day()
    )

    target_temperature_yr_schedule.to_epbunch(idf)
    inlet_temperature_yr_schedule.to_epbunch(idf)

    if water_use_frac_sched.schedule_type_limits != "Fraction":
        msg = f"WATER_USE_FRACTION_SCHEDULE_TYPE_NOT_FRACTION:{water_use_name}"
        raise ValueError(msg)

    idf, water_use_frac_sch_name = water_use_frac_sched.add_year_to_idf(
        idf, name_prefix=None
    )
    # sch_obj = idf.getobject("SCHEDULE:YEAR", water_use_frac_sch_name)
    # arch_sch = Schedule.from_epbunch(sch_obj)
    # values = np.array(arch_sch.Values)
    # avg_fractional_value = np.sum(values) / 8760
    logger.warning("USING HARDCODED 2018 to compute avg fractional value")
    avg_fractional_value = water_use_frac_sched.fractional_year_sum(2018)
    # total_fractional_value * peak_flow_rate_per_s = avg_float_per_s
    peak_flow_rate_per_s = avg_flow_per_s / avg_fractional_value

    hot_water = WaterUseEquipment(
        Name=water_use_name,
        EndUse_Subcategory="Domestic Hot Water",
        Peak_Flow_Rate=peak_flow_rate_per_s,
        Flow_Rate_Fraction_Schedule_Name=water_use_frac_sch_name,
        Zone_Name=target_zone_name,
        Target_Temperature_Schedule_Name=target_temperature_yr_schedule.Name,
        Hot_Water_Supply_Temperature_Schedule_Name=target_temperature_schedule.Name,
        Cold_Water_Supply_Temperature_Schedule_Name=inlet_temperature_schedule.Name,
        Sensible_Fraction_Schedule_Name=None,
        Latent_Fraction_Schedule_Name=None,
    )
    idf = hot_water.add(idf)
    return idf

Envelope#

Envelope components for the SBEM library.

ConstructionAssemblyComponent #

Bases: NamedObject, MetadataMixin

Opaque construction object.

Source code in epinterface/sbem/components/envelope.py
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
class ConstructionAssemblyComponent(
    NamedObject,
    MetadataMixin,
    extra="forbid",
):
    """Opaque construction object."""

    Layers: list[ConstructionLayerComponent] = Field(
        ..., title="Layers of the opaque construction"
    )
    VegetationLayer: NanStr | None = Field(
        default=None, title="Vegetation layer of the opaque construction"
    )
    Type: ConstructionComponentSurfaceType = Field(
        ..., title="Type of the opaque construction"
    )

    @field_validator("Layers", mode="after")
    def validate_layers(cls, v: list[ConstructionLayerComponent]):
        """Validate the layers of the construction."""
        if len(v) == 0:
            msg = "At least one layer is required"
            raise ValueError(msg)
        layer_orders = [layer.LayerOrder for layer in v]
        if set(layer_orders) != set(range(0, len(v))):
            msg = "Layer orders must be consecutive integers starting from 0"
            raise ValueError(msg)
        v = sorted(v, key=lambda x: x.LayerOrder)
        total_thickness = sum(layer.Thickness for layer in v)
        if total_thickness > 3:
            msg = "Total construction assembly thickness must be less than 3 meters"
            raise ValueError(msg)
        return v

    def add_to_idf(self, idf: IDF) -> IDF:
        """Adds an opaque construction to an IDF object.

        Note that this will add the individual materials as well.

        Args:
            idf (IDF): The IDF object to add the construction to.

        Returns:
            idf (IDF): The updated IDF object.
        """
        layers = [layer.ep_material for layer in self.Layers]

        construction = Construction(
            name=self.Name,
            layers=layers,
        )
        idf = construction.add(idf)
        return idf

    @property
    def sorted_layers(self):
        """Return the layers of the construction sorted by layer order."""
        return sorted(self.Layers, key=lambda x: x.LayerOrder)

    @property
    def reversed(self):
        """Return a reversed version of the construction."""
        copy = self.model_copy(deep=True)
        for i, layer in enumerate(copy.sorted_layers[::-1]):
            layer.LayerOrder = i
        copy.Layers = copy.sorted_layers
        copy.Name = f"{self.Name}_Reversed"
        return copy

    @property
    def r_value(self):
        """Return the R-value of the construction in m²K/W."""
        return sum(layer.r_value for layer in self.sorted_layers)

    @property
    def u_value(self):
        """Return the U-value of the construction in W/m²K."""
        return 1 / self.r_value

r_value property #

Return the R-value of the construction in m²K/W.

reversed property #

Return a reversed version of the construction.

sorted_layers property #

Return the layers of the construction sorted by layer order.

u_value property #

Return the U-value of the construction in W/m²K.

add_to_idf(idf) #

Adds an opaque construction to an IDF object.

Note that this will add the individual materials as well.

Parameters:

Name Type Description Default
idf IDF

The IDF object to add the construction to.

required

Returns:

Name Type Description
idf IDF

The updated IDF object.

Source code in epinterface/sbem/components/envelope.py
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
def add_to_idf(self, idf: IDF) -> IDF:
    """Adds an opaque construction to an IDF object.

    Note that this will add the individual materials as well.

    Args:
        idf (IDF): The IDF object to add the construction to.

    Returns:
        idf (IDF): The updated IDF object.
    """
    layers = [layer.ep_material for layer in self.Layers]

    construction = Construction(
        name=self.Name,
        layers=layers,
    )
    idf = construction.add(idf)
    return idf

validate_layers(v) #

Validate the layers of the construction.

Source code in epinterface/sbem/components/envelope.py
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
@field_validator("Layers", mode="after")
def validate_layers(cls, v: list[ConstructionLayerComponent]):
    """Validate the layers of the construction."""
    if len(v) == 0:
        msg = "At least one layer is required"
        raise ValueError(msg)
    layer_orders = [layer.LayerOrder for layer in v]
    if set(layer_orders) != set(range(0, len(v))):
        msg = "Layer orders must be consecutive integers starting from 0"
        raise ValueError(msg)
    v = sorted(v, key=lambda x: x.LayerOrder)
    total_thickness = sum(layer.Thickness for layer in v)
    if total_thickness > 3:
        msg = "Total construction assembly thickness must be less than 3 meters"
        raise ValueError(msg)
    return v

ConstructionLayerComponent #

Bases: BaseModel

Layer of an opaque construction.

Source code in epinterface/sbem/components/envelope.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
class ConstructionLayerComponent(BaseModel, extra="forbid"):
    """Layer of an opaque construction."""

    Thickness: float = Field(..., title="Thickness of the layer [m]", ge=0, le=2)
    LayerOrder: int
    ConstructionMaterial: ConstructionMaterialComponent

    @property
    def name(self):
        """Return the name of the layer."""
        # TODO: do we want to consider scoping construction layer names by parent construction?
        # We currently have disabled LayerOrder in the name because it was causing
        # warnings to be rised by EP since a reversed construction would end up with
        # using different materials though with identical definitions; energyplus would
        # throw a warning but still proceed.
        # by not name scoping here by the parent construction, we end up with silent logical
        # failures in the event where a user has mutated
        # post-instantiated, decoupled ConstructionMaterial definitions with the same
        # name.
        # return f"{self.LayerOrder}_{self.ConstructionMaterial.Name}_{self.Thickness}m"
        return f"{self.ConstructionMaterial.Name}_{self.Thickness}m"

    @property
    def ep_material(self):
        """Return the EP material for the layer."""
        return Material(
            Name=self.name,
            Thickness=self.Thickness,
            Conductivity=self.ConstructionMaterial.Conductivity,
            Density=self.ConstructionMaterial.Density,
            Specific_Heat=self.ConstructionMaterial.SpecificHeat,
            Thermal_Absorptance=self.ConstructionMaterial.ThermalAbsorptance,
            Solar_Absorptance=self.ConstructionMaterial.SolarAbsorptance,
            Roughness=self.ConstructionMaterial.Roughness,
            Visible_Absorptance=self.ConstructionMaterial.VisibleAbsorptance,
        )

    @property
    def r_value(self):
        """Return the R-value of the layer in m²K/W."""
        return self.Thickness / self.ConstructionMaterial.Conductivity

    @property
    def u_value(self):
        """Return the U-value of the layer in W/m²K."""
        return 1 / self.r_value

ep_material property #

Return the EP material for the layer.

name property #

Return the name of the layer.

r_value property #

Return the R-value of the layer in m²K/W.

u_value property #

Return the U-value of the layer in W/m²K.

EnvelopeAssemblyComponent #

Bases: NamedObject, MetadataMixin

Zone construction object.

Source code in epinterface/sbem/components/envelope.py
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
class EnvelopeAssemblyComponent(
    NamedObject,
    MetadataMixin,
    extra="forbid",
):
    """Zone construction object."""

    FlatRoofAssembly: ConstructionAssemblyComponent = Field(
        ..., title="Flat roof construction object; only used when no attic is present."
    )
    FacadeAssembly: ConstructionAssemblyComponent = Field(
        ..., title="Facade construction object"
    )
    FloorCeilingAssembly: ConstructionAssemblyComponent = Field(
        ..., title="Floor/ceiling construction object"
    )
    AtticRoofAssembly: ConstructionAssemblyComponent = Field(
        ...,
        title="Attic roof construction object (outer surface) - only used when attic is present.",
    )
    AtticFloorAssembly: ConstructionAssemblyComponent = Field(
        ..., title="Attic floor construction object  - only used when attic is present."
    )
    PartitionAssembly: ConstructionAssemblyComponent = Field(
        ..., title="Partition construction object"
    )
    ExternalFloorAssembly: ConstructionAssemblyComponent = Field(
        ..., title="External floor construction object"
    )
    GroundSlabAssembly: ConstructionAssemblyComponent = Field(
        ..., title="Ground slab construction object"
    )
    GroundWallAssembly: ConstructionAssemblyComponent = Field(
        ...,
        title="Ground wall construction object (only used when basement is present)",
    )
    BasementCeilingAssembly: ConstructionAssemblyComponent = Field(
        ...,
        title="Basement ceiling construction object (only used when basement is present)",
    )
    InternalMassAssembly: ConstructionAssemblyComponent | None = Field(
        default=None, title="Internal mass construction object"
    )
    InternalMassExposedAreaPerArea: float | None = Field(
        default=None,
        title="Internal mass exposed area per area [m²/m²]",
        ge=0,
    )

    @model_validator(mode="after")
    def validate_internal_mass_exposed_area_per_area(self):
        """Validate that either both internal mass assembly and internal mass exposed area are provided, or neither."""
        if self.InternalMassAssembly and (
            self.InternalMassExposedAreaPerArea is None
            or self.InternalMassExposedAreaPerArea == 0
        ):
            msg = "Internal mass assembly cannot be provided if internal mass exposed area per area is not provided (or 0)."
            raise ValueError(msg)
        if (
            self.InternalMassExposedAreaPerArea is not None
            and self.InternalMassExposedAreaPerArea != 0
        ) and self.InternalMassAssembly is None:
            msg = "Internal mass exposed area per area must be provided if internal mass assembly is provided"
            raise ValueError(msg)
        return self

validate_internal_mass_exposed_area_per_area() #

Validate that either both internal mass assembly and internal mass exposed area are provided, or neither.

Source code in epinterface/sbem/components/envelope.py
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
@model_validator(mode="after")
def validate_internal_mass_exposed_area_per_area(self):
    """Validate that either both internal mass assembly and internal mass exposed area are provided, or neither."""
    if self.InternalMassAssembly and (
        self.InternalMassExposedAreaPerArea is None
        or self.InternalMassExposedAreaPerArea == 0
    ):
        msg = "Internal mass assembly cannot be provided if internal mass exposed area per area is not provided (or 0)."
        raise ValueError(msg)
    if (
        self.InternalMassExposedAreaPerArea is not None
        and self.InternalMassExposedAreaPerArea != 0
    ) and self.InternalMassAssembly is None:
        msg = "Internal mass exposed area per area must be provided if internal mass assembly is provided"
        raise ValueError(msg)
    return self

GlazingConstructionSimpleComponent #

Bases: NamedObject, StandardMaterialMetadataMixin, MetadataMixin

Glazing construction object.

Source code in epinterface/sbem/components/envelope.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class GlazingConstructionSimpleComponent(
    NamedObject,
    StandardMaterialMetadataMixin,
    MetadataMixin,
    extra="forbid",
):
    """Glazing construction object."""

    SHGF: float = Field(..., title="Solar heat gain factor", ge=0, le=1)
    UValue: float = Field(
        ...,
        title="U-value [W/m²K]",
        ge=0,
    )
    TVis: float = Field(..., title="Visible transmittance", ge=0, le=1)
    Type: WindowType = Field(..., title="Type of the glazing construction")

    def add_to_idf(self, idf: IDF) -> IDF:
        """Adds the glazing construction to an IDF object.

        Args:
            idf (IDF): The IDF object to add the construction to.

        Returns:
            IDF: The updated IDF object.
        """
        glazing_mat = SimpleGlazingMaterial(
            Name=self.Name,
            UFactor=self.UValue,
            Solar_Heat_Gain_Coefficient=self.SHGF,
            Visible_Transmittance=self.TVis,
        )

        construction = Construction(
            name=self.Name,
            layers=[glazing_mat],
        )

        idf = construction.add(idf)
        return idf

add_to_idf(idf) #

Adds the glazing construction to an IDF object.

Parameters:

Name Type Description Default
idf IDF

The IDF object to add the construction to.

required

Returns:

Name Type Description
IDF IDF

The updated IDF object.

Source code in epinterface/sbem/components/envelope.py
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def add_to_idf(self, idf: IDF) -> IDF:
    """Adds the glazing construction to an IDF object.

    Args:
        idf (IDF): The IDF object to add the construction to.

    Returns:
        IDF: The updated IDF object.
    """
    glazing_mat = SimpleGlazingMaterial(
        Name=self.Name,
        UFactor=self.UValue,
        Solar_Heat_Gain_Coefficient=self.SHGF,
        Visible_Transmittance=self.TVis,
    )

    construction = Construction(
        name=self.Name,
        layers=[glazing_mat],
    )

    idf = construction.add(idf)
    return idf

InfiltrationComponent #

Bases: NamedObject, MetadataMixin

Zone infiltration object.

Source code in epinterface/sbem/components/envelope.py
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
class InfiltrationComponent(
    NamedObject,
    MetadataMixin,
    extra="forbid",
):
    """Zone infiltration object."""

    IsOn: BoolStr = Field(..., title="Infiltration is on")
    # TODO: add assumed_constants
    # TODO: these values should have stronger validators are on them or be dropped entirely and use defaults from EP wrapper classes
    ConstantCoefficient: float = Field(
        ...,
        title="Infiltration constant coefficient",
    )
    TemperatureCoefficient: float = Field(
        ...,
        title="Infiltration temperature coefficient",
    )
    WindVelocityCoefficient: float = Field(
        ...,
        title="Infiltration wind velocity coefficient",
    )
    WindVelocitySquaredCoefficient: float = Field(
        ...,
        title="Infiltration wind velocity squared coefficient",
    )
    AFNAirMassFlowCoefficientCrack: float = Field(
        ...,
        title="AFN air mass flow coefficient crack",
    )

    AirChangesPerHour: float = Field(
        ...,
        title="Infiltration air changes per hour [ACH]",
        ge=0,
    )
    FlowPerExteriorSurfaceArea: float = Field(
        ...,
        title="Infiltration flow per exterior surface area [m3/s/m2]",
        ge=0,
    )
    CalculationMethod: InfDesignFlowRateCalculationMethodType = Field(
        ...,
        title="Calculation method",
    )

    def add_infiltration_to_idf_zone(
        self, idf: IDF, target_zone_or_zone_list_name: str
    ):
        """Add infiltration to an IDF zone.

        Args:
            idf (IDF): The IDF object to add the infiltration to.
            target_zone_or_zone_list_name (str): The name of the zone or zone list to add the infiltration to.

        Returns:
            idf (IDF): The updated IDF object.
        """
        if not self.IsOn:
            return idf

        infiltration_schedule_name = (
            f"{target_zone_or_zone_list_name}_{self.safe_name}_INFILTRATION_Schedule"
        )
        infiltration_name = (
            f"{target_zone_or_zone_list_name}_{self.safe_name}_INFILTRATION"
        )
        schedule = Schedule.constant_schedule(
            value=1, Name=infiltration_schedule_name, Type="Fraction"
        )
        inf_schedule, *_ = schedule.to_year_week_day()
        inf_schedule.to_epbunch(idf)
        inf = ZoneInfiltrationDesignFlowRate(
            Name=infiltration_name,
            Zone_or_ZoneList_Name=target_zone_or_zone_list_name,
            Schedule_Name=inf_schedule.Name,
            Design_Flow_Rate_Calculation_Method=self.CalculationMethod,
            Flow_Rate_per_Exterior_Surface_Area=self.FlowPerExteriorSurfaceArea,
            Air_Changes_per_Hour=self.AirChangesPerHour,
            Flow_Rate_per_Floor_Area=None,
            Design_Flow_Rate=None,
            # Constant_Term_Coefficient=self.ConstantCoefficient,
            # Temperature_Term_Coefficient=self.TemperatureCoefficient,
            # Velocity_Term_Coefficient=self.WindVelocityCoefficient,
            # Velocity_Squared_Term_Coefficient=self.WindVelocitySquaredCoefficient,
        )
        idf = inf.add(idf)
        return idf

add_infiltration_to_idf_zone(idf, target_zone_or_zone_list_name) #

Add infiltration to an IDF zone.

Parameters:

Name Type Description Default
idf IDF

The IDF object to add the infiltration to.

required
target_zone_or_zone_list_name str

The name of the zone or zone list to add the infiltration to.

required

Returns:

Name Type Description
idf IDF

The updated IDF object.

Source code in epinterface/sbem/components/envelope.py
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
def add_infiltration_to_idf_zone(
    self, idf: IDF, target_zone_or_zone_list_name: str
):
    """Add infiltration to an IDF zone.

    Args:
        idf (IDF): The IDF object to add the infiltration to.
        target_zone_or_zone_list_name (str): The name of the zone or zone list to add the infiltration to.

    Returns:
        idf (IDF): The updated IDF object.
    """
    if not self.IsOn:
        return idf

    infiltration_schedule_name = (
        f"{target_zone_or_zone_list_name}_{self.safe_name}_INFILTRATION_Schedule"
    )
    infiltration_name = (
        f"{target_zone_or_zone_list_name}_{self.safe_name}_INFILTRATION"
    )
    schedule = Schedule.constant_schedule(
        value=1, Name=infiltration_schedule_name, Type="Fraction"
    )
    inf_schedule, *_ = schedule.to_year_week_day()
    inf_schedule.to_epbunch(idf)
    inf = ZoneInfiltrationDesignFlowRate(
        Name=infiltration_name,
        Zone_or_ZoneList_Name=target_zone_or_zone_list_name,
        Schedule_Name=inf_schedule.Name,
        Design_Flow_Rate_Calculation_Method=self.CalculationMethod,
        Flow_Rate_per_Exterior_Surface_Area=self.FlowPerExteriorSurfaceArea,
        Air_Changes_per_Hour=self.AirChangesPerHour,
        Flow_Rate_per_Floor_Area=None,
        Design_Flow_Rate=None,
        # Constant_Term_Coefficient=self.ConstantCoefficient,
        # Temperature_Term_Coefficient=self.TemperatureCoefficient,
        # Velocity_Term_Coefficient=self.WindVelocityCoefficient,
        # Velocity_Squared_Term_Coefficient=self.WindVelocitySquaredCoefficient,
    )
    idf = inf.add(idf)
    return idf

ZoneEnvelopeComponent #

Bases: NamedObject, MetadataMixin

Zone envelope object.

Source code in epinterface/sbem/components/envelope.py
353
354
355
356
357
358
359
360
class ZoneEnvelopeComponent(NamedObject, MetadataMixin, extra="forbid"):
    """Zone envelope object."""

    Assemblies: EnvelopeAssemblyComponent
    Infiltration: InfiltrationComponent
    AtticInfiltration: InfiltrationComponent
    BasementInfiltration: InfiltrationComponent
    Window: GlazingConstructionSimpleComponent | None

Materials#

Materials for the SBEM library.

CommonMaterialPropertiesMixin #

Bases: BaseModel

Common material properties for glazing and opaque materials.

Source code in epinterface/sbem/components/materials.py
36
37
38
39
40
41
42
43
44
45
46
47
48
class CommonMaterialPropertiesMixin(BaseModel):
    """Common material properties for glazing and opaque materials."""

    Conductivity: float = Field(
        ...,
        title="Conductivity [W/mK]",
        ge=0,
    )
    Density: float = Field(
        ...,
        title="Density [kg/m3]",
        ge=0,
    )

ConstructionMaterialComponent #

Bases: ConstructionMaterialProperties, StandardMaterialMetadataMixin, NamedObject, MetadataMixin

Construction material object.

Source code in epinterface/sbem/components/materials.py
135
136
137
138
139
140
141
142
143
144
class ConstructionMaterialComponent(
    ConstructionMaterialProperties,
    StandardMaterialMetadataMixin,
    NamedObject,
    MetadataMixin,
    extra="forbid",
):
    """Construction material object."""

    pass

ConstructionMaterialProperties #

Bases: CommonMaterialPropertiesMixin

Properties of an opaque material.

Source code in epinterface/sbem/components/materials.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
class ConstructionMaterialProperties(
    CommonMaterialPropertiesMixin,
    extra="forbid",
):
    """Properties of an opaque material."""

    # add in the commonMaterialsPropertis
    Roughness: MaterialRoughness = Field(..., title="Roughness of the opaque material")
    SpecificHeat: float = Field(
        ...,
        title="Specific heat [J/kgK]",
        ge=0,
    )
    ThermalAbsorptance: float = Field(
        ...,
        title="Thermal absorptance [0-1]",
        ge=0,
        le=1,
    )
    SolarAbsorptance: float = Field(
        ...,
        title="Solar absorptance [0-1]",
        ge=0,
        le=1,
    )
    VisibleAbsorptance: float = Field(
        ...,
        title="Visible absorptance [0-1]",
        ge=0,
        le=1,
    )

    TemperatureCoefficientThermalConductivity: float = Field(
        ...,
        # a superscript 2 looks like this:
        title="Temperature coefficient of thermal conductivity [W/m.K2²]",
        ge=0,
    )
    # TODO: material type should be dynamic user entry or enum
    Type: ConstructionMaterialType = Field(..., title="Type of the opaque material")

EnvironmentalMixin #

Bases: BaseModel

Environmental data for a SBEM template table object.

Source code in epinterface/sbem/components/materials.py
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class EnvironmentalMixin(BaseModel):
    """Environmental data for a SBEM template table object."""

    Cost: float | None = Field(
        default=None, title="Cost", ge=0, description="Cost of the material/unit"
    )
    RateUnit: Literal["m3", "m2", "m", "kg"] | None = Field(
        default=None,
        description="The base unit for cost and embodied carbon, i.e. $/unit",
    )
    Life: float | None = Field(
        default=None, title="Life [years]", ge=0, description="Life of the material"
    )
    EmbodiedCarbon: float | None = Field(
        default=None, title="Embodied carbon [kgCO2e/unit]", ge=0
    )

StandardMaterialMetadataMixin #

Bases: EnvironmentalMixin, MetadataMixin

Standard metadata for a SBEM data.

Source code in epinterface/sbem/components/materials.py
29
30
31
32
class StandardMaterialMetadataMixin(EnvironmentalMixin, MetadataMixin):
    """Standard metadata for a SBEM data."""

    pass

Schedules#

This module contains the definitions for the schedules.

DayComponent #

Bases: NamedObject

A day of the week with a schedule type limit and a list of values.

Source code in epinterface/sbem/components/schedules.py
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
class DayComponent(NamedObject, extra="forbid"):
    """A day of the week with a schedule type limit and a list of values."""

    Type: ScheduleTypeLimitType = Field(
        ..., description="The ScheduleTypeLimits of the day."
    )
    Hour_00: float
    Hour_01: float
    Hour_02: float
    Hour_03: float
    Hour_04: float
    Hour_05: float
    Hour_06: float
    Hour_07: float
    Hour_08: float
    Hour_09: float
    Hour_10: float
    Hour_11: float
    Hour_12: float
    Hour_13: float
    Hour_14: float
    Hour_15: float
    Hour_16: float
    Hour_17: float
    Hour_18: float
    Hour_19: float
    Hour_20: float
    Hour_21: float
    Hour_22: float
    Hour_23: float

    @property
    def AverageValue(self) -> float:
        """Get the average value of the day."""
        return sum(self.Values) / len(self.Values)

    @property
    def bounds(self) -> tuple[float, float]:
        """Get the bounds of the day."""
        return min(self.Values), max(self.Values)

    @property
    def Values(self) -> list[float]:
        """Get the values of the day as a list."""
        return [
            self.Hour_00,
            self.Hour_01,
            self.Hour_02,
            self.Hour_03,
            self.Hour_04,
            self.Hour_05,
            self.Hour_06,
            self.Hour_07,
            self.Hour_08,
            self.Hour_09,
            self.Hour_10,
            self.Hour_11,
            self.Hour_12,
            self.Hour_13,
            self.Hour_14,
            self.Hour_15,
            self.Hour_16,
            self.Hour_17,
            self.Hour_18,
            self.Hour_19,
            self.Hour_20,
            self.Hour_21,
            self.Hour_22,
            self.Hour_23,
        ]

    @model_validator(mode="after")
    def validate_values(self):
        """Validate the values of the day are consistent with the schedule type limit."""
        # TODO: Implement with a eye for Archetypal

        lim_low = TypeLimits[self.Type].Lower_Limit_Value
        lim_high = TypeLimits[self.Type].Upper_Limit_Value
        if lim_low is not None and any(v < lim_low for v in self.Values):
            msg = f"Values are less than the lower limit: {lim_low}"
            raise ValueError(msg)
        if lim_high is not None and any(v > lim_high for v in self.Values):
            msg = f"Values are greater than the upper limit: {lim_high}"
            raise ValueError(msg)
        return self

    def add_day_to_idf(self, idf: IDF, name_prefix: str | None) -> tuple[IDF, str]:
        """Add the day to the IDF.

        The name prefix can be used to scope the schedule creation to ensure a unique schedule per object.

        Args:
            idf (IDF): The IDF object to add the day to.
            name_prefix (str | None): The prefix to use for the schedule name.

        Returns:
            idf (IDF): The IDF object with the day added.
            day_name (str): The name of the day schedule.
        """
        desired_name = self.Name
        if name_prefix is not None:
            desired_name = f"{name_prefix}_DAY_{desired_name}"

        if idf.getobject("SCHEDULE:DAY:HOURLY", desired_name):
            return idf, desired_name

        day_sched = ScheduleDayHourly(
            Name=desired_name,
            Schedule_Type_Limits_Name=self.Type,
            Hour_1=self.Hour_00,
            Hour_2=self.Hour_01,
            Hour_3=self.Hour_02,
            Hour_4=self.Hour_03,
            Hour_5=self.Hour_04,
            Hour_6=self.Hour_05,
            Hour_7=self.Hour_06,
            Hour_8=self.Hour_07,
            Hour_9=self.Hour_08,
            Hour_10=self.Hour_09,
            Hour_11=self.Hour_10,
            Hour_12=self.Hour_11,
            Hour_13=self.Hour_12,
            Hour_14=self.Hour_13,
            Hour_15=self.Hour_14,
            Hour_16=self.Hour_15,
            Hour_17=self.Hour_16,
            Hour_18=self.Hour_17,
            Hour_19=self.Hour_18,
            Hour_20=self.Hour_19,
            Hour_21=self.Hour_20,
            Hour_22=self.Hour_21,
            Hour_23=self.Hour_22,
            Hour_24=self.Hour_23,
        )
        idf = day_sched.add(idf)
        return idf, day_sched.Name

AverageValue property #

Get the average value of the day.

Values property #

Get the values of the day as a list.

bounds property #

Get the bounds of the day.

add_day_to_idf(idf, name_prefix) #

Add the day to the IDF.

The name prefix can be used to scope the schedule creation to ensure a unique schedule per object.

Parameters:

Name Type Description Default
idf IDF

The IDF object to add the day to.

required
name_prefix str | None

The prefix to use for the schedule name.

required

Returns:

Name Type Description
idf IDF

The IDF object with the day added.

day_name str

The name of the day schedule.

Source code in epinterface/sbem/components/schedules.py
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
def add_day_to_idf(self, idf: IDF, name_prefix: str | None) -> tuple[IDF, str]:
    """Add the day to the IDF.

    The name prefix can be used to scope the schedule creation to ensure a unique schedule per object.

    Args:
        idf (IDF): The IDF object to add the day to.
        name_prefix (str | None): The prefix to use for the schedule name.

    Returns:
        idf (IDF): The IDF object with the day added.
        day_name (str): The name of the day schedule.
    """
    desired_name = self.Name
    if name_prefix is not None:
        desired_name = f"{name_prefix}_DAY_{desired_name}"

    if idf.getobject("SCHEDULE:DAY:HOURLY", desired_name):
        return idf, desired_name

    day_sched = ScheduleDayHourly(
        Name=desired_name,
        Schedule_Type_Limits_Name=self.Type,
        Hour_1=self.Hour_00,
        Hour_2=self.Hour_01,
        Hour_3=self.Hour_02,
        Hour_4=self.Hour_03,
        Hour_5=self.Hour_04,
        Hour_6=self.Hour_05,
        Hour_7=self.Hour_06,
        Hour_8=self.Hour_07,
        Hour_9=self.Hour_08,
        Hour_10=self.Hour_09,
        Hour_11=self.Hour_10,
        Hour_12=self.Hour_11,
        Hour_13=self.Hour_12,
        Hour_14=self.Hour_13,
        Hour_15=self.Hour_14,
        Hour_16=self.Hour_15,
        Hour_17=self.Hour_16,
        Hour_18=self.Hour_17,
        Hour_19=self.Hour_18,
        Hour_20=self.Hour_19,
        Hour_21=self.Hour_20,
        Hour_22=self.Hour_21,
        Hour_23=self.Hour_22,
        Hour_24=self.Hour_23,
    )
    idf = day_sched.add(idf)
    return idf, day_sched.Name

validate_values() #

Validate the values of the day are consistent with the schedule type limit.

Source code in epinterface/sbem/components/schedules.py
113
114
115
116
117
118
119
120
121
122
123
124
125
126
@model_validator(mode="after")
def validate_values(self):
    """Validate the values of the day are consistent with the schedule type limit."""
    # TODO: Implement with a eye for Archetypal

    lim_low = TypeLimits[self.Type].Lower_Limit_Value
    lim_high = TypeLimits[self.Type].Upper_Limit_Value
    if lim_low is not None and any(v < lim_low for v in self.Values):
        msg = f"Values are less than the lower limit: {lim_low}"
        raise ValueError(msg)
    if lim_high is not None and any(v > lim_high for v in self.Values):
        msg = f"Values are greater than the upper limit: {lim_high}"
        raise ValueError(msg)
    return self

WeekComponent #

Bases: NamedObject

A week with a list of days.

Source code in epinterface/sbem/components/schedules.py
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
class WeekComponent(NamedObject, extra="forbid"):
    """A week with a list of days."""

    Monday: DayComponent
    Tuesday: DayComponent
    Wednesday: DayComponent
    Thursday: DayComponent
    Friday: DayComponent
    Saturday: DayComponent
    Sunday: DayComponent

    @model_validator(mode="after")
    def validate_type_limits_are_consistent(self):
        """Validate that the type limits are consistent."""
        lim = self.Monday.Type
        for day in self.Days:
            if day.Type != lim:
                msg = "Type limits are not consistent"
                raise ValueError(msg)
        return self

    @property
    def bounds(self) -> tuple[float, float]:
        """Get the bounds of the week."""
        lows = [day.bounds[0] for day in self.Days]
        highs = [day.bounds[1] for day in self.Days]
        return min(lows), max(highs)

    @property
    def AverageValue(self) -> float:
        """Get the average value of the week."""
        return sum(day.AverageValue for day in self.Days) / len(self.Days)

    @property
    def Days(self) -> list[DayComponent]:
        """Get the days of the week as a list."""
        return [
            self.Monday,
            self.Tuesday,
            self.Wednesday,
            self.Thursday,
            self.Friday,
            self.Saturday,
            self.Sunday,
        ]

    def add_week_to_idf(
        self,
        idf: IDF,
        name_prefix: str | None,
        summer_design_day_sch_name: str | None = None,
        winter_design_day_sch_name: str | None = None,
    ) -> tuple[IDF, str]:
        """Add the week to the IDF.

        The name prefix can be used to scope the schedule creation to ensure a unique schedule per object.

        Args:
            idf (IDF): The IDF object to add the week to.
            name_prefix (str | None): The prefix to use for the schedule name.
            summer_design_day_sch_name (str | None): The name of the summer design day schedule.
            winter_design_day_sch_name (str | None): The name of the winter design day schedule.

        Returns:
            idf (IDF): The IDF object with the week added.
            week_name (str): The name of the week schedule.
        """
        desired_name = self.Name
        if name_prefix is not None:
            desired_name = f"{name_prefix}_WEEK_{desired_name}"

        if idf.getobject("SCHEDULE:WEEK:DAILY", desired_name):
            return idf, desired_name

        idf, monday_name = self.Monday.add_day_to_idf(idf, name_prefix)
        idf, tuesday_name = self.Tuesday.add_day_to_idf(idf, name_prefix)
        idf, wednesday_name = self.Wednesday.add_day_to_idf(idf, name_prefix)
        idf, thursday_name = self.Thursday.add_day_to_idf(idf, name_prefix)
        idf, friday_name = self.Friday.add_day_to_idf(idf, name_prefix)
        idf, saturday_name = self.Saturday.add_day_to_idf(idf, name_prefix)
        idf, sunday_name = self.Sunday.add_day_to_idf(idf, name_prefix)
        week_sched = ScheduleWeekDaily(
            Name=desired_name,
            Monday_ScheduleDay_Name=monday_name,
            Tuesday_ScheduleDay_Name=tuesday_name,
            Wednesday_ScheduleDay_Name=wednesday_name,
            Thursday_ScheduleDay_Name=thursday_name,
            Friday_ScheduleDay_Name=friday_name,
            Saturday_ScheduleDay_Name=saturday_name,
            Sunday_ScheduleDay_Name=sunday_name,
            SummerDesignDay_ScheduleDay_Name=summer_design_day_sch_name,
            WinterDesignDay_ScheduleDay_Name=winter_design_day_sch_name,
        )
        idf = week_sched.add(idf)
        return idf, week_sched.Name

    @property
    def Type(self) -> ScheduleTypeLimitType:
        """Get the type limit of the week."""
        return self.Monday.Type

AverageValue property #

Get the average value of the week.

Days property #

Get the days of the week as a list.

Type property #

Get the type limit of the week.

bounds property #

Get the bounds of the week.

add_week_to_idf(idf, name_prefix, summer_design_day_sch_name=None, winter_design_day_sch_name=None) #

Add the week to the IDF.

The name prefix can be used to scope the schedule creation to ensure a unique schedule per object.

Parameters:

Name Type Description Default
idf IDF

The IDF object to add the week to.

required
name_prefix str | None

The prefix to use for the schedule name.

required
summer_design_day_sch_name str | None

The name of the summer design day schedule.

None
winter_design_day_sch_name str | None

The name of the winter design day schedule.

None

Returns:

Name Type Description
idf IDF

The IDF object with the week added.

week_name str

The name of the week schedule.

Source code in epinterface/sbem/components/schedules.py
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
def add_week_to_idf(
    self,
    idf: IDF,
    name_prefix: str | None,
    summer_design_day_sch_name: str | None = None,
    winter_design_day_sch_name: str | None = None,
) -> tuple[IDF, str]:
    """Add the week to the IDF.

    The name prefix can be used to scope the schedule creation to ensure a unique schedule per object.

    Args:
        idf (IDF): The IDF object to add the week to.
        name_prefix (str | None): The prefix to use for the schedule name.
        summer_design_day_sch_name (str | None): The name of the summer design day schedule.
        winter_design_day_sch_name (str | None): The name of the winter design day schedule.

    Returns:
        idf (IDF): The IDF object with the week added.
        week_name (str): The name of the week schedule.
    """
    desired_name = self.Name
    if name_prefix is not None:
        desired_name = f"{name_prefix}_WEEK_{desired_name}"

    if idf.getobject("SCHEDULE:WEEK:DAILY", desired_name):
        return idf, desired_name

    idf, monday_name = self.Monday.add_day_to_idf(idf, name_prefix)
    idf, tuesday_name = self.Tuesday.add_day_to_idf(idf, name_prefix)
    idf, wednesday_name = self.Wednesday.add_day_to_idf(idf, name_prefix)
    idf, thursday_name = self.Thursday.add_day_to_idf(idf, name_prefix)
    idf, friday_name = self.Friday.add_day_to_idf(idf, name_prefix)
    idf, saturday_name = self.Saturday.add_day_to_idf(idf, name_prefix)
    idf, sunday_name = self.Sunday.add_day_to_idf(idf, name_prefix)
    week_sched = ScheduleWeekDaily(
        Name=desired_name,
        Monday_ScheduleDay_Name=monday_name,
        Tuesday_ScheduleDay_Name=tuesday_name,
        Wednesday_ScheduleDay_Name=wednesday_name,
        Thursday_ScheduleDay_Name=thursday_name,
        Friday_ScheduleDay_Name=friday_name,
        Saturday_ScheduleDay_Name=saturday_name,
        Sunday_ScheduleDay_Name=sunday_name,
        SummerDesignDay_ScheduleDay_Name=summer_design_day_sch_name,
        WinterDesignDay_ScheduleDay_Name=winter_design_day_sch_name,
    )
    idf = week_sched.add(idf)
    return idf, week_sched.Name

validate_type_limits_are_consistent() #

Validate that the type limits are consistent.

Source code in epinterface/sbem/components/schedules.py
191
192
193
194
195
196
197
198
199
@model_validator(mode="after")
def validate_type_limits_are_consistent(self):
    """Validate that the type limits are consistent."""
    lim = self.Monday.Type
    for day in self.Days:
        if day.Type != lim:
            msg = "Type limits are not consistent"
            raise ValueError(msg)
    return self

YearComponent #

Bases: NamedObject

A year with a schedule type limit and a list of repeated weeks.

Source code in epinterface/sbem/components/schedules.py
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
class YearComponent(NamedObject, extra="forbid"):
    """A year with a schedule type limit and a list of repeated weeks."""

    Type: YearScheduleCategory = Field(
        ..., description="The system that the schedule is applicable to."
    )
    January: WeekComponent
    February: WeekComponent
    March: WeekComponent
    April: WeekComponent
    May: WeekComponent
    June: WeekComponent
    July: WeekComponent
    August: WeekComponent
    September: WeekComponent
    October: WeekComponent
    November: WeekComponent
    December: WeekComponent

    @property
    def AverageValue(self) -> float:
        """Get the average value of the year."""
        return sum(week.AverageValue for week in self.Weeks) / len(self.Weeks)

    @property
    def MonthlyAverageValues(self) -> list[float]:
        """Get the average values of the year."""
        return [week.AverageValue for week in self.Weeks]

    @property
    def bounds(self) -> tuple[float, float]:
        """Get the bounds of the year."""
        lows = [week.bounds[0] for week in self.Weeks]
        highs = [week.bounds[1] for week in self.Weeks]
        return min(lows), max(highs)

    @property
    def Weeks(self) -> list[WeekComponent]:
        """Get the weeks of the year as a list."""
        return [
            self.January,
            self.February,
            self.March,
            self.April,
            self.May,
            self.June,
            self.July,
            self.August,
            self.September,
            self.October,
            self.November,
            self.December,
        ]

    @model_validator(mode="after")
    def check_weeks_have_consistent_type(self):
        """Check that the weeks have a consistent type."""
        lim = self.January.Type
        for week in self.Weeks:
            if week.Type != lim:
                msg = "Type limits are not consistent"
                raise ValueError(msg)

        return self

    @property
    def schedule_type_limits(self):
        """Get the schedule type limits for the year."""
        return self.January.Type

    def add_year_to_idf(
        self,
        idf: IDF,
        name_prefix: str | None = None,
        summer_design_day_sch_name: str | None = None,
        winter_design_day_sch_name: str | None = None,
    ):
        """Add the year to the IDF.

        The name prefix can be used to scope the schedule creation to ensure a unique schedule per object.

        Args:
            idf (IDF): The IDF object to add the year to.
            name_prefix (str | None): The prefix to use for the schedule name.
            summer_design_day_sch_name (str | None): The name of the summer design day schedule.
            winter_design_day_sch_name (str | None): The name of the winter design day schedule.

        Returns:
            idf (IDF): The IDF object with the year added.
            year_name (str): The name of the year schedule.
        """
        desired_name = self.Name
        if name_prefix is not None:
            desired_name = f"{name_prefix}_YEAR_{desired_name}"

        if idf.getobject("SCHEDULE:YEAR", desired_name):
            return idf, desired_name

        idf, jan_name = self.January.add_week_to_idf(
            idf,
            name_prefix,
            summer_design_day_sch_name=summer_design_day_sch_name,
            winter_design_day_sch_name=winter_design_day_sch_name,
        )
        idf, feb_name = self.February.add_week_to_idf(
            idf,
            name_prefix,
            summer_design_day_sch_name=summer_design_day_sch_name,
            winter_design_day_sch_name=winter_design_day_sch_name,
        )
        idf, mar_name = self.March.add_week_to_idf(
            idf,
            name_prefix,
            summer_design_day_sch_name=summer_design_day_sch_name,
            winter_design_day_sch_name=winter_design_day_sch_name,
        )
        idf, apr_name = self.April.add_week_to_idf(
            idf,
            name_prefix,
            summer_design_day_sch_name=summer_design_day_sch_name,
            winter_design_day_sch_name=winter_design_day_sch_name,
        )
        idf, may_name = self.May.add_week_to_idf(
            idf,
            name_prefix,
            summer_design_day_sch_name=summer_design_day_sch_name,
            winter_design_day_sch_name=winter_design_day_sch_name,
        )
        idf, jun_name = self.June.add_week_to_idf(
            idf,
            name_prefix,
            summer_design_day_sch_name=summer_design_day_sch_name,
            winter_design_day_sch_name=winter_design_day_sch_name,
        )
        idf, jul_name = self.July.add_week_to_idf(
            idf,
            name_prefix,
            summer_design_day_sch_name=summer_design_day_sch_name,
            winter_design_day_sch_name=winter_design_day_sch_name,
        )
        idf, aug_name = self.August.add_week_to_idf(
            idf,
            name_prefix,
            summer_design_day_sch_name=summer_design_day_sch_name,
            winter_design_day_sch_name=winter_design_day_sch_name,
        )
        idf, sep_name = self.September.add_week_to_idf(
            idf,
            name_prefix,
            summer_design_day_sch_name=summer_design_day_sch_name,
            winter_design_day_sch_name=winter_design_day_sch_name,
        )
        idf, oct_name = self.October.add_week_to_idf(
            idf,
            name_prefix,
            summer_design_day_sch_name=summer_design_day_sch_name,
            winter_design_day_sch_name=winter_design_day_sch_name,
        )
        idf, nov_name = self.November.add_week_to_idf(
            idf,
            name_prefix,
            summer_design_day_sch_name=summer_design_day_sch_name,
            winter_design_day_sch_name=winter_design_day_sch_name,
        )
        idf, dec_name = self.December.add_week_to_idf(
            idf,
            name_prefix,
            summer_design_day_sch_name=summer_design_day_sch_name,
            winter_design_day_sch_name=winter_design_day_sch_name,
        )
        year_sched = ScheduleYear(
            Name=self.Name,
            Schedule_Type_Limits_Name=self.schedule_type_limits,
            ScheduleWeek_Name_1=jan_name,
            Start_Month_1=1,
            Start_Day_1=1,
            End_Month_1=1,
            End_Day_1=31,
            ScheduleWeek_Name_2=feb_name,
            Start_Month_2=2,
            Start_Day_2=1,
            End_Month_2=2,
            End_Day_2=28,
            ScheduleWeek_Name_3=mar_name,
            Start_Month_3=3,
            Start_Day_3=1,
            End_Month_3=3,
            End_Day_3=31,
            ScheduleWeek_Name_4=apr_name,
            Start_Month_4=4,
            Start_Day_4=1,
            End_Month_4=4,
            End_Day_4=30,
            ScheduleWeek_Name_5=may_name,
            Start_Month_5=5,
            Start_Day_5=1,
            End_Month_5=5,
            End_Day_5=31,
            ScheduleWeek_Name_6=jun_name,
            Start_Month_6=6,
            Start_Day_6=1,
            End_Month_6=6,
            End_Day_6=30,
            ScheduleWeek_Name_7=jul_name,
            Start_Month_7=7,
            Start_Day_7=1,
            End_Month_7=7,
            End_Day_7=31,
            ScheduleWeek_Name_8=aug_name,
            Start_Month_8=8,
            Start_Day_8=1,
            End_Month_8=8,
            End_Day_8=31,
            ScheduleWeek_Name_9=sep_name,
            Start_Month_9=9,
            Start_Day_9=1,
            End_Month_9=9,
            End_Day_9=30,
            ScheduleWeek_Name_10=oct_name,
            Start_Month_10=10,
            Start_Day_10=1,
            End_Month_10=10,
            End_Day_10=31,
            ScheduleWeek_Name_11=nov_name,
            Start_Month_11=11,
            Start_Day_11=1,
            End_Month_11=11,
            End_Day_11=30,
            ScheduleWeek_Name_12=dec_name,
            Start_Month_12=12,
            Start_Day_12=1,
            End_Month_12=12,
            End_Day_12=31,
        )
        idf = year_sched.add(idf)

        type_lim = self.schedule_type_limits
        if not idf.getobject("SCHEDULETYPELIMITS", type_lim):
            if type_lim not in TypeLimits:
                msg = f"Type {type_lim} not in TypeLimits, unsure how to add to IDF."
                raise ValueError(msg)
            lim = TypeLimits[type_lim]
            lim.add(idf)

        return idf, year_sched.Name

    def fractional_year_sum(self, year: int):
        """Compute the sum of the year as a fraction of the year."""
        if self.schedule_type_limits != "Fraction":
            msg = "Schedule type limits are not Fraction, cannot compute year sum."
            raise ValueError(msg)

        # get the numer of Mondays in each month, tuesdays in each month, etc.

        def get_num_days_in_month(
            month: Literal[
                "January",
                "February",
                "March",
                "April",
                "May",
                "June",
                "July",
                "August",
                "September",
                "October",
                "November",
                "December",
            ],
            year: int,
            day_of_week: Literal[
                "Monday",
                "Tuesday",
                "Wednesday",
                "Thursday",
                "Friday",
                "Saturday",
                "Sunday",
            ],
        ):
            """Get the number of days in a month for a given year and day of the week."""
            # get the number of days in the month
            month_map = {
                "January": 1,
                "February": 2,
                "March": 3,
                "April": 4,
                "May": 5,
                "June": 6,
                "July": 7,
                "August": 8,
                "September": 9,
                "October": 10,
                "November": 11,
                "December": 12,
            }
            _weekday, num_days = calendar.monthrange(year, month_map[month])

            # get the number of days of the week in the month
            days_of_week = [calendar.day_name[(day + 1) % 7] for day in range(num_days)]
            return days_of_week.count(day_of_week)

        # get the number of Mondays in each month
        months: list[
            Literal[
                "January",
                "February",
                "March",
                "April",
                "May",
                "June",
                "July",
                "August",
                "September",
                "October",
                "November",
                "December",
            ]
        ] = [
            "January",
            "February",
            "March",
            "April",
            "May",
            "June",
            "July",
            "August",
            "September",
            "October",
            "November",
            "December",
        ]
        num_mondays = [get_num_days_in_month(month, year, "Monday") for month in months]
        num_tuesdays = [
            get_num_days_in_month(month, year, "Tuesday") for month in months
        ]
        num_wednesdays = [
            get_num_days_in_month(month, year, "Wednesday") for month in months
        ]
        num_thursdays = [
            get_num_days_in_month(month, year, "Thursday") for month in months
        ]
        num_fridays = [get_num_days_in_month(month, year, "Friday") for month in months]
        num_saturdays = [
            get_num_days_in_month(month, year, "Saturday") for month in months
        ]
        num_sundays = [get_num_days_in_month(month, year, "Sunday") for month in months]

        monday_sums = [
            sum(getattr(self, month).Monday.Values) * num_days
            for month, num_days in zip(months, num_mondays, strict=True)
        ]
        tuesday_sums = [
            sum(getattr(self, month).Tuesday.Values) * num_days
            for month, num_days in zip(months, num_tuesdays, strict=True)
        ]
        wednesday_sums = [
            sum(getattr(self, month).Wednesday.Values) * num_days
            for month, num_days in zip(months, num_wednesdays, strict=True)
        ]
        thursday_sums = [
            sum(getattr(self, month).Thursday.Values) * num_days
            for month, num_days in zip(months, num_thursdays, strict=True)
        ]
        friday_sums = [
            sum(getattr(self, month).Friday.Values) * num_days
            for month, num_days in zip(months, num_fridays, strict=True)
        ]
        saturday_sums = [
            sum(getattr(self, month).Saturday.Values) * num_days
            for month, num_days in zip(months, num_saturdays, strict=True)
        ]
        sunday_sums = [
            sum(getattr(self, month).Sunday.Values) * num_days
            for month, num_days in zip(months, num_sundays, strict=True)
        ]

        monday_sum = sum(monday_sums)
        tuesday_sum = sum(tuesday_sums)
        wednesday_sum = sum(wednesday_sums)
        thursday_sum = sum(thursday_sums)
        friday_sum = sum(friday_sums)
        saturday_sum = sum(saturday_sums)
        sunday_sum = sum(sunday_sums)

        annual_sum = (
            monday_sum
            + tuesday_sum
            + wednesday_sum
            + thursday_sum
            + friday_sum
            + saturday_sum
            + sunday_sum
        )
        days_in_year = calendar.isleap(year) * 366 + (1 - calendar.isleap(year)) * 365

        return annual_sum / (days_in_year * 24)

    @field_validator("Name")
    def validate_name(cls, v):
        """Validate the name of the schedule, specifically that it cannot be a protected name."""
        if v in PROTECTED_SCHEDULE_NAMES or "Activity_Schedule" in v:
            msg = f"Schedule name {v} is protected, please choose another name."
            raise ValueError(msg)
        return v

AverageValue property #

Get the average value of the year.

MonthlyAverageValues property #

Get the average values of the year.

Weeks property #

Get the weeks of the year as a list.

bounds property #

Get the bounds of the year.

schedule_type_limits property #

Get the schedule type limits for the year.

add_year_to_idf(idf, name_prefix=None, summer_design_day_sch_name=None, winter_design_day_sch_name=None) #

Add the year to the IDF.

The name prefix can be used to scope the schedule creation to ensure a unique schedule per object.

Parameters:

Name Type Description Default
idf IDF

The IDF object to add the year to.

required
name_prefix str | None

The prefix to use for the schedule name.

None
summer_design_day_sch_name str | None

The name of the summer design day schedule.

None
winter_design_day_sch_name str | None

The name of the winter design day schedule.

None

Returns:

Name Type Description
idf IDF

The IDF object with the year added.

year_name str

The name of the year schedule.

Source code in epinterface/sbem/components/schedules.py
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
def add_year_to_idf(
    self,
    idf: IDF,
    name_prefix: str | None = None,
    summer_design_day_sch_name: str | None = None,
    winter_design_day_sch_name: str | None = None,
):
    """Add the year to the IDF.

    The name prefix can be used to scope the schedule creation to ensure a unique schedule per object.

    Args:
        idf (IDF): The IDF object to add the year to.
        name_prefix (str | None): The prefix to use for the schedule name.
        summer_design_day_sch_name (str | None): The name of the summer design day schedule.
        winter_design_day_sch_name (str | None): The name of the winter design day schedule.

    Returns:
        idf (IDF): The IDF object with the year added.
        year_name (str): The name of the year schedule.
    """
    desired_name = self.Name
    if name_prefix is not None:
        desired_name = f"{name_prefix}_YEAR_{desired_name}"

    if idf.getobject("SCHEDULE:YEAR", desired_name):
        return idf, desired_name

    idf, jan_name = self.January.add_week_to_idf(
        idf,
        name_prefix,
        summer_design_day_sch_name=summer_design_day_sch_name,
        winter_design_day_sch_name=winter_design_day_sch_name,
    )
    idf, feb_name = self.February.add_week_to_idf(
        idf,
        name_prefix,
        summer_design_day_sch_name=summer_design_day_sch_name,
        winter_design_day_sch_name=winter_design_day_sch_name,
    )
    idf, mar_name = self.March.add_week_to_idf(
        idf,
        name_prefix,
        summer_design_day_sch_name=summer_design_day_sch_name,
        winter_design_day_sch_name=winter_design_day_sch_name,
    )
    idf, apr_name = self.April.add_week_to_idf(
        idf,
        name_prefix,
        summer_design_day_sch_name=summer_design_day_sch_name,
        winter_design_day_sch_name=winter_design_day_sch_name,
    )
    idf, may_name = self.May.add_week_to_idf(
        idf,
        name_prefix,
        summer_design_day_sch_name=summer_design_day_sch_name,
        winter_design_day_sch_name=winter_design_day_sch_name,
    )
    idf, jun_name = self.June.add_week_to_idf(
        idf,
        name_prefix,
        summer_design_day_sch_name=summer_design_day_sch_name,
        winter_design_day_sch_name=winter_design_day_sch_name,
    )
    idf, jul_name = self.July.add_week_to_idf(
        idf,
        name_prefix,
        summer_design_day_sch_name=summer_design_day_sch_name,
        winter_design_day_sch_name=winter_design_day_sch_name,
    )
    idf, aug_name = self.August.add_week_to_idf(
        idf,
        name_prefix,
        summer_design_day_sch_name=summer_design_day_sch_name,
        winter_design_day_sch_name=winter_design_day_sch_name,
    )
    idf, sep_name = self.September.add_week_to_idf(
        idf,
        name_prefix,
        summer_design_day_sch_name=summer_design_day_sch_name,
        winter_design_day_sch_name=winter_design_day_sch_name,
    )
    idf, oct_name = self.October.add_week_to_idf(
        idf,
        name_prefix,
        summer_design_day_sch_name=summer_design_day_sch_name,
        winter_design_day_sch_name=winter_design_day_sch_name,
    )
    idf, nov_name = self.November.add_week_to_idf(
        idf,
        name_prefix,
        summer_design_day_sch_name=summer_design_day_sch_name,
        winter_design_day_sch_name=winter_design_day_sch_name,
    )
    idf, dec_name = self.December.add_week_to_idf(
        idf,
        name_prefix,
        summer_design_day_sch_name=summer_design_day_sch_name,
        winter_design_day_sch_name=winter_design_day_sch_name,
    )
    year_sched = ScheduleYear(
        Name=self.Name,
        Schedule_Type_Limits_Name=self.schedule_type_limits,
        ScheduleWeek_Name_1=jan_name,
        Start_Month_1=1,
        Start_Day_1=1,
        End_Month_1=1,
        End_Day_1=31,
        ScheduleWeek_Name_2=feb_name,
        Start_Month_2=2,
        Start_Day_2=1,
        End_Month_2=2,
        End_Day_2=28,
        ScheduleWeek_Name_3=mar_name,
        Start_Month_3=3,
        Start_Day_3=1,
        End_Month_3=3,
        End_Day_3=31,
        ScheduleWeek_Name_4=apr_name,
        Start_Month_4=4,
        Start_Day_4=1,
        End_Month_4=4,
        End_Day_4=30,
        ScheduleWeek_Name_5=may_name,
        Start_Month_5=5,
        Start_Day_5=1,
        End_Month_5=5,
        End_Day_5=31,
        ScheduleWeek_Name_6=jun_name,
        Start_Month_6=6,
        Start_Day_6=1,
        End_Month_6=6,
        End_Day_6=30,
        ScheduleWeek_Name_7=jul_name,
        Start_Month_7=7,
        Start_Day_7=1,
        End_Month_7=7,
        End_Day_7=31,
        ScheduleWeek_Name_8=aug_name,
        Start_Month_8=8,
        Start_Day_8=1,
        End_Month_8=8,
        End_Day_8=31,
        ScheduleWeek_Name_9=sep_name,
        Start_Month_9=9,
        Start_Day_9=1,
        End_Month_9=9,
        End_Day_9=30,
        ScheduleWeek_Name_10=oct_name,
        Start_Month_10=10,
        Start_Day_10=1,
        End_Month_10=10,
        End_Day_10=31,
        ScheduleWeek_Name_11=nov_name,
        Start_Month_11=11,
        Start_Day_11=1,
        End_Month_11=11,
        End_Day_11=30,
        ScheduleWeek_Name_12=dec_name,
        Start_Month_12=12,
        Start_Day_12=1,
        End_Month_12=12,
        End_Day_12=31,
    )
    idf = year_sched.add(idf)

    type_lim = self.schedule_type_limits
    if not idf.getobject("SCHEDULETYPELIMITS", type_lim):
        if type_lim not in TypeLimits:
            msg = f"Type {type_lim} not in TypeLimits, unsure how to add to IDF."
            raise ValueError(msg)
        lim = TypeLimits[type_lim]
        lim.add(idf)

    return idf, year_sched.Name

check_weeks_have_consistent_type() #

Check that the weeks have a consistent type.

Source code in epinterface/sbem/components/schedules.py
386
387
388
389
390
391
392
393
394
395
@model_validator(mode="after")
def check_weeks_have_consistent_type(self):
    """Check that the weeks have a consistent type."""
    lim = self.January.Type
    for week in self.Weeks:
        if week.Type != lim:
            msg = "Type limits are not consistent"
            raise ValueError(msg)

    return self

fractional_year_sum(year) #

Compute the sum of the year as a fraction of the year.

Source code in epinterface/sbem/components/schedules.py
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
def fractional_year_sum(self, year: int):
    """Compute the sum of the year as a fraction of the year."""
    if self.schedule_type_limits != "Fraction":
        msg = "Schedule type limits are not Fraction, cannot compute year sum."
        raise ValueError(msg)

    # get the numer of Mondays in each month, tuesdays in each month, etc.

    def get_num_days_in_month(
        month: Literal[
            "January",
            "February",
            "March",
            "April",
            "May",
            "June",
            "July",
            "August",
            "September",
            "October",
            "November",
            "December",
        ],
        year: int,
        day_of_week: Literal[
            "Monday",
            "Tuesday",
            "Wednesday",
            "Thursday",
            "Friday",
            "Saturday",
            "Sunday",
        ],
    ):
        """Get the number of days in a month for a given year and day of the week."""
        # get the number of days in the month
        month_map = {
            "January": 1,
            "February": 2,
            "March": 3,
            "April": 4,
            "May": 5,
            "June": 6,
            "July": 7,
            "August": 8,
            "September": 9,
            "October": 10,
            "November": 11,
            "December": 12,
        }
        _weekday, num_days = calendar.monthrange(year, month_map[month])

        # get the number of days of the week in the month
        days_of_week = [calendar.day_name[(day + 1) % 7] for day in range(num_days)]
        return days_of_week.count(day_of_week)

    # get the number of Mondays in each month
    months: list[
        Literal[
            "January",
            "February",
            "March",
            "April",
            "May",
            "June",
            "July",
            "August",
            "September",
            "October",
            "November",
            "December",
        ]
    ] = [
        "January",
        "February",
        "March",
        "April",
        "May",
        "June",
        "July",
        "August",
        "September",
        "October",
        "November",
        "December",
    ]
    num_mondays = [get_num_days_in_month(month, year, "Monday") for month in months]
    num_tuesdays = [
        get_num_days_in_month(month, year, "Tuesday") for month in months
    ]
    num_wednesdays = [
        get_num_days_in_month(month, year, "Wednesday") for month in months
    ]
    num_thursdays = [
        get_num_days_in_month(month, year, "Thursday") for month in months
    ]
    num_fridays = [get_num_days_in_month(month, year, "Friday") for month in months]
    num_saturdays = [
        get_num_days_in_month(month, year, "Saturday") for month in months
    ]
    num_sundays = [get_num_days_in_month(month, year, "Sunday") for month in months]

    monday_sums = [
        sum(getattr(self, month).Monday.Values) * num_days
        for month, num_days in zip(months, num_mondays, strict=True)
    ]
    tuesday_sums = [
        sum(getattr(self, month).Tuesday.Values) * num_days
        for month, num_days in zip(months, num_tuesdays, strict=True)
    ]
    wednesday_sums = [
        sum(getattr(self, month).Wednesday.Values) * num_days
        for month, num_days in zip(months, num_wednesdays, strict=True)
    ]
    thursday_sums = [
        sum(getattr(self, month).Thursday.Values) * num_days
        for month, num_days in zip(months, num_thursdays, strict=True)
    ]
    friday_sums = [
        sum(getattr(self, month).Friday.Values) * num_days
        for month, num_days in zip(months, num_fridays, strict=True)
    ]
    saturday_sums = [
        sum(getattr(self, month).Saturday.Values) * num_days
        for month, num_days in zip(months, num_saturdays, strict=True)
    ]
    sunday_sums = [
        sum(getattr(self, month).Sunday.Values) * num_days
        for month, num_days in zip(months, num_sundays, strict=True)
    ]

    monday_sum = sum(monday_sums)
    tuesday_sum = sum(tuesday_sums)
    wednesday_sum = sum(wednesday_sums)
    thursday_sum = sum(thursday_sums)
    friday_sum = sum(friday_sums)
    saturday_sum = sum(saturday_sums)
    sunday_sum = sum(sunday_sums)

    annual_sum = (
        monday_sum
        + tuesday_sum
        + wednesday_sum
        + thursday_sum
        + friday_sum
        + saturday_sum
        + sunday_sum
    )
    days_in_year = calendar.isleap(year) * 366 + (1 - calendar.isleap(year)) * 365

    return annual_sum / (days_in_year * 24)

validate_name(v) #

Validate the name of the schedule, specifically that it cannot be a protected name.

Source code in epinterface/sbem/components/schedules.py
730
731
732
733
734
735
736
@field_validator("Name")
def validate_name(cls, v):
    """Validate the name of the schedule, specifically that it cannot be a protected name."""
    if v in PROTECTED_SCHEDULE_NAMES or "Activity_Schedule" in v:
        msg = f"Schedule name {v} is protected, please choose another name."
        raise ValueError(msg)
    return v

Zones#

Zone components.

ZoneComponent #

Bases: NamedObject

Zone definition.

Source code in epinterface/sbem/components/zones.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class ZoneComponent(NamedObject):
    """Zone definition."""

    Operations: ZoneOperationsComponent
    Envelope: ZoneEnvelopeComponent

    def add_to_idf_zone(self, idf: IDF, zone_name: str) -> IDF:
        """Add the zone to the IDF."""
        # TODO: consider lifting up the individual loads since the pass through method here
        # does not actually handle all of space use, given that it must synthesize
        # DHW and Operations.SpaceUse.WaterUse
        idf = self.Operations.SpaceUse.add_loads_to_idf_zone(idf, zone_name)
        idf = self.Operations.add_water_use_to_idf_zone(idf, zone_name)
        idf = self.Operations.add_conditioning_to_idf_zone(idf, zone_name)
        idf = self.Envelope.Infiltration.add_infiltration_to_idf_zone(idf, zone_name)
        return idf

add_to_idf_zone(idf, zone_name) #

Add the zone to the IDF.

Source code in epinterface/sbem/components/zones.py
16
17
18
19
20
21
22
23
24
25
def add_to_idf_zone(self, idf: IDF, zone_name: str) -> IDF:
    """Add the zone to the IDF."""
    # TODO: consider lifting up the individual loads since the pass through method here
    # does not actually handle all of space use, given that it must synthesize
    # DHW and Operations.SpaceUse.WaterUse
    idf = self.Operations.SpaceUse.add_loads_to_idf_zone(idf, zone_name)
    idf = self.Operations.add_water_use_to_idf_zone(idf, zone_name)
    idf = self.Operations.add_conditioning_to_idf_zone(idf, zone_name)
    idf = self.Envelope.Infiltration.add_infiltration_to_idf_zone(idf, zone_name)
    return idf