Coverage for bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/task/create_result_df.py: 0%
133 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-01 10:24 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-01 10:24 +0000
1import json
2from pathlib import Path
4import pandas as pd
5import pint_pandas
6from geomeppy import IDF
7from pint_pandas import PintArray
9from bim2sim.plugins.PluginEnergyPlus.bim2sim_energyplus.task import \
10 IdfPostprocessing
11from bim2sim.plugins.PluginEnergyPlus.bim2sim_energyplus.utils import \
12 PostprocessingUtils
13from bim2sim.tasks.base import ITask
14from bim2sim.elements.mapping.units import ureg
15from bim2sim.utilities.common_functions import filter_elements
17bim2sim_energyplus_mapping_base = {
18 "NOT_AVAILABLE": "heat_demand_total",
19 "SPACEGUID IDEAL LOADS AIR SYSTEM:Zone Ideal Loads Supply Air Total "
20 "Heating Rate [W](Hourly)": "heat_demand_rooms",
21 "NOT_AVAILABLE": "cool_demand_total",
22 "SPACEGUID IDEAL LOADS AIR SYSTEM:Zone Ideal Loads Supply Air Total "
23 "Cooling Rate [W](Hourly)": "cool_demand_rooms",
24 "DistrictHeating:HVAC [J](Hourly)": "heat_energy_total",
25 "DistrictCooling:HVAC [J](Hourly) ": "cool_energy_total",
26 "SPACEGUID IDEAL LOADS AIR SYSTEM:Zone Ideal Loads Supply Air Total "
27 "Heating Energy [J](Hourly)":
28 "heat_energy_rooms",
29 "SPACEGUID IDEAL LOADS AIR SYSTEM:Zone Ideal Loads Supply Air Total "
30 "Cooling Energy [J](Hourly)":
31 "cool_energy_rooms",
32 "Environment:Site Outdoor Air Drybulb Temperature [C](Hourly)":
33 "air_temp_out",
34 "SPACEGUID:Zone Operative Temperature [C](Hourly)":
35 "operative_temp_rooms",
36 "SPACEGUID:Zone Mean Air Temperature [C](Hourly)": "air_temp_rooms",
37 "SPACEGUID:Zone Electric Equipment Total Heating Rate [W](Hourly)": "internal_gains_machines_rooms",
38 "SPACEGUID:Zone People Total Heating Rate [W](Hourly)": "internal_gains_persons_rooms",
39 "SPACEGUID:Zone People Occupant Count [](Hourly)": "n_persons_rooms",
40 "SPACEGUID:Zone Lights Total Heating Rate [W](Hourly)": "internal_gains_lights_rooms",
41 "SPACEGUID:Zone Infiltration Air Change Rate [ach](Hourly)": "infiltration_rooms",
42 "SPACEGUID:Zone Ventilation Standard Density Volume Flow Rate [m3/s](Hourly)": "mech_ventilation_rooms",
43 "SPACEGUID:Zone Thermostat Heating Setpoint Temperature [C](Hourly)": "heat_set_rooms",
44 "SPACEGUID:Zone Thermostat Cooling Setpoint Temperature [C](Hourly)": "cool_set_rooms",
45 "BOUNDGUID:Surface Inside Face Temperature [C](Hourly)": "surf_inside_temp",
46}
48pint_pandas.PintType.ureg = ureg
49unit_mapping = {
50 "heat_demand": ureg.watt,
51 "cool_demand": ureg.watt,
52 "heat_energy": ureg.joule,
53 "cool_energy": ureg.joule,
54 "operative_temp": ureg.degree_Celsius,
55 "surf_inside_temp": ureg.degree_Celsius,
56 "air_temp": ureg.degree_Celsius,
57 "heat_set": ureg.degree_Celsius,
58 "cool_set": ureg.degree_Celsius,
59 "internal_gains": ureg.watt,
60 "n_persons": ureg.dimensionless,
61 "infiltration": ureg.hour ** (-1),
62 "mech_ventilation": (ureg.meter ** 3) / ureg.second,
63}
64final_units = {
65 "heat_energy": ureg.watthour,
66 "cool_energy": ureg.watthour,
67}
70class CreateResultDF(ITask):
71 """This ITask creates a result dataframe for EnergyPlus BEPS simulations
73 See detailed explanation in the run function below.
74 """
76 reads = ('idf', 'sim_results_path', 'elements')
77 touches = ('df_finals',)
79 def run(self, idf: IDF, sim_results_path: Path, elements: dict) \
80 -> dict[str: pd.DataFrame]:
81 """ Create a result DataFrame for EnergyPlus BEPS results.
83 This function transforms the EnergyPlus simulation results to the
84 general result data format used in this bim2sim project. The
85 simulation results stored in the EnergyPlus result file (
86 "eplusout.csv") are shifted by one hour to match the simulation
87 results of the modelica simulation. Afterwards, the simulation
88 results are formatted to match the bim2sim dataframe format.
90 Args:
91 idf (IDF): eppy idf
92 sim_results_path (Path): path to the simulation results from
93 EnergyPlus
94 elements (dict): dictionary in the format dict[guid: element],
95 holds preprocessed elements including space boundaries.
96 Returns:
97 df_finals (dict): dictionary in the format
98 dict[str(project name): pd.DataFrame], final dataframe
99 that holds only relevant data, with generic `bim2sim` names and
100 index in form of MM/DD-hh:mm:ss
102 """
103 # ToDO handle multiple buildings/ifcs #35
104 df_finals = {}
105 if not self.playground.sim_settings.create_plots:
106 self.logger.warning("Skipping task CreateResultDF as sim_setting "
107 "'create_plots' is set to False and no "
108 "DataFrame ist needed.")
109 return df_finals,
110 raw_csv_path = sim_results_path / self.prj_name / 'eplusout.csv'
111 # TODO @Veronika: the zone_dict.json can be removed and instead the
112 # elements structure can be used to get the zone guids
113 zone_dict_path = sim_results_path / self.prj_name / 'zone_dict.json'
114 if not zone_dict_path.exists():
115 IdfPostprocessing.write_zone_names(idf, elements,
116 sim_results_path / self.prj_name)
117 with open(zone_dict_path) as j:
118 zone_dict = json.load(j)
120 # create dict for mapping surfaces to spaces
121 space_bound_dict = {}
122 space_bound_renamed_dict = {}
123 spaces = filter_elements(elements, 'ThermalZone')
124 for space in spaces:
125 space_guids = []
126 for bound in space.space_boundaries:
127 if isinstance(bound, str):
128 space_guids.append(bound)
129 else:
130 space_guids.append(bound.guid)
131 # rename space boundaries according to their surface orientation
132 # for better identification in surface temperature plots.
133 space_bound_renamed_dict[space.guid] = self.oriented_surface_names(
134 idf,
135 space.guid)
136 space_bound_dict[space.guid] = space_guids
137 with open(sim_results_path / self.prj_name / 'space_bound_dict.json',
138 'w+') as file1:
139 json.dump(space_bound_dict, file1, indent=4)
140 with open(sim_results_path / self.prj_name /
141 'space_bound_renamed_dict.json',
142 'w+') as file2:
143 json.dump(space_bound_renamed_dict, file2, indent=4)
145 df_original = PostprocessingUtils.read_csv_and_format_datetime(
146 raw_csv_path)
147 df_original = (
148 PostprocessingUtils.shift_dataframe_to_midnight(df_original))
149 df_final = self.format_dataframe(df_original, zone_dict,
150 space_bound_dict)
151 df_finals[self.prj_name] = df_final
153 return df_finals,
155 def format_dataframe(
156 self, df_original: pd.DataFrame, zone_dict: dict,
157 space_bound_dict: dict) -> pd.DataFrame:
158 """Formats the dataframe to generic bim2sim output structure.
160 This function:
161 - adds the space GUIDs to the results
162 - selects only the selected simulation outputs from the result
164 Args:
165 df_original: original dataframe directly taken from simulation
166 zone_dict: dictionary with all zones, in format {GUID : Zone Usage}
167 space_bound_dict: dictionary with space_guid and a list of space
168 boundary guids of this space.
169 Returns:
170 df_final: converted dataframe in `bim2sim` result structure
171 """
172 bim2sim_energyplus_mapping = self.map_zonal_results(
173 bim2sim_energyplus_mapping_base, zone_dict, space_bound_dict,
174 self.playground.sim_settings.plot_singe_zone_guid)
175 # select only relevant columns
176 short_list = \
177 list(bim2sim_energyplus_mapping.keys())
178 short_list.remove('NOT_AVAILABLE')
179 df_final = df_original[df_original.columns[
180 df_original.columns.isin(short_list)]].rename(
181 columns=bim2sim_energyplus_mapping)
183 # convert negative cooling demands and energies to absolute values
184 energy_and_demands = df_final.filter(like='energy').columns.union(
185 df_final.filter(like='demand').columns)
186 df_final[energy_and_demands].abs()
187 heat_demand_columns = df_final.filter(like='heat_demand')
188 cool_demand_columns = df_final.filter(like='cool_demand')
189 df_final['heat_demand_total'] = heat_demand_columns.sum(axis=1)
190 df_final['cool_demand_total'] = cool_demand_columns.sum(axis=1)
191 # handle units
192 for column in df_final:
193 for key, unit in unit_mapping.items():
194 if key in column:
195 df_final[column] = PintArray(df_final[column], unit)
196 for key, unit in final_units.items():
197 if key in column:
198 df_final[column] = df_final[column].pint.to(unit)
200 return df_final
202 def select_wanted_results(self):
203 """Selected only the wanted outputs based on sim_setting sim_results"""
204 bim2sim_energyplus_mapping = bim2sim_energyplus_mapping_base.copy()
205 for key, value in bim2sim_energyplus_mapping_base.items():
206 if value not in self.playground.sim_settings.sim_results:
207 del bim2sim_energyplus_mapping[key]
208 return bim2sim_energyplus_mapping
210 @staticmethod
211 def map_zonal_results(bim2sim_energyplus_mapping_base, zone_dict,
212 space_bound_dict=None, plot_single_zone_guid=None):
213 """Add zone/space guids/names to mapping dict.
215 EnergyPlus outputs the results referencing to the IFC-GlobalId. This
216 function adds the real zone/space guids or
217 aggregation names to the dict for easy readable results.
218 Rooms are mapped with their space GUID, aggregated zones are mapped
219 with their zone name. The mapping between zones and rooms can be taken
220 from tz_mapping.json file with can be found in export directory.
222 Args:
223 bim2sim_energyplus_mapping_base: Holds the mapping between
224 simulation outputs and generic `bim2sim` output names.
225 zone_dict: dictionary with all zones, in format {GUID : Zone Usage}
226 space_bound_dict: dictionary mapping space guids and their bounds
227 plot_single_zone_guid: guid of single space that should be analyzed
228 Returns:
229 dict: A mapping between simulation results and space guids, with
230 appropriate adjustments for aggregated zones.
232 """
233 bim2sim_energyplus_mapping = {}
234 space_guid_list = list(zone_dict.keys())
235 for key, value in bim2sim_energyplus_mapping_base.items():
236 # add entry for each room/zone
237 if "SPACEGUID" in key:
238 # TODO write case sensitive GUIDs into dataframe
239 for i, space_guid in enumerate(space_guid_list):
240 new_key = key.replace("SPACEGUID", space_guid.upper())
241 # todo: according to #497, names should keep a _zone_ flag
242 new_value = value.replace("rooms", 'rooms_' + space_guid)
243 bim2sim_energyplus_mapping[new_key] = new_value
244 elif "BOUNDGUID" in key and space_bound_dict is not None:
245 for i, space in enumerate(space_bound_dict):
246 if plot_single_zone_guid and \
247 space not in plot_single_zone_guid:
248 # avoid loading space boundary data of multiple
249 # zones unless all zones should be considered to
250 # avoid an unreasonably large dataframe
251 continue
252 for bound in space_bound_dict[space]:
253 guid = bound
254 new_key = key.replace("BOUNDGUID", guid.upper())
255 new_value = value.replace("temp", "temp_" + guid)
256 bim2sim_energyplus_mapping[new_key] = new_value
257 else:
258 bim2sim_energyplus_mapping[key] = value
259 return bim2sim_energyplus_mapping
261 @staticmethod
262 def oriented_surface_names(idf, space_guid):
264 """
265 Identify surface names for each individual surface in a zone based on
266 boundary conditions, constructions, and surface orientations for a
267 proper identification in plots of surface variables (e.g., temperature)
268 Args:
269 idf: Eppy IDF
270 space_guid: single space GUID
272 Returns: dictionary of renamed space boundaries
274 """
275 space_bounds_renamed = {}
276 temp_name_list = []
277 for ib in idf.getobject("ZONE", space_guid).zonesurfaces:
278 temp_name = None
279 if ib is not None:
280 try:
281 az = PostprocessingUtils.true_azimuth(ib)
282 except:
283 az = None
284 if az is None:
285 continue
286 for key in PostprocessingUtils.azimuth_orientations:
287 if (float(key) - 22.5) <= az < (float(key) + 22.5):
288 if ib.Outside_Boundary_Condition == 'Surface':
289 temp_name = 'Inner'
290 elif ib.Outside_Boundary_Condition == 'Outdoors':
291 temp_name = 'Outer'
292 elif ib.Outside_Boundary_Condition == 'Ground':
293 temp_name = 'Ground'
294 elif ib.Outside_Boundary_Condition == 'Adiabatic':
295 temp_name = 'Adiabatic'
296 else:
297 temp_name = ''
298 surface_type = ib.Surface_Type
299 if ib.Construction_Name == '_AirWall':
300 surface_type = ib.Construction_Name
301 temp_name = temp_name + surface_type
302 if ib.Surface_Type == 'Wall':
303 temp_name = temp_name + "_" + PostprocessingUtils.azimuth_orientations[key]
304 if temp_name not in temp_name_list:
305 temp_name_list.append(temp_name)
306 else:
307 temp_count = len([True for s in temp_name_list if
308 temp_name in s])
309 temp_name = temp_name + "_" + str(temp_count)
310 space_bounds_renamed[ib.Name] = temp_name
311 break
312 else:
313 continue
314 return space_bounds_renamed