epinterface 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. epinterface/__init__.py +1 -0
  2. epinterface/actions.py +382 -0
  3. epinterface/builder.py +310 -0
  4. epinterface/cli.py +284 -0
  5. epinterface/climate_studio/__init__.py +1 -0
  6. epinterface/climate_studio/builder.py +634 -0
  7. epinterface/climate_studio/interface.py +2159 -0
  8. epinterface/constants/__init__.py +73 -0
  9. epinterface/data/Minimal.idf +164 -0
  10. epinterface/data/USA_MA_Boston-Logan.Intl.AP.725090_TMYx.2009-2023.ddy +3353 -0
  11. epinterface/data/USA_MA_Boston-Logan.Intl.AP.725090_TMYx.2009-2023.epw +8768 -0
  12. epinterface/data/USA_MA_Boston-Logan.Intl.AP.725090_TMYx.2009-2023.zip +0 -0
  13. epinterface/data/__init__.py +18 -0
  14. epinterface/data/res_schedules.parquet +0 -0
  15. epinterface/ddy_injector_bayes.py +330 -0
  16. epinterface/ddy_interface_bayes.py +330 -0
  17. epinterface/geometry.py +724 -0
  18. epinterface/interface.py +1080 -0
  19. epinterface/py.typed +0 -0
  20. epinterface/sbem/__init__.py +1 -0
  21. epinterface/sbem/annotations.py +57 -0
  22. epinterface/sbem/builder.py +1350 -0
  23. epinterface/sbem/common.py +38 -0
  24. epinterface/sbem/components/__init__.py +1 -0
  25. epinterface/sbem/components/composer.py +388 -0
  26. epinterface/sbem/components/envelope.py +367 -0
  27. epinterface/sbem/components/materials.py +144 -0
  28. epinterface/sbem/components/operations.py +342 -0
  29. epinterface/sbem/components/schedules.py +716 -0
  30. epinterface/sbem/components/space_use.py +270 -0
  31. epinterface/sbem/components/systems.py +270 -0
  32. epinterface/sbem/components/zones.py +25 -0
  33. epinterface/sbem/exceptions.py +101 -0
  34. epinterface/sbem/fields/__init__.py +1 -0
  35. epinterface/sbem/fields/spec.py +95 -0
  36. epinterface/sbem/flat_model.py +2210 -0
  37. epinterface/sbem/interface.py +590 -0
  38. epinterface/sbem/model_graph_structure.md +31 -0
  39. epinterface/sbem/prisma/client.py +650 -0
  40. epinterface/sbem/prisma/migrations/20250309133033_create_initial_schema/migration.sql +443 -0
  41. epinterface/sbem/prisma/migrations/20250310162045_switch_to_using_one_week_per_month/migration.sql +61 -0
  42. epinterface/sbem/prisma/migrations/20250317202733_change_names_ventilation/migration.sql +30 -0
  43. epinterface/sbem/prisma/migrations/20250325185158_add_mutually_exclusive_ventilation_techtypes/migration.sql +32 -0
  44. epinterface/sbem/prisma/migrations/20250326141941_reduce_naming_complexity_ventilation/migration.sql +37 -0
  45. epinterface/sbem/prisma/migrations/20250331141910_add_support_for_attic_and_basement_constructions/migration.sql +74 -0
  46. epinterface/sbem/prisma/migrations/20250919152559_decouple_basement_infiltration/migration.sql +31 -0
  47. epinterface/sbem/prisma/migrations/migration_lock.toml +3 -0
  48. epinterface/sbem/prisma/partial_types.py +196 -0
  49. epinterface/sbem/prisma/schema.prisma +582 -0
  50. epinterface/sbem/prisma/seed_fns.py +732 -0
  51. epinterface/sbem/utils.py +74 -0
  52. epinterface/weather.py +138 -0
  53. epinterface-1.0.0.dist-info/METADATA +70 -0
  54. epinterface-1.0.0.dist-info/RECORD +57 -0
  55. epinterface-1.0.0.dist-info/WHEEL +4 -0
  56. epinterface-1.0.0.dist-info/entry_points.txt +3 -0
  57. epinterface-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1 @@
