Skip to content

Commit c1d4726

Browse files
authored
Merge pull request #22 from BuildingEnergySimulationTools/corrai-adapatations
Corrai adapatations
2 parents 8abf089 + b6e7fba commit c1d4726

12 files changed

Lines changed: 832 additions & 1080 deletions

energytool/building.py

Lines changed: 47 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from pathlib import Path
1111

1212
import pandas as pd
13-
13+
from corrai.base.model import Model
1414
from eppy.modeleditor import IDF
1515
from eppy.runner.run_functions import run
1616
import eppy.json_functions as json_functions
@@ -56,56 +56,7 @@ def temporary_directory():
5656
shutil.rmtree(temp_dir)
5757

5858

59-
def expand_parameter_dict(parameter_dict, param_mappings):
60-
"""
61-
Expands a parameter dictionary based on predefined mappings.
62-
63-
This method takes a dictionary of parameters and expands it by applying
64-
predefined mappings stored in param_mappings. The expansion process works
65-
as follows:
66-
67-
1. **If the mapping for a parameter is a dictionary**:
68-
- The method checks if the value exists as a key in the mapping.
69-
- If found, the corresponding mapped values are added to the expanded dictionary.
70-
71-
2. **If the mapping for a parameter is a list**:
72-
- The method applies the value to each key in the mapping and adds
73-
these key-value pairs to the expanded dictionary.
74-
75-
3. **If a parameter has no predefined mapping**:
76-
- The original parameter and its value are added directly to the expanded
77-
dictionary.
78-
79-
Parameters
80-
----------
81-
parameter_dict : dict
82-
The original parameter dictionary, with parameter names as keys
83-
and their values as values.
84-
param_mappings: dict
85-
A dictionary defining how sampled parameters should be expanded.
86-
87-
Returns
88-
-------
89-
dict
90-
An expanded parameter dictionary containing the original parameters along with
91-
additional key-value pairs derived from the predefined mappings.
92-
93-
"""
94-
expanded_dict = {}
95-
for param_name, value in parameter_dict.items():
96-
if param_name in param_mappings:
97-
mapping = param_mappings[param_name]
98-
if isinstance(mapping, dict):
99-
if value in mapping:
100-
expanded_dict.update(mapping[value])
101-
else:
102-
expanded_dict.update({k: value for k in mapping})
103-
else:
104-
expanded_dict[param_name] = value
105-
return expanded_dict
106-
107-
108-
class Building:
59+
class Building(Model):
10960
"""
11061
The Building class represents a building model. It is based on an EnergyPlus
11162
simulation file.
@@ -124,19 +75,20 @@ class Building:
12475
volume: Calculates and returns the total volume of the building in cubic meters.
12576
add_system(system): Adds an HVAC system to the building's systems.
12677
del_system(system_name): Deletes an HVAC system from the building's systems by name.
127-
simulate(parameter_dict, simulation_options): Simulates the building model with
78+
simulate(property_dict, simulation_options): Simulates the building model with
12879
specified parameters and simulation options, returning the simulation results
12980
as a pandas DataFrame.
13081
"""
13182

132-
def __init__(
133-
self,
134-
idf_path,
135-
):
83+
def __init__(self, idf_path):
84+
super().__init__(is_dynamic=True)
13685
self.idf = IDF(str(idf_path))
13786
self._idf_path = str(idf_path)
13887
self.systems = {category: [] for category in SystemCategories}
13988

