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
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
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=name_prefix)
        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
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
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=name_prefix)
    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
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
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")

        logger.warning(
            f"Ignoring IlluminanceTarget for zone(s) {target_zone_or_zone_list_name}."
        )
        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=name_prefix)
        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
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
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")

    logger.warning(
        f"Ignoring IlluminanceTarget for zone(s) {target_zone_or_zone_list_name}."
    )
    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=name_prefix)
    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
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)
        logger.warning(
            f"Ignoring AirspeedSchedule for zone(s) {target_zone_or_zone_list_name}."
        )

        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=name_prefix)
        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
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)
    logger.warning(
        f"Ignoring AirspeedSchedule for zone(s) {target_zone_or_zone_list_name}."
    )

    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=name_prefix)
    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
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
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",
    )
    HeatingSchedule: YearComponent = Field(..., title="Heating schedule of the object")
    CoolingSetpoint: float = Field(
        ...,
        title="Cooling setpoint of the object",
    )
    CoolingSchedule: YearComponent = Field(..., title="Cooling schedule of the object")

    def add_thermostat_to_idf_zone(
        self, idf: IDF, target_zone_or_zone_list_name: str
    ) -> IDF:
        """Add thermostat settings to an IDF zone.

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

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

        raise NotImplementedError
        idf = idf.newidfobject("HVACTEMPLATE:THERMOSTAT", **self.model_dump())
        return idf

add_thermostat_to_idf_zone(idf, target_zone_or_zone_list_name) #

Add thermostat settings to an IDF zone.

Parameters:

Name Type Description Default
idf IDF

The IDF object to add the thermostat settings to.

required
target_zone_or_zone_list_name str

The name of the zone or zone list to add the thermostat settings to.

required

Returns:

Name Type Description
IDF IDF

The updated IDF object.

Source code in epinterface\sbem\components\space_use.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
def add_thermostat_to_idf_zone(
    self, idf: IDF, target_zone_or_zone_list_name: str
) -> IDF:
    """Add thermostat settings to an IDF zone.

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

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

    raise NotImplementedError
    idf = idf.newidfobject("HVACTEMPLATE:THERMOSTAT", **self.model_dump())
    return idf

WaterUseComponent #

Bases: NamedObject, MetadataMixin

A water use object in the SBEM library.

Source code in epinterface\sbem\components\space_use.py
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
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
    )
    Schedule: YearComponent = Field(..., title="Water schedule")

    @property
    def schedule_names(self) -> set[str]:
        """Get the schedule names used in the object.

        Returns:
            set[str]: The schedule names.
        """
        raise NotImplementedError
        return {self.WaterSchedule} if self.IsOn else set()

schedule_names: set[str] property #

Get the schedule names used in the object.

Returns:

Type Description
set[str]

set[str]: The schedule names.

ZoneSpaceUseComponent #

Bases: NamedObject, MetadataMixin

Space use object.

