Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 47 additions & 76 deletions energytool/building.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from pathlib import Path

import pandas as pd

from corrai.base.model import Model
from eppy.modeleditor import IDF
from eppy.runner.run_functions import run
import eppy.json_functions as json_functions
Expand Down Expand Up @@ -56,56 +56,7 @@ def temporary_directory():
shutil.rmtree(temp_dir)


def expand_parameter_dict(parameter_dict, param_mappings):
"""
Expands a parameter dictionary based on predefined mappings.

This method takes a dictionary of parameters and expands it by applying
predefined mappings stored in param_mappings. The expansion process works
as follows:

1. **If the mapping for a parameter is a dictionary**:
- The method checks if the value exists as a key in the mapping.
- If found, the corresponding mapped values are added to the expanded dictionary.

2. **If the mapping for a parameter is a list**:
- The method applies the value to each key in the mapping and adds
these key-value pairs to the expanded dictionary.

3. **If a parameter has no predefined mapping**:
- The original parameter and its value are added directly to the expanded
dictionary.

Parameters
----------
parameter_dict : dict
The original parameter dictionary, with parameter names as keys
and their values as values.
param_mappings: dict
A dictionary defining how sampled parameters should be expanded.

Returns
-------
dict
An expanded parameter dictionary containing the original parameters along with
additional key-value pairs derived from the predefined mappings.

"""
expanded_dict = {}
for param_name, value in parameter_dict.items():
if param_name in param_mappings:
mapping = param_mappings[param_name]
if isinstance(mapping, dict):
if value in mapping:
expanded_dict.update(mapping[value])
else:
expanded_dict.update({k: value for k in mapping})
else:
expanded_dict[param_name] = value
return expanded_dict


class Building:
class Building(Model):
"""
The Building class represents a building model. It is based on an EnergyPlus
simulation file.
Expand All @@ -124,19 +75,20 @@ class Building:
volume: Calculates and returns the total volume of the building in cubic meters.
add_system(system): Adds an HVAC system to the building's systems.
del_system(system_name): Deletes an HVAC system from the building's systems by name.
simulate(parameter_dict, simulation_options): Simulates the building model with
simulate(property_dict, simulation_options): Simulates the building model with
specified parameters and simulation options, returning the simulation results
as a pandas DataFrame.
"""

def __init__(
self,
idf_path,
):
def __init__(self, idf_path):
super().__init__(is_dynamic=True)
self.idf = IDF(str(idf_path))
self._idf_path = str(idf_path)
self.systems = {category: [] for category in SystemCategories}

def get_property_values(self, property_list: list[str]) -> list[str | int | float]:
return self.get_param_init_value(property_list)