1
+ """A library for interfacing with EnergyPlus."""
epinterface/actions.py ADDED
@@ -0,0 +1,382 @@
1
+ """Actions to modify a library object."""
2
+
3
+ from abc import abstractmethod
4
+ from collections.abc import Callable
5
+ from functools import reduce
6
+ from typing import Any, Generic, Literal, TypeVar, cast
7
+
8
+ from pydantic import BaseModel, Field, model_validator
9
+
10
+ from epinterface.climate_studio.builder import Model
11
+ from epinterface.climate_studio.interface import ClimateStudioLibraryV2
12
+
13
+ LibT = TypeVar("LibT", dict[str, Any], ClimateStudioLibraryV2, Model, BaseModel)
14
+
15
+ # TODO: Major!
16
+ # allow operating on an object in conjunction with operating on a library
17
+ # TODO: allow callbacks in path reduction, e.g. to compute a value like "layer with most insulation"
18
+
19
+
20
+ def get_dict_val_or_attr(obj, key):
21
+ """Retrieve a value from a dictionary or list, or an attribute from an object.
22
+
23
+ Args:
24
+ obj (Union[dict, list, Any]): The object from which to retrieve the value or attribute.
25
+ key (Any): The key or attribute name to retrieve.
26
+
27
+ Returns:
28
+ val (Any): The value associated with the key if `obj` is a dictionary or list,
29
+ or the attribute value if `obj` is an object.
30
+ """
31
+ if isinstance(obj, dict | list):
32
+ return obj[key]
33
+ else:
34
+ return getattr(obj, key)
35
+
36
+
37
+ def set_dict_val_or_attr(obj, key, val):
38
+ """Sets a value in a dictionary or list, or sets an attribute on an object.
39
+
40
+ If the provided object is a dictionary or list, the function sets the value
41
+ at the specified key or index. If the object is not a dictionary or list,
42
+ the function sets an attribute on the object with the specified key and value.
43
+
44
+ Args:
45
+ obj (Union[dict, list, object]): The object to modify.
46
+ key (Union[str, int]): The key or attribute name to set.
47
+ val (Any): The value to set.
48
+
49
+ Raises:
50
+ TypeError: If the object is a list and the key is not an integer.
51
+ """
52
+ if isinstance(obj, dict | list):
53
+ obj[key] = val
54
+ else:
55
+ setattr(obj, key, val)
56
+
57
+
58
+ T = TypeVar("T")
59
+
60
+
61
+ class ParameterPath(BaseModel, Generic[T]):
62
+ """Pathing to find a parameter in a library/object.
63
+
64
+ ParameterPath is a generic class that represents a path consisting of strings, integers,
65
+ or other ParameterPath instances. It provides methods to resolve the path and retrieve
66
+ values from a given library.
67
+ """
68
+
69
+ path: list["str | int | ParameterPath"] = Field(
70
+ ..., description="The path to the parameter to select."
71
+ )
72
+
73
+ def resolved_path(self, lib: LibT):
74
+ """Resolve the path to the parameter in the library.
75
+
76
+ Args:
77
+ lib (LibT): The library to search for the parameter.
78
+
79
+ Returns:
80
+ path (list[Any]): The resolved path to the parameter in the library.
81
+ """
82
+ return [
83
+ p if isinstance(p, str | int) else p.get_lib_val(lib) for p in self.path
84
+ ]
85
+
86
+ def get_lib_val(self, lib: LibT) -> T:
87
+ """Retrieves a value from a nested dictionary or object attribute path.
88
+
89
+ Args:
90
+ lib (LibT): The library object from which to retrieve the value.
91
+
92
+ Returns:
93
+ val (T): The value retrieved from the nested dictionary or object attribute path.
94
+ """
95
+ return cast(T, reduce(get_dict_val_or_attr, self.resolved_path(lib), lib))
96
+
97
+ @property
98
+ def parent_path(self):
99
+ """Returns the parent path of the current path.
100
+
101
+ Returns:
102
+ parent_path (ParameterPath): The parent path of the current path.
103
+ """
104
+ # TODO: how can we type-narrow the generic parameterpath here?
105
+ # get the parent using the similar reduction technique as before
106
+ return ParameterPath[Any](path=self.path[:-1])
107
+
108
+
109
+ Priority = Literal["low", "high"]
110
+
111
+
112
+ class Action(BaseModel, Generic[T]):
113
+ """An action to modify a library object.
114
+
115
+ This base class should be inherited by classes that represent actions to modify
116
+ a library object. It provides an abstract method `run` that should be implemented
117
+ by subclasses to perform the modification.
118
+ """
119
+
120
+ target: ParameterPath[T] = Field(
121
+ ..., description="The path to the parameter to modify."
122
+ )
123
+ priority: Priority | None = Field(
124
+ default=None,
125
+ description="The priority of the action (low will execute if the new value is less than the old value).",
126
+ )
127
+
128
+ def run(self, lib: LibT) -> LibT:
129
+ """Run the action to modify the library object.
130
+
131
+ Args:
132
+ lib (LibT): The library object to modify.
133
+
134
+ Returns:
135
+ lib (LibT): The modified library object.
136
+ """
137
+ new_val = self.new_val(lib)
138
+ original_val = self.get_original_val(lib)
139
+ if self.check_priority(original_val, new_val):
140
+ original_obj = self.get_original_obj(lib)
141
+ key = self.original_key
142
+ set_dict_val_or_attr(original_obj, key, new_val)
143
+ return lib
144
+
145
+ def check_priority(self, original: T, new: T) -> bool:
146
+ """Check if the new value should be applied based on the action priority.
147
+
148
+ Args:
149
+ original (T): The original value in the library object.
150
+ new (T): The new value to apply.
151
+
152
+ Returns:
153
+ apply (bool): True if the new value should be applied, False otherwise.
154
+ """
155
+ if self.priority is None:
156
+ return True
157
+
158
+ if not isinstance(original, int | float) or not isinstance(new, int | float):
159
+ msg = "priority comparison only supported for numerical values."
160
+ raise TypeError(msg)
161
+
162
+ if self.priority == "low":
163
+ return original > new
164
+
165
+ elif self.priority == "high":
166
+ return original < new
167
+ else:
168
+ msg = f"Invalid priority value: {self.priority}"
169
+ raise ValueError(msg)
170
+
171
+ def get_original_val(self, lib: LibT) -> T:
172
+ """Retrieve the original value from the library object.
173
+
174
+ Args:
175
+ lib (LibT): The library object from which to retrieve the original value.
176
+
177
+ Returns:
178
+ val (T): The original value from the library object.
179
+ """
180
+ return self.target.get_lib_val(lib)
181
+
182
+ @property
183
+ def original_key(self) -> str | int | ParameterPath:
184
+ """Retrieve the key of the original value in the library object.
185
+
186
+ Returns:
187
+ key (str | int | ParameterPath): The key of the original value in the library object.
188
+ """
189
+ # TODO: handle cases where final key is a ParameterPath!!
190
+ return self.target.path[-1]
191
+
192
+ def get_original_obj(self, lib: LibT):
193
+ """Retrieve the object containing the original value in the library object.
194
+
195
+ Args:
196
+ lib (LibT): The library object from which to retrieve the original object.
197
+
198
+ Returns:
199
+ obj (Any): The object containing the original value in the library object.
200
+ """
201
+ return self.target.parent_path.get_lib_val(lib)
202
+
203
+ @abstractmethod
204
+ def new_val(self, lib: LibT) -> T:
205
+ """Calculate the new value to apply to the library object.
206
+
207
+ NB: This method should be implemented by subclasses to calculate the new value.
208
+
209
+ Args:
210
+ lib (LibT): The library object on which to apply the new value.
211
+
212
+ Returns:
213
+ val (T): The new value to apply to the library object.
214
+ """
215
+ pass
216
+
217
+
218
+ class ReplaceWithExisting(Action[T]):
219
+ """Replace a value in a library object with a value from another location in the library."""
220
+
221
+ source: ParameterPath[T]
222
+
223
+ def new_val(self, lib: LibT) -> T:
224
+ """Retrieve the value from the source path to replace the target value.
225
+
226
+ Args:
227
+ lib (LibT): The library object from which to retrieve the new value.
228
+
229
+ Returns:
230
+ val (T): The new value to replace the target value.
231
+ """
232
+ return self.source.get_lib_val(lib)
233
+
234
+
235
+ class ReplaceWithVal(Action[T]):
236
+ """Replace a value in a library object with a new value."""
237
+
238
+ val: T
239
+
240
+ def new_val(self, lib: LibT) -> T:
241
+ """Returns the current value of the instance to use for updating.
242
+
243
+ Args:
244
+ lib (LibT): A library instance of type LibT.
245
+
246
+ Returns:
247
+ val (T): The current value of the instance.
248
+ """
249
+ return self.val
250
+
251
+
252
+ Numeric = TypeVar("Numeric", int, float)
253
+ Operation = Literal["+", "*"]
254
+
255
+
256
+ class DeltaVal(Action[Numeric]):
257
+ """Add a value to a parameter in a library object."""
258
+
259
+ delta: Numeric = Field(
260
+ ..., description="The value to modify to the original value."
261
+ )
262
+ op: Operation = Field(
263
+ ..., description="The operation to perform on the original value."
264
+ )
265
+
266
+ def new_val(self, lib: LibT) -> Numeric:
267
+ """Calculate a new value by combining the original value from the given library with a delta.
268
+
269
+ Args:
270
+ lib (LibT): The library from which to retrieve the original value.
271
+
272
+ Returns:
273
+ new_val (Numeric): The new value obtained by combining the original value with the delta.
274
+ """
275
+ original_val = self.get_original_val(lib)
276
+
277
+ return self.combine(original_val, self.delta)
278
+
279
+ @property
280
+ def combine(self) -> Callable[[Numeric, Numeric], Numeric]:
281
+ """Combines two numeric values based on the specified operation.
282
+
283
+ Supported operations:
284
+ - "+": Addition
285
+ - "*": Multiplication
286
+
287
+ Returns:
288
+ fn (Callable[[Numeric, Numeric], Numeric]): A function that takes two numeric arguments and returns a numeric result.
289
+
290
+ Raises:
291
+ ValueError: If the operation specified by `self.op` is not supported.
292
+
293
+ """
294
+ if self.op == "+":
295
+ return lambda x, y: x + y
296
+ elif self.op == "*":
297
+ return lambda x, y: x * y
298
+ else:
299
+ msg = f"Invalid operation: {self.op}"
300
+ raise ValueError(msg)
301
+
302
+
303
+ class ActionSequence(BaseModel):
304
+ """A sequence of actions to perform on a library object."""
305
+
306
+ name: str = Field(..., description="The name of the action sequence.")
307
+ actions: list[
308
+ "DeltaVal | ReplaceWithExisting | ReplaceWithVal | ActionSequence"
309
+ ] = Field( # TODO: should we allow nested actionsequences?
310
+ ..., description="A sequence of actions to perform on a library object."
311
+ )
312
+
313
+ def run(self, lib: LibT) -> LibT:
314
+ """Run the sequence of actions on the library object.
315
+
316
+ Args:
317
+ lib (LibT): The library object to modify.
318
+
319
+ Returns:
320
+ lib (LibT): The modified library object.
321
+ """
322
+ for action in self.actions:
323
+ lib = action.run(lib)
324
+ return lib
325
+
326
+
327
+ class ActionLibrary(BaseModel):
328
+ """A library of action sequences, e.g. to represent deep and shallow retrofits for different types of buildings."""
329
+
330
+ name: str = Field(..., description="The name of the action library.")
331
+ actions: list[ActionSequence] = Field(
332
+ ..., description="A list of action sequences to perform on a library object."
333
+ )
334
+
335
+ @model_validator(mode="after")
336
+ def check_action_names_are_unique(self):
337
+ """Check that the names of the action sequences in the action library are unique.
338
+
339
+ Raises:
340
+ ValueError: If the names of the action sequences are not unique.
341
+ """
342
+ action_names = self.action_names
343
+ if len(action_names) != len(set(action_names)):
344
+ msg = f"Action names must be unique: {', '.join(action_names)}"
345
+ raise ValueError(msg)
346
+ return self
347
+
348
+ @property
349
+ def action_names(self):
350
+ """Return the names of the action sequences in the action library.
351
+
352
+ Returns:
353
+ action_names (list[str]): The names of the action sequences in the action
354
+ """
355
+ return [action.name for action in self.actions]
356
+
357
+ def get(self, name: str) -> ActionSequence:
358
+ """Retrieve an action sequence by name.
359
+
360
+ Args:
361
+ name (str): The name of the action sequence to retrieve.
362
+
363
+ Returns:
364
+ action (ActionSequence): The action sequence with the specified name.
365
+
366
+ Raises:
367
+ KeyError: If the action sequence with the specified name is not found in the action library.
368
+ """
369
+ if name in self.action_names:
370
+ return self.actions[self.action_names.index(name)]
371
+ else:
372
+ msg = f"Action sequence not found: {name}\nAvailable action sequences: {', '.join(self.action_names)}"
373
+ raise KeyError(msg)
374
+
375
+
376
+ ParameterPath.model_rebuild()
377
+ Action.model_rebuild()
378
+ ReplaceWithExisting.model_rebuild()
379
+ ReplaceWithVal.model_rebuild()
380
+ DeltaVal.model_rebuild()
381
+ ActionSequence.model_rebuild()
382
+ ActionLibrary.model_rebuild()
epinterface/builder.py ADDED
@@ -0,0 +1,310 @@
1
+ """A simple model for automatically constructing a shoebox building energy model."""
2
+
3
+ import logging
4
+ import shutil
5
+ from pathlib import Path
6
+
7
+ import pandas as pd
8
+ from archetypal.idfclass import IDF
9
+ from archetypal.schedule import Schedule
10
+ from archetypal.schedule import ScheduleTypeLimits as AScheduleTypeLimits
11
+
12
+ from epinterface.ddy_injector_bayes import DDYSizingSpec
13
+ from epinterface.geometry import ShoeboxGeometry
14
+ from epinterface.interface import (
15
+ Construction,
16
+ DefaultMaterialLibrary,
17
+ ElectricEquipment,
18
+ HVACTemplateThermostat,
19
+ HVACTemplateZoneIdealLoadsAirSystem,
20
+ Lights,
21
+ People,
22
+ RunPeriod,
23
+ SimpleGlazingMaterial,
24
+ SimulationControl,
25
+ Timestep,
26
+ ZoneInfiltrationDesignFlowRate,
27
+ ZoneList,
28
+ )
29
+ from epinterface.weather import BaseWeather
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class SimpleResidentialModel(BaseWeather, extra="allow"):
35
+ """A simple model for automatically constructing a shoebox building energy model."""
36
+
37
+ EPD: float = 9.0
38
+ LPD: float = 4.0
39
+ WWR: float = 0.15
40
+ WindowU: float = 2.7
41
+ WindowSHGC: float = 0.763
42
+ HeatingSetpoint: float = 19
43
+ CoolingSetpoint: float = 24
44
+ PeopleDensity: float = 0.05
45
+ Infiltration: float = 0.1
46
+ timestep: int = 6
47
+
48
+ async def build(
49
+ self, output_dir: Path | str, weather_cache_dir: Path | str | None = None
50
+ ) -> IDF:
51
+ """Build the energy model.
52
+
53
+ Args:
54
+ output_dir (Path | str): The directory to save the IDF file.
55
+ weather_cache_dir (Path | str | None): The directory to cache the weather files.
56
+
57
+ Returns:
58
+ IDF: The constructed IDF model.
59
+ """
60
+ if isinstance(output_dir, str):
61
+ output_dir = Path(output_dir)
62
+ output_dir.mkdir(parents=True, exist_ok=True)
63
+
64
+ material_lib = DefaultMaterialLibrary()
65
+ weather_cache_dir = Path(weather_cache_dir) if weather_cache_dir else output_dir
66
+ epw_path, ddy_path = await self.fetch_weather(weather_cache_dir)
67
+ schedules = pd.read_parquet(
68
+ Path(__file__).parent / "data" / "res_schedules.parquet"
69
+ )
70
+
71
+ shutil.copy(
72
+ Path(__file__).parent / "data" / "Minimal.idf",
73
+ output_dir / "Minimal.idf",
74
+ )
75
+ idf = IDF(
76
+ (output_dir / "Minimal.idf").as_posix(),
77
+ epw=(epw_path.as_posix()),
78
+ output_directory=output_dir.as_posix(),
79
+ prep_outputs=True,
80
+ ) # pyright: ignore [reportArgumentType]
81
+ ddy = IDF(
82
+ (ddy_path.as_posix()),
83
+ as_version="9.2.0",
84
+ file_version="9.2.0",
85
+ prep_outputs=False,
86
+ )
87
+ ddyspec = DDYSizingSpec(match=True)
88
+ ddyspec.inject_ddy(idf, ddy)
89
+
90
+ # Configure simulation
91
+ sim_control = SimulationControl(
92
+ Do_Zone_Sizing_Calculation="Yes",
93
+ Do_System_Sizing_Calculation="Yes",
94
+ Do_Plant_Sizing_Calculation="Yes",
95
+ Run_Simulation_for_Sizing_Periods="Yes",
96
+ Run_Simulation_for_Weather_File_Run_Periods="Yes",
97
+ Do_HVAC_Sizing_Simulation_for_Sizing_Periods="Yes",
98
+ )
99
+ sim_control.add(idf)
100
+
101
+ # Configure run period
102
+ run_period = RunPeriod(
103
+ Name="Year",
104
+ Use_Weather_File_Daylight_Saving_Period="No",
105
+ Use_Weather_File_Rain_Indicators="No",
106
+ Use_Weather_File_Snow_Indicators="No",
107
+ Use_Weather_File_Holidays_and_Special_Days="No",
108
+ Begin_Month=1,
109
+ Begin_Day_of_Month=1,
110
+ End_Month=12,
111
+ End_Day_of_Month=31,
112
+ Day_of_Week_for_Start_Day="Sunday",
113
+ )
114
+ run_period.add(idf)
115
+
116
+ # configure timestep
117
+ timestep = Timestep(
118
+ Number_of_Timesteps_per_Hour=self.timestep,
119
+ )
120
+ timestep.add(idf)
121
+
122
+ # create constant scheds
123
+ always_on_schedule = Schedule.constant_schedule(Name="Always_On", value=1)
124
+ always_off_schedule = Schedule.constant_schedule(Name="Always_Off", value=0)
125
+ year, *_ = always_on_schedule.to_year_week_day()
126
+ year.to_epbunch(idf)
127
+ year, *_ = always_off_schedule.to_year_week_day()
128
+ year.to_epbunch(idf)
129
+
130
+ always_on_schedule = Schedule.constant_schedule(Name="Always On", value=1)
131
+ always_off_schedule = Schedule.constant_schedule(Name="Always Off", value=0)
132
+ year, *_ = always_on_schedule.to_year_week_day()
133
+ year.to_epbunch(idf)
134
+ year, *_ = always_off_schedule.to_year_week_day()
135
+ year.to_epbunch(idf)
136
+
137
+ always_on_schedule = Schedule.constant_schedule(Name="On", value=1)
138
+ always_off_schedule = Schedule.constant_schedule(Name="Off", value=0)
139
+ year, *_ = always_on_schedule.to_year_week_day()
140
+ year.to_epbunch(idf)
141
+ year, *_ = always_off_schedule.to_year_week_day()
142
+ year.to_epbunch(idf)
143
+
144
+ # Handle Geometry
145
+ zone_dims = ShoeboxGeometry(
146
+ x=0,
147
+ y=0,
148
+ w=10,
149
+ d=10,
150
+ h=3,
151
+ num_stories=3,
152
+ roof_height=2.5,
153
+ basement=False,
154
+ wwr=0.15,
155
+ zoning="by_storey",
156
+ )
157
+ idf = zone_dims.add(idf)
158
+
159
+ window_construction = Construction(
160
+ name="Project External Window",
161
+ layers=[
162
+ SimpleGlazingMaterial(
163
+ Name="DefaultGlazing",
164
+ Solar_Heat_Gain_Coefficient=self.WindowSHGC,
165
+ UFactor=self.WindowU,
166
+ Visible_Transmittance=0.7,
167
+ )
168
+ ],
169
+ )
170
+ window_construction.add(idf)
171
+
172
+ # Handle Wall Constructions
173
+ wall_construction = Construction(
174
+ name="Project Wall", # NB/TODO: this is the name set by geomeppy
175
+ layers=[
176
+ material_lib.stucco.as_layer(thickness=0.03),
177
+ material_lib.insulation.as_layer(thickness=0.15),
178
+ material_lib.gypsum.as_layer(thickness=0.02),
179
+ ],
180
+ )
181
+ wall_construction.add(idf)
182
+
183
+ # Handle Roof Constructions
184
+ # roof_construction = Construction(
185
+ # name="Project Flat Roof",
186
+ # layers=[
187
+ # material_lib.stucco.as_layer(thickness=0.03),
188
+ # material_lib.insulation.as_layer(thickness=0.15),
189
+ # material_lib.gypsum.as_layer(thickness=0.02),
190
+ # ],
191
+ # )
192
+ # roof_construction.add(idf)
193
+
194
+ # TODO: handle other constructions.
195
+
196
+ # Handle Zone List
197
+ zone_names = [
198
+ zone.Name
199
+ for zone in idf.idfobjects["ZONE"]
200
+ if "attic" not in zone.Name.lower()
201
+ ]
202
+ zone_list = ZoneList(Name="Conditioned_Zones", Names=zone_names)
203
+ zone_list.add(idf)
204
+
205
+ # Handle HVAC
206
+ thermostat = HVACTemplateThermostat(
207
+ Name="Thermostat",
208
+ Constant_Heating_Setpoint=self.HeatingSetpoint,
209
+ Constant_Cooling_Setpoint=self.CoolingSetpoint,
210
+ )
211
+ thermostat.add(idf)
212
+ for zone in idf.idfobjects["ZONE"]:
213
+ zone_name = zone.Name
214
+ if "attic" in zone_name.lower():
215
+ continue
216
+ hvac_template = HVACTemplateZoneIdealLoadsAirSystem(
217
+ Zone_Name=zone_name,
218
+ Template_Thermostat_Name=thermostat.Name,
219
+ )
220
+ idf = hvac_template.add(idf)
221
+
222
+ # Handle People
223
+ occ_sched, *_ = Schedule.from_values(
224
+ "Occupancy_sch",
225
+ Values=schedules.Occupancy.values.tolist(),
226
+ Type="fraction",
227
+ ).to_year_week_day()
228
+ any_number_lim = AScheduleTypeLimits(
229
+ Name="any number", LowerLimit=None, UpperLimit=None
230
+ )
231
+ any_number_lim.to_epbunch(idf)
232
+ acti_year, *_ = Schedule.constant_schedule(
233
+ value=117.28, # pyright: ignore [reportArgumentType]
234
+ Name="Activity_Schedule",
235
+ Type="any number",
236
+ ).to_year_week_day()
237
+ occ_sched.to_epbunch(idf)
238
+ acti_year.to_epbunch(idf)
239
+ people = People(
240
+ Name="People",
241
+ Zone_or_ZoneList_Name=zone_list.Name,
242
+ Number_of_People_Schedule_Name=occ_sched.Name,
243
+ Activity_Level_Schedule_Name=acti_year.Name,
244
+ Number_of_People_Calculation_Method="People/Area",
245
+ People_per_Floor_Area=self.PeopleDensity,
246
+ )
247
+ people.add(idf)
248
+
249
+ # Handle Infiltration
250
+ infiltration = ZoneInfiltrationDesignFlowRate(
251
+ Name="Infiltration",
252
+ Zone_or_ZoneList_Name=zone_list.Name,
253
+ Schedule_Name=always_on_schedule.Name,
254
+ Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
255
+ Flow_Rate_per_Exterior_Surface_Area=self.Infiltration,
256
+ )
257
+ infiltration.add(idf)
258
+
259
+ # Handle Equipment
260
+ equip_sched, *_ = Schedule.from_values(
261
+ "Equipment_sch",
262
+ Values=schedules.Equipment.values.tolist(),
263
+ Type="fraction",
264
+ ).to_year_week_day()
265
+ equip_sched.to_epbunch(idf)
266
+ equipment = ElectricEquipment(
267
+ Name="Equipment",
268
+ Zone_or_ZoneList_Name=zone_list.Name,
269
+ Schedule_Name=equip_sched.Name,
270
+ Design_Level_Calculation_Method="Watts/Area",
271
+ Watts_per_Zone_Floor_Area=self.EPD,
272
+ )
273
+ equipment.add(idf)
274
+
275
+ # Handle Lights
276
+ lights_sched, *_ = Schedule.from_values(
277
+ "Lights_sch",
278
+ Values=schedules.Lights.values.tolist(),
279
+ Type="fraction",
280
+ ).to_year_week_day()
281
+ lights_sched.to_epbunch(idf)
282
+ lights = Lights(
283
+ Name="Lights",
284
+ Zone_or_ZoneList_Name=zone_list.Name,
285
+ Schedule_Name=lights_sched.Name,
286
+ Design_Level_Calculation_Method="Watts/Area",
287
+ Watts_per_Zone_Floor_Area=self.LPD,
288
+ )
289
+ lights.add(idf)
290
+
291
+ # Handle Water
292
+ # TODO: energy only changes with flow rate because no heater is provided.
293
+ # water_flow_sch, *_ = Schedule.constant_schedule(Name="Water_Flow", value=0.1).to_year_week_day()
294
+ # water_flow_sch.to_epbunch(idf)
295
+ # target_temp_sch, *_ = Schedule.constant_schedule(Name="Target_Temperature", value=10, Type="Temperature").to_year_week_day()
296
+ # target_temp_sch.to_epbunch(idf)
297
+ # water = WaterUseEquipment(
298
+ # Name="DHW",
299
+ # Peak_Flow_Rate=0.0001,
300
+ # Flow_Rate_Fraction_Schedule_Name=water_flow_sch.Name,
301
+ # Hot_Water_Supply_Temperature_Schedule_Name=target_temp_sch.Name,
302
+ # # Zone_Name=
303
+ # # Latent_Fraction_Schedule_Name=
304
+ # # Sensible_Fraction_Schedule_Name=
305
+ # )
306
+ # water.add(idf)
307
+
308
+ # TODO: should we be using a slab processor?
309
+
310
+ return idf