Source code in epinterface\sbem\components\space_use.py
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
class ZoneSpaceUseComponent(NamedObject, MetadataMixin, extra="forbid"):
    """Space use object."""

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

    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
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
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
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
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

    def add_water_to_idf_zone(
        self, idf: IDF, target_zone_name: str, total_ppl: float
    ) -> IDF:
        """Add the hot water to an IDF zone.

        Args:
            idf (IDF): The IDF object to add the hot water to.
            target_zone_name (str): The name of the zone to add the hot water to.
            total_ppl (float): The total number of people in the zone.

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

        total_flow_rate = self.FlowRatePerPerson * total_ppl  # m3/day
        total_flow_rate_per_s = total_flow_rate / (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)

        target_temperature_schedule = Schedule.constant_schedule(
            value=self.WaterSupplyTemperature,  # pyright: ignore [reportArgumentType]
            Name=f"{target_zone_name}_{self.Name}_TargetWaterTemperatureSch",
            Type="Temperature",
        )
        inlet_temperature_schedule = Schedule.constant_schedule(
            value=self.WaterTemperatureInlet,  # pyright: ignore [reportArgumentType]
            Name=f"{target_zone_name}_{self.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)

        hot_water = WaterUseEquipment(
            Name=f"{target_zone_name}_{self.Name}_HotWater",
            EndUse_Subcategory="Domestic Hot Water",
            Peak_Flow_Rate=total_flow_rate_per_s,  # TODO: Update this to actual peak rate?
            Flow_Rate_Fraction_Schedule_Name=self.WaterSchedule,
            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

    @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: float property #

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

Returns:

Name Type Description
cop float

The effective system COP.

add_water_to_idf_zone(idf, target_zone_name, total_ppl) #

Add the hot water to an IDF zone.

Parameters:

Name Type Description Default
idf IDF

The IDF object to add the hot water to.

required
target_zone_name str

The name of the zone to add the hot water to.

required
total_ppl float

The total number of people in the zone.

required

Returns:

Name Type Description
IDF IDF

The updated IDF object.

Source code in epinterface\sbem\components\systems.py
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
def add_water_to_idf_zone(
    self, idf: IDF, target_zone_name: str, total_ppl: float
) -> IDF:
    """Add the hot water to an IDF zone.

    Args:
        idf (IDF): The IDF object to add the hot water to.
        target_zone_name (str): The name of the zone to add the hot water to.
        total_ppl (float): The total number of people in the zone.

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

    total_flow_rate = self.FlowRatePerPerson * total_ppl  # m3/day
    total_flow_rate_per_s = total_flow_rate / (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)

    target_temperature_schedule = Schedule.constant_schedule(
        value=self.WaterSupplyTemperature,  # pyright: ignore [reportArgumentType]
        Name=f"{target_zone_name}_{self.Name}_TargetWaterTemperatureSch",
        Type="Temperature",
    )
    inlet_temperature_schedule = Schedule.constant_schedule(
        value=self.WaterTemperatureInlet,  # pyright: ignore [reportArgumentType]
        Name=f"{target_zone_name}_{self.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)

    hot_water = WaterUseEquipment(
        Name=f"{target_zone_name}_{self.Name}_HotWater",
        EndUse_Subcategory="Domestic Hot Water",
        Peak_Flow_Rate=total_flow_rate_per_s,  # TODO: Update this to actual peak rate?
        Flow_Rate_Fraction_Schedule_Name=self.WaterSchedule,
        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

validate_supply_greater_than_inlet() #

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

Source code in epinterface\sbem\components\systems.py
211
212
213
214
215
216
217
@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: 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: DistributionType property #

Compute the distribution type based on the system COP.

Returns:

Name Type Description
distribution_type DistributionType

The distribution type.

HeatingSystemType: 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: float 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
127
128
129
130
131
132
133
134
135
136
137
138
class VentilationComponent(NamedObject, MetadataMixin, extra="forbid"):
    """A ventilation object in the SBEM library."""

    # TODO: add unit notes in field descriptions
    Rate: float = Field(..., title="Ventilation rate of the object")
    MinFreshAir: float = Field(
        ...,
        title="Minimum fresh air of the object [m³/s]",
    )
    Schedule: YearComponent = Field(..., title="Ventilation schedule of the object")
    Type: VentilationType = Field(..., title="Type of the object")
    TechType: VentilationTechType = Field(..., title="Technology type of the object")

ZoneHVACComponent #

Bases: NamedObject, MetadataMixin

Conditioning object in the SBEM library.

Source code in epinterface\sbem\components\systems.py
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
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: VentilationComponent 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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
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
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class ZoneOperationsComponent(
    NamedObject,
    MetadataMixin,
    extra="forbid",
):
    """Zone use consolidation across space use, HVAC, DHW."""

    SpaceUse: ZoneSpaceUseComponent
    HVAC: ZoneHVACComponent
    DHW: DHWComponent

    def add_operations_to_idf_zone(
        self, idf: IDF, target_zone_name: str, zone_area: float
    ) -> IDF:
        """Add an entire operations component and its subchildren to an IDF zone.

        NB: the area is required because DHW needs it when computing flow rates.

        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.
            zone_area (float): The area of the zone.

        Returns:
            idf (IDF): The updated IDF object.
        """
        # TODO: remember to add schedules!
        self.SpaceUse.add_loads_to_idf_zone(idf, target_zone_name)
        self.HVAC.add_conditioning_to_idf_zone(idf, target_zone_name)
        total_people = self.SpaceUse.Occupancy.PeopleDensity * zone_area
        self.DHW.add_water_to_idf_zone(idf, target_zone_name, total_ppl=total_people)
        return idf

add_operations_to_idf_zone(idf, target_zone_name, zone_area) #

Add an entire operations component and its subchildren to an IDF zone.

NB: the area is required because DHW needs it when computing flow rates.

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
zone_area float

The area of the zone.

required

Returns:

Name Type Description
idf IDF

The updated IDF object.

Source code in epinterface\sbem\components\operations.py
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def add_operations_to_idf_zone(
    self, idf: IDF, target_zone_name: str, zone_area: float
) -> IDF:
    """Add an entire operations component and its subchildren to an IDF zone.

    NB: the area is required because DHW needs it when computing flow rates.

    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.
        zone_area (float): The area of the zone.

    Returns:
        idf (IDF): The updated IDF object.
    """
    # TODO: remember to add schedules!
    self.SpaceUse.add_loads_to_idf_zone(idf, target_zone_name)
    self.HVAC.add_conditioning_to_idf_zone(idf, target_zone_name)
    total_people = self.SpaceUse.Occupancy.PeopleDensity * zone_area
    self.DHW.add_water_to_idf_zone(idf, target_zone_name, total_ppl=total_people)
    return idf

Envelope#

Envelope components for the SBEM library.

ConstructionAssemblyComponent #

Bases: NamedObject, MetadataMixin

Opaque construction object.

Source code in epinterface\sbem\components\envelope.py
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
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)
        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

reversed property #

Return a reversed version of the construction.

sorted_layers property #

Return the layers of the construction sorted by layer order.

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
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
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
199
200
201
202
203
204
205
206
207
208
209
210
@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)
    return v

