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.
- epinterface/__init__.py +1 -0
- epinterface/actions.py +382 -0
- epinterface/builder.py +310 -0
- epinterface/cli.py +284 -0
- epinterface/climate_studio/__init__.py +1 -0
- epinterface/climate_studio/builder.py +634 -0
- epinterface/climate_studio/interface.py +2159 -0
- epinterface/constants/__init__.py +73 -0
- epinterface/data/Minimal.idf +164 -0
- epinterface/data/USA_MA_Boston-Logan.Intl.AP.725090_TMYx.2009-2023.ddy +3353 -0
- epinterface/data/USA_MA_Boston-Logan.Intl.AP.725090_TMYx.2009-2023.epw +8768 -0
- epinterface/data/USA_MA_Boston-Logan.Intl.AP.725090_TMYx.2009-2023.zip +0 -0
- epinterface/data/__init__.py +18 -0
- epinterface/data/res_schedules.parquet +0 -0
- epinterface/ddy_injector_bayes.py +330 -0
- epinterface/ddy_interface_bayes.py +330 -0
- epinterface/geometry.py +724 -0
- epinterface/interface.py +1080 -0
- epinterface/py.typed +0 -0
- epinterface/sbem/__init__.py +1 -0
- epinterface/sbem/annotations.py +57 -0
- epinterface/sbem/builder.py +1350 -0
- epinterface/sbem/common.py +38 -0
- epinterface/sbem/components/__init__.py +1 -0
- epinterface/sbem/components/composer.py +388 -0
- epinterface/sbem/components/envelope.py +367 -0
- epinterface/sbem/components/materials.py +144 -0
- epinterface/sbem/components/operations.py +342 -0
- epinterface/sbem/components/schedules.py +716 -0
- epinterface/sbem/components/space_use.py +270 -0
- epinterface/sbem/components/systems.py +270 -0
- epinterface/sbem/components/zones.py +25 -0
- epinterface/sbem/exceptions.py +101 -0
- epinterface/sbem/fields/__init__.py +1 -0
- epinterface/sbem/fields/spec.py +95 -0
- epinterface/sbem/flat_model.py +2210 -0
- epinterface/sbem/interface.py +590 -0
- epinterface/sbem/model_graph_structure.md +31 -0
- epinterface/sbem/prisma/client.py +650 -0
- epinterface/sbem/prisma/migrations/20250309133033_create_initial_schema/migration.sql +443 -0
- epinterface/sbem/prisma/migrations/20250310162045_switch_to_using_one_week_per_month/migration.sql +61 -0
- epinterface/sbem/prisma/migrations/20250317202733_change_names_ventilation/migration.sql +30 -0
- epinterface/sbem/prisma/migrations/20250325185158_add_mutually_exclusive_ventilation_techtypes/migration.sql +32 -0
- epinterface/sbem/prisma/migrations/20250326141941_reduce_naming_complexity_ventilation/migration.sql +37 -0
- epinterface/sbem/prisma/migrations/20250331141910_add_support_for_attic_and_basement_constructions/migration.sql +74 -0
- epinterface/sbem/prisma/migrations/20250919152559_decouple_basement_infiltration/migration.sql +31 -0
- epinterface/sbem/prisma/migrations/migration_lock.toml +3 -0
- epinterface/sbem/prisma/partial_types.py +196 -0
- epinterface/sbem/prisma/schema.prisma +582 -0
- epinterface/sbem/prisma/seed_fns.py +732 -0
- epinterface/sbem/utils.py +74 -0
- epinterface/weather.py +138 -0
- epinterface-1.0.0.dist-info/METADATA +70 -0
- epinterface-1.0.0.dist-info/RECORD +57 -0
- epinterface-1.0.0.dist-info/WHEEL +4 -0
- epinterface-1.0.0.dist-info/entry_points.txt +3 -0
- epinterface-1.0.0.dist-info/licenses/LICENSE +21 -0
epinterface/__init__.py
ADDED
|
@@ -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
|