@staticmethod
def set_idd(root_eplus):
try:
Expand Down Expand Up @@ -214,8 +166,18 @@ def get_param_init_value(
split_key = full_key.split(".")

if split_key[0] == ParamCategories.IDF.value:
value = energytool.base.idf_utils.getidfvalue(working_idf, full_key)
values.append(value)
if "*" in split_key:
is_single = False

object_type = split_key[1]
field_name = split_key[-1]

objs = working_idf.idfobjects[object_type.upper()]
for obj in objs:
values.append(getattr(obj, field_name))
else:
value = energytool.base.idf_utils.getidfvalue(working_idf, full_key)
values.append(value)

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

def simulate(
self,
parameter_dict: dict[str, str | float | int] = None,
simulation_options: dict[str, str | float | int] = None,
idf_save_path: Path | None = None,
param_mapping: dict = None,
property_dict=None,
simulation_options=None,
idf_save_path=None,
**simulation_kwargs,
) -> pd.DataFrame:
"""
Simulate the building model with specified parameters and simulation options.

:param parameter_dict: A dictionary containing key-value pairs representing
:param property_dict: A dictionary containing key-value pairs representing
parameters to be modified in the building model.
These parameters can include changes to the IDF file, energytool HVAC system
settings, or weather file.
Expand Down Expand Up @@ -286,25 +248,34 @@ def simulate(
'STOP': '2023-01-31 23:59:59',
'TIMESTEP': 900
}
results = building.simulate(parameter_dict=parameter_changes,
results = building.simulate(property_dict=parameter_changes,
simulation_options=simulation_options)

"""
self.idf_save_path = idf_save_path

working_idf = deepcopy(self.idf)
working_syst = deepcopy(self.systems)

epw_path = None
if parameter_dict is None:
parameter_dict = {}
if param_mapping:
parameter_dict = expand_parameter_dict(parameter_dict, param_mapping)
if property_dict is None:
property_dict = {}

for key in parameter_dict:
for key in property_dict:
split_key = key.split(".")

# IDF modification
if split_key[0] == ParamCategories.IDF.value:
json_functions.updateidf(working_idf, {key: parameter_dict[key]})
if "*" in split_key:
object_type = split_key[1]
field_name = split_key[-1]
value = property_dict[key]

objs = working_idf.idfobjects[object_type.upper()]
for obj in objs:
setattr(obj, field_name, value)
else:
json_functions.updateidf(working_idf, {key: property_dict[key]})

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

# Meteo file
elif split_key[0] == ParamCategories.EPW_FILE.value:
epw_path = parameter_dict[key]
epw_path = property_dict[key]
else:
raise ValueError(
f"{split_key[0]} was not recognize as a valid parameter category"
Expand All @@ -333,11 +304,11 @@ def simulate(
epw_path = simulation_options[SimuOpt.EPW_FILE.value]
except KeyError:
raise ValueError(
"'epw_path' not found in parameter_dict nor in simulation_options"
"'epw_path' not found in property_dict nor in simulation_options"
)
elif SimuOpt.EPW_FILE.value in list(simulation_options.keys()):
raise ValueError(
"'epw_path' have been used in both parameter_dict and "
"'epw_path' have been used in both property_dict and "
"simulation_options"
)
ref_year = None
Expand Down Expand Up @@ -397,8 +368,8 @@ def simulate(
)

# Save IDF file after pre-process
if idf_save_path:
self.save(idf_save_path)
if self.idf_save_path:
working_idf.save(idf_save_path)

# POST-PROCESS
return get_results(
Expand Down
143 changes: 143 additions & 0 deletions energytool/variant.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import enum
import itertools
from collections.abc import Callable
from copy import deepcopy
from pathlib import Path
from typing import Any

from multiprocessing import cpu_count

from joblib import Parallel, delayed
from fastprogress.fastprogress import progress_bar

from corrai.base.model import Model


class VariantKeys(enum.Enum):
MODIFIER = "MODIFIER"
ARGUMENTS = "ARGUMENTS"
DESCRIPTION = "DESCRIPTION"


def get_modifier_dict(
variant_dict: dict[str, dict[VariantKeys, Any]], add_existing: bool = False
):
"""
Generate a dictionary that maps modifier values (name) to associated variant names.

This function takes a dictionary containing variant information and extracts
the MODIFIER values along with their corresponding variants, creating a new
dictionary where each modifier is associated with a list of variant names
that share that modifier.

:param variant_dict: A dictionary containing variant information where keys are
variant names and values are dictionaries with keys from the
VariantKeys enum (e.g., MODIFIER, ARGUMENTS, DESCRIPTION).
:param add_existing: A boolean flag indicating whether to include existing
variant to each modifier.
If True, existing modifiers will be included;
if False, only non-existing modifiers will be considered.
Set to False by default.
:return: A dictionary that maps modifier values to lists of variant names.
"""
temp_dict = {}

if add_existing:
temp_dict = {
variant_dict[var][VariantKeys.MODIFIER]: [
f"EXISTING_{variant_dict[var][VariantKeys.MODIFIER]}"
]
for var in variant_dict.keys()
}
for var in variant_dict.keys():
temp_dict[variant_dict[var][VariantKeys.MODIFIER]].append(var)
else:
for var in variant_dict.keys():
modifier = variant_dict[var][VariantKeys.MODIFIER]
if modifier not in temp_dict:
temp_dict[modifier] = []
temp_dict[modifier].append(var)

return temp_dict


def get_combined_variants(
variant_dict: dict[str, dict[VariantKeys, Any]], add_existing: bool = False
):
"""
Generate a list of combined variants based on the provided variant dictionary.

This function takes a dictionary containing variant information and generates a list
of combined variants by taking the Cartesian product of the variant names.
The resulting list contains tuples, where each tuple represents a
combination of variant to create a unique combination.

:param variant_dict: A dictionary containing variant information where keys are
variant names and values are dictionaries with keys from the
VariantKeys enum (e.g., MODIFIER, ARGUMENTS, DESCRIPTION).
:param add_existing: A boolean flag indicating whether to include existing
variant to each modifier.
If True, existing modifiers will be included;
if False, only non-existing modifiers will be considered.
Set to False by default.
:return: A list of tuples representing combined variants based on the provided
variant dictionary.
"""
modifier_dict = get_modifier_dict(variant_dict, add_existing)
return list(itertools.product(*list(modifier_dict.values())))


def simulate_variants(
model: Model,
variant_dict: dict[str, dict[VariantKeys, Any]],
modifier_map: dict[str, Callable],
simulation_options: dict[str, Any],
n_cpu: int = -1,
add_existing: bool = False,
custom_combinations=None,
save_dir: Path = None,
file_extension: str = ".txt",
simulate_kwargs: dict = None,
):
simulate_kwargs = simulate_kwargs or {}

if n_cpu <= 0:
n_cpu = max(1, cpu_count() + n_cpu)

if custom_combinations is not None:
combined_variants = custom_combinations
else:
combined_variants = get_combined_variants(variant_dict, add_existing)

models = []

for idx, simulation in enumerate(combined_variants, start=1):
working_model = deepcopy(model)

for variant in simulation:
split_var = variant.split("_")
if (add_existing and split_var[0] != "EXISTING") or not add_existing:
modifier = modifier_map[variant_dict[variant][VariantKeys.MODIFIER]]
modifier(
model=working_model,
description=variant_dict[variant][VariantKeys.DESCRIPTION],
**variant_dict[variant][VariantKeys.ARGUMENTS],
)

if save_dir:
working_model.save((save_dir / f"Model_{idx}").with_suffix(file_extension))

models.append(working_model)

bar = progress_bar(models)

results = Parallel(n_jobs=n_cpu)(
delayed(
lambda m: m.simulate(
simulation_options=simulation_options, **simulate_kwargs
)
)(m)
for m in bar
)

return results
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ classifiers = [
]
requires-python = ">=3.10"
dependencies = [
"corrai>=1.0.0",
"ipython >= 8.13.0",
"numpy>=1.22.4, <2.0",
"pandas>=2.0.0, <3.0",
"eppy>=0.5.63",
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
corrai>=1.0.0
numpy~=1.22.3
pandas~=1.4.2
setuptools~=62.1.0
Expand Down
7 changes: 4 additions & 3 deletions requirements/install-min.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
numpy==1.22.4
pandas==2.0.0
eppy==0.5.63
eppy==0.5.63
corrai>=1.0.0
pandas>=2.0.0
numpy>=1.22.4
Loading