ConstructionLayerComponent #

Bases: BaseModel

Layer of an opaque construction.

Source code in epinterface\sbem\components\envelope.py
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
class ConstructionLayerComponent(BaseModel, extra="forbid"):
    """Layer of an opaque construction."""

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

    @property
    def name(self):
        """Return the name of the layer."""
        return f"{self.LayerOrder}_{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,
        )

ep_material property #

Return the EP material for the layer.

name property #

Return the name of the layer.

EnvelopeAssemblyComponent #

Bases: NamedObject, MetadataMixin

Zone construction object.

Source code in epinterface\sbem\components\envelope.py
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
class EnvelopeAssemblyComponent(
    NamedObject,
    MetadataMixin,
    extra="forbid",
):
    """Zone construction object."""

    RoofAssembly: ConstructionAssemblyComponent = Field(
        ..., title="Roof construction object name"
    )
    FacadeAssembly: ConstructionAssemblyComponent = Field(
        ..., title="Facade construction object name"
    )
    SlabAssembly: ConstructionAssemblyComponent = Field(
        ..., title="Slab construction object name"
    )
    PartitionAssembly: ConstructionAssemblyComponent = Field(
        ..., title="Partition construction object name"
    )
    ExternalFloorAssembly: ConstructionAssemblyComponent = Field(
        ..., title="External floor construction object name"
    )
    GroundSlabAssembly: ConstructionAssemblyComponent = Field(
        ..., title="Ground slab construction object name"
    )
    GroundWallAssembly: ConstructionAssemblyComponent = Field(
        ..., title="Ground wall construction object name"
    )
    InternalMassAssembly: ConstructionAssemblyComponent | None = Field(
        default=None, title="Internal mass construction object name"
    )
    InternalMassExposedAreaPerArea: float | None = Field(
        default=None,
        title="Internal mass exposed area per area [m²/m²]",
        ge=0,
    )
    GroundIsAdiabatic: BoolStr = Field(..., title="Ground is adiabatic")
    RoofIsAdiabatic: BoolStr = Field(..., title="Roof is adiabatic")
    FacadeIsAdiabatic: BoolStr = Field(..., title="Facade is adiabatic")
    SlabIsAdiabatic: BoolStr = Field(..., title="Slab is adiabatic")
    PartitionIsAdiabatic: BoolStr = Field(..., title="Partition is adiabatic")

    @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:
            msg = "Internal mass assembly must be provided if internal mass exposed area per area is provided"
            raise ValueError(msg)
        if (
            self.InternalMassExposedAreaPerArea is not None
            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
290
291
292
293
294
295
296
297
298
299
300
301
302
@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:
        msg = "Internal mass assembly must be provided if internal mass exposed area per area is provided"
        raise ValueError(msg)
    if (
        self.InternalMassExposedAreaPerArea is not None
        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
class InfiltrationComponent(
    NamedObject,
    MetadataMixin,
    extra="forbid",
):
    """Zone infiltration object."""

    # TODO: add assumed_constants
    IsOn: BoolStr = Field(..., title="Infiltration is on")
    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.Name}_Infiltration_Schedule"
        )
        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=f"{target_zone_or_zone_list_name}_{self.Name}_Infiltration",
            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
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
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.Name}_Infiltration_Schedule"
    )
    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=f"{target_zone_or_zone_list_name}_{self.Name}_Infiltration",
        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
305
306
307
308
309
310
class ZoneEnvelopeComponent(NamedObject, MetadataMixin, extra="forbid"):
    """Zone envelope object."""

    Assemblies: EnvelopeAssemblyComponent
    Infiltration: 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
124
125
126
127
128
129
130
131
132
133
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
 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