89+
def get_property_values(self, property_list: list[str]) -> list[str | int | float]:
90+
return self.get_param_init_value(property_list)
91+
14092
@staticmethod
14193
def set_idd(root_eplus):
14294
try:
@@ -214,8 +166,18 @@ def get_param_init_value(
214166
split_key = full_key.split(".")
215167

216168
if split_key[0] == ParamCategories.IDF.value:
217-
value = energytool.base.idf_utils.getidfvalue(working_idf, full_key)
218-
values.append(value)
169+
if "*" in split_key:
170+
is_single = False
171+
172+
object_type = split_key[1]
173+
field_name = split_key[-1]
174+
175+
objs = working_idf.idfobjects[object_type.upper()]
176+
for obj in objs:
177+
values.append(getattr(obj, field_name))
178+
else:
179+
value = energytool.base.idf_utils.getidfvalue(working_idf, full_key)
180+
values.append(value)
219181

220182
elif split_key[0] == ParamCategories.SYSTEM.value:
221183
if split_key[1].upper() in [sys.value for sys in SystemCategories]:
@@ -236,15 +198,15 @@ def get_param_init_value(
236198

237199
def simulate(
238200
self,
239-
parameter_dict: dict[str, str | float | int] = None,
240-
simulation_options: dict[str, str | float | int] = None,
241-
idf_save_path: Path | None = None,
242-
param_mapping: dict = None,
201+
property_dict=None,
202+
simulation_options=None,
203+
idf_save_path=None,
204+
**simulation_kwargs,
243205
) -> pd.DataFrame:
244206
"""
245207
Simulate the building model with specified parameters and simulation options.
246208
247-
:param parameter_dict: A dictionary containing key-value pairs representing
209+
:param property_dict: A dictionary containing key-value pairs representing
248210
parameters to be modified in the building model.
249211
These parameters can include changes to the IDF file, energytool HVAC system
250212
settings, or weather file.
@@ -286,25 +248,34 @@ def simulate(
286248
'STOP': '2023-01-31 23:59:59',
287249
'TIMESTEP': 900
288250
}
289-
results = building.simulate(parameter_dict=parameter_changes,
251+
results = building.simulate(property_dict=parameter_changes,
290252
simulation_options=simulation_options)
291253
292254
"""
255+
self.idf_save_path = idf_save_path
256+
293257
working_idf = deepcopy(self.idf)
294258
working_syst = deepcopy(self.systems)
295259

296260
epw_path = None
297-
if parameter_dict is None:
298-
parameter_dict = {}
299-
if param_mapping:
300-
parameter_dict = expand_parameter_dict(parameter_dict, param_mapping)
261+
if property_dict is None:
262+
property_dict = {}
301263

302-
for key in parameter_dict:
264+
for key in property_dict:
303265
split_key = key.split(".")
304266

305267
# IDF modification
306268
if split_key[0] == ParamCategories.IDF.value:
307-
json_functions.updateidf(working_idf, {key: parameter_dict[key]})
269+
if "*" in split_key:
270+
object_type = split_key[1]
271+
field_name = split_key[-1]
272+
value = property_dict[key]
273+
274+
objs = working_idf.idfobjects[object_type.upper()]
275+
for obj in objs:
276+
setattr(obj, field_name, value)
277+
else:
278+
json_functions.updateidf(working_idf, {key: property_dict[key]})
308279

309280
# In case it's a SYSTEM parameter, retrieve it in dict by category and name
310281
elif split_key[0] == ParamCategories.SYSTEM.value:
@@ -317,11 +288,11 @@ def simulate(
317288
)
318289
for syst in working_syst[sys_key]:
319290
if syst.name == split_key[2]:
320-
setattr(syst, split_key[3], parameter_dict[key])
291+
setattr(syst, split_key[3], property_dict[key])
321292

322293
# Meteo file
323294
elif split_key[0] == ParamCategories.EPW_FILE.value:
324-
epw_path = parameter_dict[key]
295+
epw_path = property_dict[key]
325296
else:
326297
raise ValueError(
327298
f"{split_key[0]} was not recognize as a valid parameter category"
@@ -333,11 +304,11 @@ def simulate(
333304
epw_path = simulation_options[SimuOpt.EPW_FILE.value]
334305
except KeyError:
335306
raise ValueError(
336-
"'epw_path' not found in parameter_dict nor in simulation_options"
307+
"'epw_path' not found in property_dict nor in simulation_options"
337308
)
338309
elif SimuOpt.EPW_FILE.value in list(simulation_options.keys()):
339310
raise ValueError(
340-
"'epw_path' have been used in both parameter_dict and "
311+
"'epw_path' have been used in both property_dict and "
341312
"simulation_options"
342313
)
343314
ref_year = None
@@ -397,8 +368,8 @@ def simulate(
397368
)
398369

399370
# Save IDF file after pre-process
400-
if idf_save_path:
401-
self.save(idf_save_path)
371+
if self.idf_save_path:
372+
working_idf.save(idf_save_path)
402373

403374
# POST-PROCESS
404375
return get_results(

energytool/variant.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import enum
2+
import itertools
3+
from collections.abc import Callable
4+
from copy import deepcopy
5+
from pathlib import Path
6+
from typing import Any
7+
8+
from multiprocessing import cpu_count
9+
10+
from joblib import Parallel, delayed
11+
from fastprogress.fastprogress import progress_bar
12+
13+
from corrai.base.model import Model
14+
15+
16+
class VariantKeys(enum.Enum):
17+
MODIFIER = "MODIFIER"
18+
ARGUMENTS = "ARGUMENTS"
19+
DESCRIPTION = "DESCRIPTION"
20+
21+
22+
def get_modifier_dict(
23+
variant_dict: dict[str, dict[VariantKeys, Any]], add_existing: bool = False
24+
):
25+
"""
26+
Generate a dictionary that maps modifier values (name) to associated variant names.
27+
28+
This function takes a dictionary containing variant information and extracts
29+
the MODIFIER values along with their corresponding variants, creating a new
30+
dictionary where each modifier is associated with a list of variant names
31+
that share that modifier.
32+
33+
:param variant_dict: A dictionary containing variant information where keys are
34+
variant names and values are dictionaries with keys from the
35+
VariantKeys enum (e.g., MODIFIER, ARGUMENTS, DESCRIPTION).
36+
:param add_existing: A boolean flag indicating whether to include existing
37+
variant to each modifier.
38+
If True, existing modifiers will be included;
39+
if False, only non-existing modifiers will be considered.
40+
Set to False by default.
41+
:return: A dictionary that maps modifier values to lists of variant names.
42+
"""
43+
temp_dict = {}
44+
45+
if add_existing:
46+
temp_dict = {
47+
variant_dict[var][VariantKeys.MODIFIER]: [
48+
f"EXISTING_{variant_dict[var][VariantKeys.MODIFIER]}"
49+
]
50+
for var in variant_dict.keys()
51+
}
52+
for var in variant_dict.keys():
53+
temp_dict[variant_dict[var][VariantKeys.MODIFIER]].append(var)
54+
else:
55+
for var in variant_dict.keys():
56+
modifier = variant_dict[var][VariantKeys.MODIFIER]
57+
if modifier not in temp_dict:
58+
temp_dict[modifier] = []
59+
temp_dict[modifier].append(var)
60+
61+
return temp_dict
62+
63+
64+
def get_combined_variants(
65+
variant_dict: dict[str, dict[VariantKeys, Any]], add_existing: bool = False
66+
):
67+
"""
68+
Generate a list of combined variants based on the provided variant dictionary.
69+
70+
This function takes a dictionary containing variant information and generates a list
71+
of combined variants by taking the Cartesian product of the variant names.
72+
The resulting list contains tuples, where each tuple represents a
73+
combination of variant to create a unique combination.
74+
75+
:param variant_dict: A dictionary containing variant information where keys are
76+
variant names and values are dictionaries with keys from the
77+
VariantKeys enum (e.g., MODIFIER, ARGUMENTS, DESCRIPTION).
78+
:param add_existing: A boolean flag indicating whether to include existing
79+
variant to each modifier.
80+
If True, existing modifiers will be included;
81+
if False, only non-existing modifiers will be considered.
82+
Set to False by default.
83+
:return: A list of tuples representing combined variants based on the provided
84+
variant dictionary.
85+
"""
86+
modifier_dict = get_modifier_dict(variant_dict, add_existing)
87+
return list(itertools.product(*list(modifier_dict.values())))
88+
89+
90+
def simulate_variants(
91+
model: Model,
92+
variant_dict: dict[str, dict[VariantKeys, Any]],
93+
modifier_map: dict[str, Callable],
94+
simulation_options: dict[str, Any],
95+
n_cpu: int = -1,
96+
add_existing: bool = False,
97+
custom_combinations=None,
98+
save_dir: Path = None,
99+
file_extension: str = ".txt",
100+
simulate_kwargs: dict = None,
101+
):
102+
simulate_kwargs = simulate_kwargs or {}
103+
104+
if n_cpu <= 0:
105+
n_cpu = max(1, cpu_count() + n_cpu)
106+
107+
if custom_combinations is not None:
108+
combined_variants = custom_combinations
109+
else:
110+
combined_variants = get_combined_variants(variant_dict, add_existing)
111+
112+
models = []
113+
114+
for idx, simulation in enumerate(combined_variants, start=1):
115+
working_model = deepcopy(model)
116+
117+
for variant in simulation:
118+
split_var = variant.split("_")
119+
if (add_existing and split_var[0] != "EXISTING") or not add_existing:
120+
modifier = modifier_map[variant_dict[variant][VariantKeys.MODIFIER]]
121+
modifier(
122+
model=working_model,
123+
description=variant_dict[variant][VariantKeys.DESCRIPTION],
124+
**variant_dict[variant][VariantKeys.ARGUMENTS],
125+
)
126+
127+
if save_dir:
128+
working_model.save((save_dir / f"Model_{idx}").with_suffix(file_extension))
129+
130+
models.append(working_model)
131+
132+
bar = progress_bar(models)
133+
134+
results = Parallel(n_jobs=n_cpu)(
135+
delayed(
136+
lambda m: m.simulate(
137+
simulation_options=simulation_options, **simulate_kwargs
138+
)
139+
)(m)
140+
for m in bar
141+
)
142+
143+
return results

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ classifiers = [
2020
]
2121
requires-python = ">=3.10"
2222
dependencies = [
23+
"corrai>=1.0.0",
24+
"ipython >= 8.13.0",
2325
"numpy>=1.22.4, <2.0",
2426
"pandas>=2.0.0, <3.0",
2527
"eppy>=0.5.63",

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
corrai>=1.0.0
12
numpy~=1.22.3
23
pandas~=1.4.2
34
setuptools~=62.1.0

requirements/install-min.txt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
numpy==1.22.4
2-
pandas==2.0.0
3-
eppy==0.5.63
1+
eppy==0.5.63
2+
corrai>=1.0.0
3+
pandas>=2.0.0
4+
numpy>=1.22.4

0 commit comments

Comments
 (0)