class ConstructionMaterialProperties(
    CommonMaterialPropertiesMixin,
    extra="forbid",
):
    """Properties of an opaque material."""

    # add in the commonMaterialsPropertis
    Roughness: str = 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
 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
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 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

        # check that the values are consistent with the schedule type limit

        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.
        """
        day_sched = ScheduleDayHourly(
            Name=self.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,
        )
        if name_prefix is not None:
            day_sched.Name = f"{name_prefix}_DAY_{day_sched.Name}"
        idf = day_sched.add(idf)
        return idf, day_sched.Name

Values: list[float] property #

Get the values of the day as a list.

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
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
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.
    """
    day_sched = ScheduleDayHourly(
        Name=self.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,
    )
    if name_prefix is not None:
        day_sched.Name = f"{name_prefix}_DAY_{day_sched.Name}"
    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
101
102
103
104
105
106
107
108
@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

    # check that the values are consistent with the schedule type limit

    return self

WeekComponent #

Bases: NamedObject

A week with a list of days.

Source code in epinterface\sbem\components\schedules.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
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 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) -> 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.

        Returns:
            idf (IDF): The IDF object with the week added.
            week_name (str): The name of the week schedule.
        """
        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=self.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,
        )
        if name_prefix is not None:
            week_sched.Name = f"{name_prefix}_WEEK_{week_sched.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

Days: list[DayComponent] property #

Get the days of the week as a list.

Type: ScheduleTypeLimitType property #

Get the type limit of the week.

add_week_to_idf(idf, name_prefix) #

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

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
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
def add_week_to_idf(self, idf: IDF, name_prefix: str | 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.

    Returns:
        idf (IDF): The IDF object with the week added.
        week_name (str): The name of the week schedule.
    """
    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=self.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,
    )
    if name_prefix is not None:
        week_sched.Name = f"{name_prefix}_WEEK_{week_sched.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
168
169
170
171
172
173
174
175
176
@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
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
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
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 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):
        """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.

        Returns:
            idf (IDF): The IDF object with the year added.
            year_name (str): The name of the year schedule.
        """
        idf, jan_name = self.January.add_week_to_idf(idf, name_prefix)
        idf, feb_name = self.February.add_week_to_idf(idf, name_prefix)
        idf, mar_name = self.March.add_week_to_idf(idf, name_prefix)
        idf, apr_name = self.April.add_week_to_idf(idf, name_prefix)
        idf, may_name = self.May.add_week_to_idf(idf, name_prefix)
        idf, jun_name = self.June.add_week_to_idf(idf, name_prefix)
        idf, jul_name = self.July.add_week_to_idf(idf, name_prefix)
        idf, aug_name = self.August.add_week_to_idf(idf, name_prefix)
        idf, sep_name = self.September.add_week_to_idf(idf, name_prefix)
        idf, oct_name = self.October.add_week_to_idf(idf, name_prefix)
        idf, nov_name = self.November.add_week_to_idf(idf, name_prefix)
        idf, dec_name = self.December.add_week_to_idf(idf, name_prefix)
        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,
        )
        if name_prefix is not None:
            year_sched.Name = f"{name_prefix}_YEAR_{year_sched.Name}"
        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

Weeks: list[WeekComponent] property #

Get the weeks of the year as a list.

schedule_type_limits property #

Get the schedule type limits for the year.

add_year_to_idf(idf, name_prefix=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

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
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
def add_year_to_idf(self, idf: IDF, name_prefix: 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.

    Returns:
        idf (IDF): The IDF object with the year added.
        year_name (str): The name of the year schedule.
    """
    idf, jan_name = self.January.add_week_to_idf(idf, name_prefix)
    idf, feb_name = self.February.add_week_to_idf(idf, name_prefix)
    idf, mar_name = self.March.add_week_to_idf(idf, name_prefix)
    idf, apr_name = self.April.add_week_to_idf(idf, name_prefix)
    idf, may_name = self.May.add_week_to_idf(idf, name_prefix)
    idf, jun_name = self.June.add_week_to_idf(idf, name_prefix)
    idf, jul_name = self.July.add_week_to_idf(idf, name_prefix)
    idf, aug_name = self.August.add_week_to_idf(idf, name_prefix)
    idf, sep_name = self.September.add_week_to_idf(idf, name_prefix)
    idf, oct_name = self.October.add_week_to_idf(idf, name_prefix)
    idf, nov_name = self.November.add_week_to_idf(idf, name_prefix)
    idf, dec_name = self.December.add_week_to_idf(idf, name_prefix)
    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,
    )
    if name_prefix is not None:
        year_sched.Name = f"{name_prefix}_YEAR_{year_sched.Name}"
    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
319
320
321
322
323
324
325
326
327
328
@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

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
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."""
        idf = self.Operations.SpaceUse.add_loads_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
def add_to_idf_zone(self, idf: IDF, zone_name: str) -> IDF:
    """Add the zone to the IDF."""
    idf = self.Operations.SpaceUse.add_loads_to_idf_zone(idf, zone_name)
    return idf