Coverage for bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/task/ep_create_idf.py: 0%
915 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-17 11:04 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-17 11:04 +0000
1from __future__ import annotations
3import logging
4import math
5import os
6from pathlib import Path, PosixPath
7from typing import Union, TYPE_CHECKING
9import pandas as pd
10from OCC.Core.BRep import BRep_Tool
11from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace
12from OCC.Core.BRepTools import breptools_UVBounds, BRepTools_WireExplorer
13from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_WIRE
14from OCC.Core.TopExp import TopExp_Explorer
15from OCC.Core.TopoDS import topods_Face, topods_Wire
16from OCC.Core._Geom import Handle_Geom_Plane_DownCast
18from OCC.Core.gp import gp_Dir, gp_XYZ, gp_Pln
19from geomeppy import IDF
21from bim2sim.elements.base_elements import IFCBased
22from bim2sim.elements.bps_elements import (ExternalSpatialElement,
23 SpaceBoundary2B, ThermalZone, Storey,
24 Layer, Window, SpaceBoundary, Wall,
25 Door, Roof, Slab, InnerFloor,
26 GroundFloor)
27from bim2sim.elements.mapping.units import ureg
28from bim2sim.project import FolderStructure
29from bim2sim.tasks.base import ITask
30from bim2sim.utilities.common_functions import filter_elements, \
31 get_spaces_with_bounds, all_subclasses
32from bim2sim.utilities.pyocc_tools import PyOCCTools
33from bim2sim.utilities.types import BoundaryOrientation
34from bim2sim.plugins.PluginEnergyPlus.bim2sim_energyplus.utils.utils_hash_function \
35 import generate_hash, add_hash_into_idf
37if TYPE_CHECKING:
38 from bim2sim.plugins.PluginEnergyPlus.bim2sim_energyplus import \
39 EnergyPlusSimSettings
41logger = logging.getLogger(__name__)
44class CreateIdf(ITask):
45 """Create an EnergyPlus Input file.
47 Task to create an EnergyPlus Input file based on the for EnergyPlus
48 preprocessed space boundary geometries. See detailed explanation in the run
49 function below.
50 """
52 reads = ('elements', 'weather_file',)
53 touches = ('idf', 'sim_results_path')
55 def __init__(self, playground):
56 super().__init__(playground)
57 self.idf = None
58 self.hash_line = None
60 def run(self, elements: dict, weather_file: Path) -> tuple[IDF, Path]:
61 """Execute all methods to export an IDF from BIM2SIM.
63 This task includes all functions for exporting EnergyPlus Input files
64 (idf) based on the previously preprocessed SpaceBoundary geometry from
65 the ep_geom_preprocessing task. Geometric preprocessing (includes
66 EnergyPlus-specific space boundary enrichment) must be executed
67 before this task.
68 In this task, first, the IDF itself is initialized. Then, the zones,
69 materials and constructions, shadings and control parameters are set
70 in the idf. Within the export of the idf, the final mapping of the
71 bim2sim elements and the idf components is executed. Shading control
72 is added if required, and the ground temperature of the building
73 surrounding ground is set, as well as the output variables of the
74 simulation. Finally, the generated idf is validated, and minor
75 corrections are performed, e.g., tiny surfaces are deleted that
76 would cause errors during the EnergyPlus Simulation run.
78 Args:
79 elements (dict): dictionary in the format dict[guid: element],
80 holds preprocessed elements including space boundaries.
81 weather_file (Path): path to weather file in .epw data format
82 Returns:
83 idf (IDF): EnergyPlus input file
84 sim_results_path (Path): path to the simulation results.
85 """
86 logger.info("IDF generation started ...")
87 idf, sim_results_path = self.init_idf(self.playground.sim_settings,
88 self.paths, weather_file,
89 self.prj_name)
90 self.init_zone(self.playground.sim_settings, elements, idf)
91 self.init_zonegroups(elements, idf)
92 self.get_preprocessed_materials_and_constructions(
93 self.playground.sim_settings, elements, idf)
94 if self.playground.sim_settings.add_shadings:
95 self.add_shadings(elements, idf)
96 self.set_simulation_control(self.playground.sim_settings, idf)
97 idf.set_default_constructions()
98 self.export_geom_to_idf(self.playground.sim_settings, elements, idf)
99 if self.playground.sim_settings.add_window_shading:
100 self.add_shading_control(
101 self.playground.sim_settings.add_window_shading, elements,
102 idf)
103 self.set_ground_temperature(idf, t_ground=get_spaces_with_bounds(
104 elements)[0].t_ground) # assuming all zones have same ground
105 self.set_output_variables(idf, self.playground.sim_settings)
106 self.idf_validity_check(idf)
107 logger.info("Save idf ...")
108 idf.save(idf.idfname)
109 if self.playground.sim_settings.add_hash:
110 logger.info("Generate IFC geometry hash ...")
111 self.hash_line = generate_hash(ifc_path=self.paths.ifc_base / 'arch' / str(self.prj_name + '.ifc'))
112 add_hash_into_idf(self.hash_line, idf.idfname)
113 logger.info("Idf file successfully saved.")
114 if (self.playground.sim_settings.weather_file_for_sizing or
115 self.playground.sim_settings.enforce_system_sizing):
116 # apply HVAC system sizing based on weather file
117 if self.playground.sim_settings.weather_file_for_sizing:
118 weather_file_sizing = (
119 self.playground.sim_settings.weather_file_for_sizing)
120 else:
121 weather_file_sizing = str(weather_file)
122 self.apply_system_sizing(
123 idf, weather_file_sizing,
124 sim_results_path)
125 logger.info("Idf has been updated with limits from weather file "
126 "sizing.")
128 return idf, sim_results_path
130 def apply_system_sizing(self, idf, sizing_weather_file, sim_results_path):
131 """
132 Apply system sizing based on weather file, sizes for maximum without
133 buffer.
135 Args:
136 idf: Eppy IDF
137 sizing_weather_file: Weather file for system sizing
138 sim_results_path: path to energyplus simulation results.
140 Returns:
142 """
143 IDF.setiddname(
144 self.playground.sim_settings.ep_install_path / 'Energy+.idd')
145 export_path = sim_results_path / self.prj_name
147 # initialize the idf with a minimal idf setup
148 idf2 = IDF(export_path / str(self.prj_name + '.idf'))
149 idf2.save(export_path / str(self.prj_name + '_before_sizing.idf'))
150 idf2.save(export_path / str(self.prj_name + '_sizing.idf'))
151 idf3 = IDF(export_path / str(self.prj_name + '_sizing.idf'))
152 idf3.removeallidfobjects('OUTPUT:VARIABLE')
153 idf3.newidfobject(
154 "OUTPUT:VARIABLE",
155 Variable_Name="Zone Ideal Loads Supply Air Total Cooling Rate",
156 Reporting_Frequency="Hourly",
157 )
158 idf3.newidfobject(
159 "OUTPUT:VARIABLE",
160 Variable_Name="Zone Ideal Loads Supply Air Total Heating Rate",
161 Reporting_Frequency="Hourly",
162 )
163 idf3.epw = sizing_weather_file
164 for sim_control in idf3.idfobjects["SIMULATIONCONTROL"]:
165 sim_control.Run_Simulation_for_Sizing_Periods = 'Yes'
166 sim_control.Run_Simulation_for_Weather_File_Run_Periods = 'No'
167 idf3.run(output_directory=export_path, readvars=True, annual=False)
168 res_sizing = pd.read_csv(export_path / 'epluszsz.csv')
169 res_sizing = res_sizing.set_index('Time')
170 peak = res_sizing.loc['Peak']
171 peak_heating = peak.filter(like='Des Heat Load')
172 peak_cooling = peak.filter(like='Des Sens Cool Load')
173 for obj in idf.idfobjects['HVACTEMPLATE:ZONE:IDEALLOADSAIRSYSTEM']:
174 curr_heating = peak_heating.filter(like=obj.Zone_Name.upper()).max()
175 curr_cooling = peak_cooling.filter(like=obj.Zone_Name.upper()).max()
176 obj.Heating_Limit = 'LimitCapacity'
177 obj.Cooling_Limit = 'LimitCapacity'
178 obj.Maximum_Sensible_Heating_Capacity = curr_heating
179 obj.Maximum_Total_Cooling_Capacity = curr_cooling
180 for sim_control in idf.idfobjects["SIMULATIONCONTROL"]:
181 sim_control.Do_System_Sizing_Calculation = 'Yes'
182 idf.save(idf.idfname)
184 @staticmethod
185 def init_idf(sim_settings: EnergyPlusSimSettings, paths: FolderStructure,
186 weather_file: PosixPath, ifc_name: str) -> tuple[IDF, Path]:
187 """ Initialize the EnergyPlus input file.
189 Initialize the EnergyPlus input file (idf) with general idf settings
190 and set default weather
191 data.
193 Args:
194 sim_settings: EnergyPlusSimSettings
195 paths: BIM2SIM FolderStructure
196 weather_file: PosixPath to *.epw weather file
197 ifc_name: str of name of ifc
198 Returns:
199 idf file of type IDF, sim_results_path
200 """
201 logger.info("Initialize the idf ...")
202 # set the installation path for the EnergyPlus installation
203 ep_install_path = sim_settings.ep_install_path
204 # set the plugin path of the PluginEnergyPlus within the BIM2SIM Tool
205 plugin_ep_path = str(Path(__file__).parent.parent.parent)
206 # set Energy+.idd as base for new idf
207 IDF.setiddname(ep_install_path / 'Energy+.idd')
208 # initialize the idf with a minimal idf setup
209 idf = IDF(plugin_ep_path + '/data/Minimal.idf')
210 # remove location and design days
211 idf.removeallidfobjects('SIZINGPERIOD:DESIGNDAY')
212 idf.removeallidfobjects('SITE:LOCATION')
213 if sim_settings.system_weather_sizing != 'DesignDay':
214 # enable system sizing for extreme or typical days.
215 if sim_settings.system_weather_sizing == 'Extreme':
216 period_selection = 'Extreme'
217 elif sim_settings.system_weather_sizing == 'Typical':
218 period_selection = 'Typical'
219 idf.newidfobject("SIZINGPERIOD:WEATHERFILECONDITIONTYPE",
220 Name='Summer Design Day from Weather File',
221 Period_Selection=f'Summer{period_selection}',
222 Day_of_Week_for_Start_Day='SummerDesignDay'
223 )
224 idf.newidfobject("SIZINGPERIOD:WEATHERFILECONDITIONTYPE",
225 Name='Winter Design Day from Weather File',
226 Period_Selection=f'Winter{period_selection}',
227 Day_of_Week_for_Start_Day='WinterDesignDay'
228 )
229 else:
230 # use default Design day (July 21, December 21) for system sizing
231 idf.newidfobject("SIZINGPERIOD:WEATHERFILEDAYS",
232 Name='Summer Design Day from Weather File',
233 Begin_Month=7,
234 Begin_Day_of_Month=21,
235 End_Month=7,
236 End_Day_of_Month=21,
237 Day_of_Week_for_Start_Day='SummerDesignDay'
238 )
239 idf.newidfobject("SIZINGPERIOD:WEATHERFILEDAYS",
240 Name='Winter Design Day from Weather File',
241 Begin_Month=12,
242 Begin_Day_of_Month=21,
243 End_Month=12,
244 End_Day_of_Month=21,
245 Day_of_Week_for_Start_Day='WinterDesignDay'
246 )
248 sim_results_path = paths.export/'EnergyPlus'/'SimResults'
249 export_path = sim_results_path / ifc_name
250 if not os.path.exists(export_path):
251 os.makedirs(export_path)
252 idf.idfname = export_path / str(ifc_name + '.idf')
253 # load and set basic compact schedules and ScheduleTypeLimits
254 schedules_idf = IDF(plugin_ep_path + '/data/Schedules.idf')
255 schedules = schedules_idf.idfobjects["Schedule:Compact".upper()]
256 sch_typelim = schedules_idf.idfobjects["ScheduleTypeLimits".upper()]
257 for s in schedules:
258 idf.copyidfobject(s)
259 for t in sch_typelim:
260 idf.copyidfobject(t)
261 # set weather file
262 idf.epw = str(weather_file)
263 return idf, sim_results_path
265 def init_zone(self, sim_settings: EnergyPlusSimSettings, elements: dict,
266 idf: IDF):
267 """Initialize zone settings.
269 Creates one idf zone per space and sets heating and cooling
270 templates, infiltration and internal loads (occupancy (people),
271 equipment, lighting).
273 Args:
274 sim_settings: BIM2SIM simulation settings
275 elements: dict[guid: element]
276 idf: idf file object
277 """
278 logger.info("Init thermal zones ...")
279 spaces = get_spaces_with_bounds(elements)
280 for space in spaces:
281 if space.space_shape_volume:
282 volume = space.space_shape_volume.to(ureg.meter ** 3).m
283 # for some shapes, shape volume calculation might not work
284 else:
285 volume = space.volume.to(ureg.meter ** 3).m
286 zone = idf.newidfobject(
287 'ZONE',
288 Name=space.ifc.GlobalId,
289 Volume=volume
290 )
291 self.set_heating_and_cooling(idf, zone_name=zone.Name, space=space)
292 self.set_infiltration(
293 idf, name=zone.Name, zone_name=zone.Name, space=space,
294 ep_version=sim_settings.ep_version)
295 if (not space.with_cooling and
296 self.playground.sim_settings.add_natural_ventilation):
297 self.set_natural_ventilation(
298 idf, name=zone.Name, zone_name=zone.Name, space=space,
299 ep_version=sim_settings.ep_version,
300 residential=sim_settings.residential,
301 ventilation_method=
302 sim_settings.natural_ventilation_approach)
303 self.set_people(sim_settings, idf, name=zone.Name,
304 zone_name=zone.Name, space=space)
305 self.set_equipment(sim_settings, idf, name=zone.Name,
306 zone_name=zone.Name, space=space)
307 self.set_lights(sim_settings, idf, name=zone.Name, zone_name=zone.Name,
308 space=space)
309 if sim_settings.building_rotation_overwrite != 0:
310 idf.idfobjects['BUILDING'][0].North_Axis = (
311 sim_settings.building_rotation_overwrite)
312 idf.idfobjects['GLOBALGEOMETRYRULES'][0].Coordinate_System =\
313 'Relative'
315 @staticmethod
316 def init_zonelist(
317 idf: IDF,
318 name: str = None,
319 zones_in_list: list[str] = None):
320 """Initialize zone lists.
322 Inits a list of zones in the idf. If the zones_in_list is not set,
323 all zones are assigned to a general zone, unless the number of total
324 zones is greater than 20 (max. allowed number of zones in a zonelist
325 in an idf).
327 Args:
328 idf: idf file object
329 name: str with name of zone list
330 zones_in_list: list with the guids of the zones to be included in
331 the list
332 """
333 if zones_in_list is None:
334 # assign all zones to one list unless the total number of zones
335 # is larger than 20.
336 idf_zones = idf.idfobjects["ZONE"]
337 if len(idf_zones) > 99:
338 logger.warning("Trying to assign more than 99 zones to a "
339 "single zone list. May require changes in "
340 "Energy+.idd to successfully execute "
341 "simulation.")
342 else:
343 # assign all zones with the zone names that are included in
344 # zones_in_list to the zonelist.
345 all_idf_zones = idf.idfobjects["ZONE"]
346 idf_zones = [zone for zone in all_idf_zones if zone.Name
347 in zones_in_list]
348 if len(idf_zones) > 99:
349 logger.warning("Trying to assign more than 99 zones to a "
350 "single zone list. May require changes in "
351 "Energy+.idd to successfully execute "
352 "simulation.")
353 if len(idf_zones) == 0:
354 return
355 if name is None:
356 name = "All_Zones"
357 zs = {}
358 for i, z in enumerate(idf_zones):
359 zs.update({"Zone_" + str(i + 1) + "_Name": z.Name})
360 idf.newidfobject("ZONELIST", Name=name, **zs)
362 def init_zonegroups(self, elements: dict, idf: IDF):
363 """Assign one zonegroup per storey.
365 Args:
366 elements: dict[guid: element]
367 idf: idf file object
368 """
369 spaces = get_spaces_with_bounds(elements)
370 space_usage_dict = {}
371 for space in spaces:
372 if space.usage.replace(',', '') not in space_usage_dict.keys():
373 space_usage_dict.update({space.usage.replace(',',
374 ''): [space.guid]})
375 else:
376 space_usage_dict[space.usage.replace(',', '')].append(
377 space.guid)
379 for key, value in space_usage_dict.items():
380 if not idf.getobject('ZONELIST', key):
381 self.init_zonelist(idf, name=key, zones_in_list=value)
382 # add zonelist for All_Zones
383 zone_lists = [zlist for zlist in idf.idfobjects["ZONELIST"]
384 if zlist.Name != "All_Zones"]
386 # add zonegroup for each zonegroup in zone_lists.
387 for zlist in zone_lists:
388 idf.newidfobject("ZONEGROUP",
389 Name=zlist.Name,
390 Zone_List_Name=zlist.Name,
391 Zone_List_Multiplier=1
392 )
393 @staticmethod
394 def check_preprocessed_materials_and_constructions(rel_elem, layers):
395 """Check if preprocessed materials and constructions are valid."""
396 correct_preprocessing = False
397 # check if thickness and material parameters are available from
398 # preprocessing
399 if all(layer.thickness for layer in layers):
400 for layer in rel_elem.layerset.layers:
401 if None in (layer.material.thermal_conduc,
402 layer.material.spec_heat_capacity,
403 layer.material.density):
404 return correct_preprocessing
405 elif 0 in (layer.material.thermal_conduc.m,
406 layer.material.spec_heat_capacity.m,
407 layer.material.density.m):
408 return correct_preprocessing
409 else:
410 pass
412 correct_preprocessing = True
414 return correct_preprocessing
416 def get_preprocessed_materials_and_constructions(
417 self, sim_settings: EnergyPlusSimSettings, elements: dict, idf: IDF):
418 """Get preprocessed materials and constructions.
420 This function sets preprocessed construction and material for
421 building surfaces and fenestration. For virtual bounds, an air
422 boundary construction is set.
424 Args:
425 sim_settings: BIM2SIM simulation settings
426 elements: dict[guid: element]
427 idf: idf file object
428 """
429 logger.info("Get predefined materials and construction ...")
430 bounds = filter_elements(elements, 'SpaceBoundary')
431 for bound in bounds:
432 rel_elem = bound.bound_element
433 if not rel_elem:
434 continue
435 if not any([isinstance(rel_elem, window) for window in
436 all_subclasses(Window, include_self=True)]):
437 # set construction for all but fenestration
438 if self.check_preprocessed_materials_and_constructions(
439 rel_elem, rel_elem.layerset.layers):
440 self.set_preprocessed_construction_elem(
441 rel_elem, rel_elem.layerset.layers, idf)
442 for layer in rel_elem.layerset.layers:
443 self.set_preprocessed_material_elem(layer, idf)
444 else:
445 logger.warning("No preprocessed construction and "
446 "material found for space boundary %s on "
447 "related building element %s. Using "
448 "default values instead.",
449 bound.guid, rel_elem.guid)
450 else:
451 # set construction elements for windows
452 self.set_preprocessed_window_material_elem(
453 rel_elem, idf, sim_settings.add_window_shading)
455 # Add air boundaries as construction as a material for virtual bounds
456 if sim_settings.ep_version in ["9-2-0", "9-4-0"]:
457 idf.newidfobject("CONSTRUCTION:AIRBOUNDARY",
458 Name='Air Wall',
459 Solar_and_Daylighting_Method='GroupedZones',
460 Radiant_Exchange_Method='GroupedZones',
461 Air_Exchange_Method='SimpleMixing',
462 Simple_Mixing_Air_Changes_per_Hour=0.5,
463 )
464 else:
465 idf.newidfobject("CONSTRUCTION:AIRBOUNDARY",
466 Name='Air Wall',
467 Air_Exchange_Method='SimpleMixing',
468 Simple_Mixing_Air_Changes_per_Hour=0.5,
469 )
471 @staticmethod
472 def set_preprocessed_construction_elem(
473 rel_elem: IFCBased,
474 layers: list[Layer],
475 idf: IDF):
476 """Write preprocessed constructions to idf.
478 This function uses preprocessed data to define idf construction
479 elements.
481 Args:
482 rel_elem: any subclass of IFCBased (e.g., Wall)
483 layers: list of Layer
484 idf: idf file object
485 """
486 construction_name = (rel_elem.key.replace('Disaggregated', '') + '_'
487 + str(len(layers)) + '_' + '_' \
488 .join([str(l.thickness.to(ureg.metre).m) for l in layers]))
489 # todo: find a unique key for construction name
490 if idf.getobject("CONSTRUCTION", construction_name) is None:
491 outer_layer = layers[-1]
492 other_layer_list = layers[:-1]
493 other_layer_list.reverse()
494 other_layers = {}
495 for i, l in enumerate(other_layer_list):
496 other_layers.update(
497 {'Layer_' + str(i + 2): l.material.name + "_" + str(
498 l.thickness.to(ureg.metre).m)})
499 idf.newidfobject("CONSTRUCTION",
500 Name=construction_name,
501 Outside_Layer=outer_layer.material.name + "_" +
502 str(outer_layer.thickness.to(
503 ureg.metre).m),
504 **other_layers
505 )
507 @staticmethod
508 def set_preprocessed_material_elem(layer: Layer, idf: IDF):
509 """Set a preprocessed material element.
511 Args:
512 layer: Layer Instance
513 idf: idf file object
514 """
515 material_name = layer.material.name + "_" + str(
516 layer.thickness.to(ureg.metre).m)
517 if idf.getobject("MATERIAL", material_name):
518 return
519 specific_heat = \
520 layer.material.spec_heat_capacity.to(ureg.joule / ureg.kelvin /
521 ureg.kilogram).m
522 if specific_heat < 100:
523 specific_heat = 100
524 conductivity = layer.material.thermal_conduc.to(
525 ureg.W / (ureg.m * ureg.K)).m
526 density = layer.material.density.to(ureg.kg / ureg.m ** 3).m
527 if conductivity == 0:
528 logger.error(f"Conductivity of {layer.material} is 0. Simulation "
529 f"will crash, please correct input or resulting idf "
530 f"file.")
531 if density == 0:
532 logger.error(f"Density of {layer.material} is 0. Simulation "
533 f"will crash, please correct input or resulting idf "
534 f"file.")
535 idf.newidfobject("MATERIAL",
536 Name=material_name,
537 Roughness="MediumRough",
538 Thickness=layer.thickness.to(ureg.metre).m,
539 Conductivity=conductivity,
540 Density=density,
541 Specific_Heat=specific_heat
542 )
544 @staticmethod
545 def set_preprocessed_window_material_elem(rel_elem: Window,
546 idf: IDF,
547 add_window_shading: False):
548 """Set preprocessed window material.
550 This function constructs windows with a
551 WindowMaterial:SimpleGlazingSystem consisting of the outermost layer
552 of the providing related element. This is a simplification, needs to
553 be extended to hold multilayer window constructions.
555 Args:
556 rel_elem: Window instance
557 idf: idf file object
558 add_window_shading: Add window shading (options: 'None',
559 'Interior', 'Exterior')
560 """
561 material_name = \
562 'WM_' + rel_elem.layerset.layers[0].material.name + '_' \
563 + str(rel_elem.layerset.layers[0].thickness.to(ureg.m).m)
564 if idf.getobject("WINDOWMATERIAL:SIMPLEGLAZINGSYSTEM", material_name):
565 return
566 if rel_elem.u_value.to(ureg.W / ureg.K / ureg.meter ** 2).m > 0:
567 ufactor = 1 / (0.04 + 1 / rel_elem.u_value.to(ureg.W / ureg.K /
568 ureg.meter ** 2).m
569 + 0.13)
570 else:
571 ufactor = 1 / (0.04 + rel_elem.layerset.layers[0].thickness.to(
572 ureg.metre).m /
573 rel_elem.layerset.layers[
574 0].material.thermal_conduc.to(
575 ureg.W / (ureg.m * ureg.K)).m +
576 0.13)
577 if rel_elem.g_value >= 1:
578 old_g_value = rel_elem.g_value
579 rel_elem.g_value = 0.999
580 logger.warning("G-Value was set to %f, "
581 "but has to be smaller than 1, so overwritten by %f",
582 old_g_value, rel_elem.g_value)
584 idf.newidfobject("WINDOWMATERIAL:SIMPLEGLAZINGSYSTEM",
585 Name=material_name,
586 UFactor=ufactor,
587 Solar_Heat_Gain_Coefficient=rel_elem.g_value,
588 # Visible_Transmittance=0.8 # optional
589 )
590 if add_window_shading:
591 default_shading_name = "DefaultWindowShade"
592 if not idf.getobject("WINDOWMATERIAL:SHADE", default_shading_name):
593 idf.newidfobject("WINDOWMATERIAL:SHADE",
594 Name=default_shading_name,
595 Solar_Transmittance=0.3,
596 Solar_Reflectance=0.5,
597 Visible_Transmittance=0.3,
598 Visible_Reflectance=0.5,
599 Infrared_Hemispherical_Emissivity=0.9,
600 Infrared_Transmittance=0.05,
601 Thickness=0.003,
602 Conductivity=0.1)
603 construction_name = 'Window_' + material_name + "_" \
604 + add_window_shading
605 if idf.getobject("CONSTRUCTION", construction_name) is None:
606 if add_window_shading == 'Interior':
607 idf.newidfobject("CONSTRUCTION",
608 Name=construction_name,
609 Outside_Layer=material_name,
610 Layer_2=default_shading_name
611 )
612 else:
613 idf.newidfobject("CONSTRUCTION",
614 Name=construction_name,
615 Outside_Layer=default_shading_name,
616 Layer_2=material_name
617 )
618 # todo: enable use of multilayer windows
619 # set construction without shading anyways
620 construction_name = 'Window_' + material_name
621 if idf.getobject("CONSTRUCTION", construction_name) is None:
622 idf.newidfobject("CONSTRUCTION",
623 Name=construction_name,
624 Outside_Layer=material_name
625 )
627 def set_heating_and_cooling(self, idf: IDF, zone_name: str,
628 space: ThermalZone):
629 """Set heating and cooling parameters.
631 This function sets heating and cooling parameters based on the data
632 available from BIM2SIM Preprocessing (either IFC-based or
633 Template-based).
635 Args:
636 idf: idf file object
637 zone_name: str
638 space: ThermalZone instance
639 """
640 stat_name = "STATS " + space.usage.replace(',', '')
641 if self.playground.sim_settings.control_operative_temperature:
642 # set control for operative temperature instead of air temperature
643 operative_stats_name = space.usage.replace(',', '') + ' THERMOSTAT'
644 htg_schedule_name = "Schedule " + "Heating " + space.usage.replace(
645 ',', '')
646 self.set_day_week_year_schedule(idf, space.heating_profile[:24],
647 'heating_profile',
648 htg_schedule_name)
649 clg_schedule_name = "Schedule " + "Cooling " + space.usage.replace(
650 ',', '')
651 self.set_day_week_year_schedule(idf, space.cooling_profile[:24],
652 'cooling_profile',
653 clg_schedule_name)
654 if idf.getobject('THERMOSTATSETPOINT:DUALSETPOINT',
655 htg_schedule_name + ' ' + clg_schedule_name) is None:
656 idf.newidfobject(
657 'THERMOSTATSETPOINT:DUALSETPOINT',
658 Name=htg_schedule_name + ' ' + clg_schedule_name,
659 Heating_Setpoint_Temperature_Schedule_Name
660 =htg_schedule_name,
661 Cooling_Setpoint_Temperature_Schedule_Name=clg_schedule_name
662 )
663 if idf.getobject("ZONECONTROL:THERMOSTAT:OPERATIVETEMPERATURE",
664 zone_name + ' ' + operative_stats_name) is None:
665 idf.newidfobject(
666 "ZONECONTROL:THERMOSTAT:OPERATIVETEMPERATURE",
667 Thermostat_Name=zone_name + ' ' + operative_stats_name,
668 Radiative_Fraction_Input_Mode='constant',
669 Fixed_Radiative_Fraction=0.5
670 )
671 if idf.getobject("ZONECONTROL:THERMOSTAT",
672 operative_stats_name) is None:
673 stat = idf.newidfobject(
674 "ZONECONTROL:THERMOSTAT",
675 Name=operative_stats_name,
676 Zone_or_ZoneList_Name=space.usage.replace(',', ''),
677 Control_Type_Schedule_Name='Zone Control Type Sched',
678 Control_1_Object_Type='ThermostatSetpoint:DualSetpoint',
679 Control_1_Name=htg_schedule_name + ' ' + clg_schedule_name,
680 # Temperature_Difference_Between_Cutout_And_Setpoint=0.2,
681 # temperature difference disables operative control
682 )
683 else:
684 stat = idf.getobject('ZONECONTROL:THERMOSTAT',
685 operative_stats_name)
686 if idf.getobject("SCHEDULE:COMPACT",
687 'Zone Control Type Sched') is None:
688 idf.newidfobject("SCHEDULE:COMPACT",
689 Name='Zone Control Type Sched',
690 Schedule_Type_Limits_Name='Control Type',
691 Field_1='Through: 12/31',
692 Field_2='For: AllDays',
693 Field_3='Until: 24:00',
694 Field_4='4',
695 )
696 if idf.getobject("SCHEDULETYPELIMITS", 'Control Type') is None:
697 idf.newidfobject('SCHEDULETYPELIMITS',
698 Name='Control Type',
699 Lower_Limit_Value=0,
700 Upper_Limit_Value=4,
701 Numeric_Type='DISCRETE')
702 template_thermostat_name = ''
703 elif idf.getobject("HVACTEMPLATE:THERMOSTAT", stat_name) is None:
704 # if air temperature is controlled, create thermostat if it is
705 # not available
706 stat = self.set_day_hvac_template(idf, space, stat_name)
707 template_thermostat_name = stat.Name
708 else:
709 # assign available thermostats for air control
710 stat = idf.getobject("HVACTEMPLATE:THERMOSTAT", stat_name)
711 template_thermostat_name = stat.Name
713 # initialize heating and cooling availability, and capacity
714 cooling_availability = "Off"
715 heating_availability = "Off"
716 heating_limit = 'NoLimit'
717 cooling_limit = 'NoLimit'
718 maximum_cooling_capacity = 'autosize'
719 maximum_cooling_rate = 'autosize'
720 outdoor_air_method = 'None'
721 # convert from l/s to m3/s
722 outdoor_air_person = (
723 self.playground.sim_settings.outdoor_air_per_person) / 1000
724 outdoor_air_area = (
725 self.playground.sim_settings.outdoor_air_per_area) / 1000
726 ventilation_demand_control = 'None'
727 outdoor_air_economizer = 'NoEconomizer'
728 heat_recovery_type = 'None'
729 heat_recovery_sensible = (
730 self.playground.sim_settings.heat_recovery_sensible)
731 heat_recovery_latent = self.playground.sim_settings.heat_recovery_latent
733 # initialize night setback if required
734 if self.playground.sim_settings.hvac_off_at_night and idf.getobject(
735 "SCHEDULE:COMPACT", "On_except_10pm_to_6am") is None:
736 idf.newidfobject(
737 "SCHEDULE:COMPACT",
738 Name='On_except_10pm_to_6am',
739 Schedule_Type_Limits_Name='on/off',
740 Field_1='Through: 12/31',
741 Field_2='For: AllDays',
742 Field_3='Until: 06:00',
743 Field_4='0',
744 Field_5='Until: 22:00',
745 Field_6='1',
746 Field_7='Until: 24:00',
747 Field_8='0'
748 )
750 # overwrite heating / cooling availability if required
751 if space.with_cooling:
752 cooling_availability = "On"
753 if self.playground.sim_settings.hvac_off_at_night:
754 cooling_availability = 'On_except_10pm_to_6am'
755 cooling_limit = 'LimitFlowRateAndCapacity'
757 if space.with_heating:
758 heating_availability = "On"
759 if self.playground.sim_settings.hvac_off_at_night:
760 heating_availability = 'On_except_10pm_to_6am'
762 if space.with_cooling or \
763 not self.playground.sim_settings.add_natural_ventilation:
764 outdoor_air_method = 'Sum'
765 ventilation_demand_control = (
766 str(self.playground.sim_settings.ventilation_demand_control))
767 outdoor_air_economizer = (
768 self.playground.sim_settings.outdoor_air_economizer)
769 heat_recovery_type = self.playground.sim_settings.heat_recovery_type
771 # initialize ideal loads air system according to the settings
772 idf.newidfobject(
773 "HVACTEMPLATE:ZONE:IDEALLOADSAIRSYSTEM",
774 Zone_Name=zone_name,
775 Template_Thermostat_Name=template_thermostat_name,
776 Heating_Availability_Schedule_Name=heating_availability,
777 Cooling_Availability_Schedule_Name=cooling_availability,
778 Heating_Limit=heating_limit,
779 Cooling_Limit=cooling_limit,
780 Maximum_Cooling_Air_Flow_Rate=maximum_cooling_rate,
781 Maximum_Total_Cooling_Capacity=maximum_cooling_capacity,
782 Outdoor_Air_Method=outdoor_air_method,
783 Outdoor_Air_Flow_Rate_per_Person=outdoor_air_person,
784 Outdoor_Air_Flow_Rate_per_Zone_Floor_Area=outdoor_air_area,
785 Demand_Controlled_Ventilation_Type=ventilation_demand_control,
786 Outdoor_Air_Economizer_Type=outdoor_air_economizer,
787 Heat_Recovery_Type=heat_recovery_type,
788 Sensible_Heat_Recovery_Effectiveness=heat_recovery_sensible,
789 Latent_Heat_Recovery_Effectiveness=heat_recovery_latent,
790 )
792 def set_people(self, sim_settings: EnergyPlusSimSettings, idf: IDF, name: str,
793 zone_name: str, space: ThermalZone):
794 """Set occupancy schedules.
796 This function sets schedules and internal loads from people (occupancy)
797 based on the BIM2SIM Preprocessing, i.e. based on IFC data if
798 available or on templates.
800 Args:
801 sim_settings: BIM2SIM simulation settings
802 idf: idf file object
803 name: name of the new people idf object
804 zone_name: name of zone or zone_list
805 space: ThermalZone instance
806 """
807 schedule_name = "Schedule " + "People " + space.usage.replace(',', '')
808 profile_name = 'persons_profile'
809 self.set_day_week_year_schedule(idf, space.persons_profile[:24],
810 profile_name, schedule_name)
811 # set default activity schedule
812 if idf.getobject("SCHEDULETYPELIMITS", "Any Number") is None:
813 idf.newidfobject("SCHEDULETYPELIMITS", Name="Any Number")
814 activity_schedule_name = "Schedule Activity " + str(
815 space.fixed_heat_flow_rate_persons)
816 if idf.getobject("SCHEDULE:COMPACT", activity_schedule_name) is None:
817 idf.newidfobject("SCHEDULE:COMPACT",
818 Name=activity_schedule_name,
819 Schedule_Type_Limits_Name="Any Number",
820 Field_1="Through: 12/31",
821 Field_2="For: Alldays",
822 Field_3="Until: 24:00",
823 Field_4=space.fixed_heat_flow_rate_persons.to(
824 ureg.watt).m#*1.8 # in W/Person
825 ) # other method for Field_4 (not used here)
826 # ="persons_profile"*"activity_degree_persons"*58,1*1,8
827 # (58.1 W/(m2*met), 1.8m2/Person)
828 if sim_settings.ep_version in ["9-2-0", "9-4-0"]:
829 idf.newidfobject(
830 "PEOPLE",
831 Name=name,
832 Zone_or_ZoneList_Name=zone_name,
833 Number_of_People_Calculation_Method="People/Area",
834 People_per_Zone_Floor_Area=space.persons,
835 Activity_Level_Schedule_Name=activity_schedule_name,
836 Number_of_People_Schedule_Name=schedule_name,
837 Fraction_Radiant=space.ratio_conv_rad_persons
838 )
839 else:
840 idf.newidfobject(
841 "PEOPLE",
842 Name=name,
843 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name,
844 Number_of_People_Calculation_Method="People/Area",
845 People_per_Floor_Area=space.persons,
846 Activity_Level_Schedule_Name=activity_schedule_name,
847 Number_of_People_Schedule_Name=schedule_name,
848 Fraction_Radiant=space.ratio_conv_rad_persons
849 )
851 @staticmethod
852 def set_day_week_year_schedule(idf: IDF, schedule: list[float],
853 profile_name: str,
854 schedule_name: str):
855 """Set day, week and year schedule (hourly).
857 This function sets an hourly day, week and year schedule.
859 Args:
860 idf: idf file object
861 schedule: list of float values for the schedule (e.g.,
862 temperatures, loads)
863 profile_name: string
864 schedule_name: str
865 """
866 if idf.getobject("SCHEDULE:DAY:HOURLY", name=schedule_name) is None:
867 limits_name = 'Fraction'
868 hours = {}
869 if profile_name in {'heating_profile', 'cooling_profile'}:
870 limits_name = 'Temperature'
871 if idf.getobject("SCHEDULETYPELIMITS", "Temperature") is None:
872 idf.newidfobject("SCHEDULETYPELIMITS", Name="Temperature")
873 for i, l in enumerate(schedule[:24]):
874 if profile_name in {'heating_profile', 'cooling_profile'}:
875 # convert Kelvin to Celsius for EnergyPlus Export
876 if schedule[i] > 270:
877 schedule[i] = schedule[i] - 273.15
878 hours.update({'Hour_' + str(i + 1): schedule[i]})
879 idf.newidfobject("SCHEDULE:DAY:HOURLY", Name=schedule_name,
880 Schedule_Type_Limits_Name=limits_name, **hours)
881 if idf.getobject("SCHEDULE:WEEK:COMPACT", name=schedule_name) is None:
882 idf.newidfobject("SCHEDULE:WEEK:COMPACT", Name=schedule_name,
883 DayType_List_1="AllDays",
884 ScheduleDay_Name_1=schedule_name)
885 if idf.getobject("SCHEDULE:YEAR", name=schedule_name) is None:
886 idf.newidfobject("SCHEDULE:YEAR", Name=schedule_name,
887 Schedule_Type_Limits_Name=limits_name,
888 ScheduleWeek_Name_1=schedule_name,
889 Start_Month_1=1,
890 Start_Day_1=1,
891 End_Month_1=12,
892 End_Day_1=31)
894 def set_equipment(self, sim_settings: EnergyPlusSimSettings, idf: IDF,
895 name: str, zone_name: str,
896 space: ThermalZone):
897 """Set internal loads from equipment.
899 This function sets schedules and internal loads from equipment based
900 on the BIM2SIM Preprocessing, i.e. based on IFC data if available or on
901 templates.
903 Args:
904 sim_settings: BIM2SIM simulation settings
905 idf: idf file object
906 name: name of the new people idf object
907 zone_name: name of zone or zone_list
908 space: ThermalZone instance
909 """
910 schedule_name = "Schedule " + "Equipment " + space.usage.replace(',',
911 '')
912 profile_name = 'machines_profile'
913 self.set_day_week_year_schedule(idf, space.machines_profile[:24],
914 profile_name, schedule_name)
915 if sim_settings.ep_version in ["9-2-0", "9-4-0"]:
916 idf.newidfobject(
917 "ELECTRICEQUIPMENT",
918 Name=name,
919 Zone_or_ZoneList_Name=zone_name,
920 Schedule_Name=schedule_name,
921 Design_Level_Calculation_Method="Watts/Area",
922 Watts_per_Zone_Floor_Area=space.machines.to(
923 ureg.watt / ureg.meter ** 2).m
924 )
925 else:
926 idf.newidfobject(
927 "ELECTRICEQUIPMENT",
928 Name=name,
929 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name,
930 Schedule_Name=schedule_name,
931 Design_Level_Calculation_Method="Watts/Area",
932 Watts_per_Zone_Floor_Area=space.machines.to(
933 ureg.watt / ureg.meter ** 2).m
934 )
936 def set_lights(self, sim_settings: EnergyPlusSimSettings, idf: IDF, name: str,
937 zone_name: str, space: ThermalZone):
938 """Set internal loads from lighting.
940 This function sets schedules and lighting based on the
941 BIM2SIM Preprocessing, i.e. based on IFC data if available or on
942 templates.
944 Args:
945 sim_settings: BIM2SIM simulation settings
946 idf: idf file object
947 name: name of the new people idf object
948 zone_name: name of zone or zone_list
949 space: ThermalZone instance
950 """
951 schedule_name = "Schedule " + "Lighting " + space.usage.replace(',', '')
952 profile_name = 'lighting_profile'
953 self.set_day_week_year_schedule(idf, space.lighting_profile[:24],
954 profile_name, schedule_name)
955 mode = "Watts/Area"
956 watts_per_zone_floor_area = space.lighting_power.to(
957 ureg.watt / ureg.meter ** 2).m
958 return_air_fraction = 0.0
959 fraction_radiant = 0.42 # fraction radiant: cf. Table 1.28 in
960 # InputOutputReference EnergyPlus (Version 9.4.0), p. 506
961 fraction_visible = 0.18 # Todo: fractions do not match with .json
962 # Data. Maybe set by user-input later
963 if sim_settings.ep_version in ["9-2-0", "9-4-0"]:
964 idf.newidfobject(
965 "LIGHTS",
966 Name=name,
967 Zone_or_ZoneList_Name=zone_name,
968 Schedule_Name=schedule_name,
969 Design_Level_Calculation_Method=mode,
970 Watts_per_Zone_Floor_Area=watts_per_zone_floor_area,
971 Return_Air_Fraction=return_air_fraction,
972 Fraction_Radiant=fraction_radiant,
973 Fraction_Visible=fraction_visible
974 )
975 else:
976 idf.newidfobject(
977 "LIGHTS",
978 Name=name,
979 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name,
980 Schedule_Name=schedule_name,
981 Design_Level_Calculation_Method=mode,
982 Watts_per_Zone_Floor_Area=watts_per_zone_floor_area,
983 Return_Air_Fraction=return_air_fraction,
984 Fraction_Radiant=fraction_radiant,
985 Fraction_Visible=fraction_visible
986 )
988 @staticmethod
989 def set_infiltration(idf: IDF,
990 name: str, zone_name: str,
991 space: ThermalZone, ep_version: str):
992 """Set infiltration rate.
994 This function sets the infiltration rate per space based on the
995 BIM2SIM preprocessing values (IFC-based if available or
996 template-based).
998 Args:
999 idf: idf file object
1000 name: name of the new people idf object
1001 zone_name: name of zone or zone_list
1002 space: ThermalZone instance
1003 ep_version: Used version of EnergyPlus
1004 """
1005 if ep_version in ["9-2-0", "9-4-0"]:
1006 idf.newidfobject(
1007 "ZONEINFILTRATION:DESIGNFLOWRATE",
1008 Name=name,
1009 Zone_or_ZoneList_Name=zone_name,
1010 Schedule_Name="Continuous",
1011 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
1012 Air_Changes_per_Hour=space.base_infiltration
1013 )
1014 else:
1015 idf.newidfobject(
1016 "ZONEINFILTRATION:DESIGNFLOWRATE",
1017 Name=name,
1018 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name,
1019 Schedule_Name="Continuous",
1020 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
1021 Air_Changes_per_Hour=space.base_infiltration
1022 )
1024 @staticmethod
1025 def set_natural_ventilation(idf: IDF, name: str, zone_name: str,
1026 space: ThermalZone, ep_version,
1027 min_in_temp=22, residential=False,
1028 ventilation_method="Simple"):
1029 """Set natural ventilation.
1031 This function sets the natural ventilation per space based on the
1032 BIM2SIM preprocessing values (IFC-based if available or
1033 template-based). Natural ventilation is defined for winter, summer
1034 and overheating cases, setting the air change per hours and minimum
1035 and maximum outdoor temperature if applicable.
1037 Args:
1038 idf: idf file object
1039 name: name of the new people idf object
1040 zone_name: name of zone or zone_list
1041 space: ThermalZone instance
1042 ep_version: Used version of EnergyPlus
1044 """
1045 if ep_version in ["9-2-0", "9-4-0"]:
1046 if ventilation_method == 'DIN4108':
1047 space_volume = space.space_shape_volume.m
1048 space_area = space.net_area.m
1049 if residential:
1050 occupied_air_exchange = 0.5
1051 unoccupied_air_exchange = 0.5
1052 occupied_increased_exchange = 3
1053 unoccupied_increased_exchange = 2
1054 else:
1055 occupied_air_exchange = 4 * space_area/space_volume
1056 unoccupied_air_exchange = 0.24
1057 occupied_increased_exchange = 3
1058 unoccupied_increased_exchange = 2
1060 if residential:
1061 occupied_schedule_name = 'residential_occupied_hours'
1062 unoccupied_schedule_name = 'residential_unoccupied_hours'
1063 if idf.getobject("SCHEDULE:COMPACT",
1064 occupied_schedule_name) is None:
1065 idf.newidfobject("SCHEDULE:COMPACT",
1066 Name=occupied_schedule_name,
1067 Schedule_Type_Limits_Name='Continuous',
1068 Field_1='Through: 12/31',
1069 Field_2='For: AllDays',
1070 Field_3='Until: 6:00',
1071 Field_4='0',
1072 Field_5='Until: 23:00',
1073 Field_6='1',
1074 Field_7='Until: 24:00',
1075 Field_8='0',
1076 )
1077 if idf.getobject("SCHEDULE:COMPACT",
1078 unoccupied_schedule_name) is None:
1079 idf.newidfobject("SCHEDULE:COMPACT",
1080 Name=unoccupied_schedule_name,
1081 Schedule_Type_Limits_Name='Continuous',
1082 Field_1='Through: 12/31',
1083 Field_2='For: AllDays',
1084 Field_3='Until: 6:00',
1085 Field_4='1',
1086 Field_5='Until: 23:00',
1087 Field_6='0',
1088 Field_7='Until: 24:00',
1089 Field_8='1',
1090 )
1091 else:
1092 occupied_schedule_name = 'non_residential_occupied_hours'
1093 unoccupied_schedule_name = 'non_residential_unoccupied_hours'
1094 if idf.getobject("SCHEDULE:COMPACT",
1095 occupied_schedule_name) is None:
1096 idf.newidfobject("SCHEDULE:COMPACT",
1097 Name=occupied_schedule_name,
1098 Schedule_Type_Limits_Name='Continuous',
1099 Field_1='Through: 12/31',
1100 Field_2='For: AllDays',
1101 Field_3='Until: 7:00',
1102 Field_4='0',
1103 Field_5='Until: 18:00',
1104 Field_6='1',
1105 Field_7='Until: 24:00',
1106 Field_8='0',
1107 )
1108 if idf.getobject("SCHEDULE:COMPACT",
1109 unoccupied_schedule_name) is None:
1110 idf.newidfobject("SCHEDULE:COMPACT",
1111 Name=unoccupied_schedule_name,
1112 Schedule_Type_Limits_Name='Continuous',
1113 Field_1='Through: 12/31',
1114 Field_2='For: AllDays',
1115 Field_3='Until: 7:00',
1116 Field_4='1',
1117 Field_5='Until: 18:00',
1118 Field_6='0',
1119 Field_7='Until: 24:00',
1120 Field_8='1',
1121 )
1122 idf.newidfobject(
1123 "ZONEVENTILATION:DESIGNFLOWRATE",
1124 Name=name + '_occupied_ventilation',
1125 Zone_or_ZoneList_Name=zone_name,
1126 Schedule_Name=occupied_schedule_name,
1127 Ventilation_Type="Natural",
1128 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
1129 Air_Changes_per_Hour=occupied_air_exchange,
1130 )
1131 idf.newidfobject(
1132 "ZONEVENTILATION:DESIGNFLOWRATE",
1133 Name=name + '_occupied_ventilation_increased',
1134 Zone_or_ZoneList_Name=zone_name,
1135 Schedule_Name=occupied_schedule_name,
1136 Ventilation_Type="Natural",
1137 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
1138 Air_Changes_per_Hour=max(
1139 occupied_increased_exchange-occupied_air_exchange, 0),
1140 Minimum_Indoor_Temperature=23,
1141 Maximum_Indoor_Temperature=26,
1142 Minimum_Outdoor_Temperature=12,
1143 Delta_Temperature=0,
1144 )
1145 idf.newidfobject(
1146 "ZONEVENTILATION:DESIGNFLOWRATE",
1147 Name=name + '_occupied_ventilation_increased_overheating',
1148 Zone_or_ZoneList_Name=zone_name,
1149 Schedule_Name=occupied_schedule_name,
1150 Ventilation_Type="Natural",
1151 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
1152 Air_Changes_per_Hour=max(
1153 occupied_increased_exchange-occupied_air_exchange, 0),
1154 Minimum_Indoor_Temperature=26,
1155 Delta_Temperature=0,
1156 )
1157 idf.newidfobject(
1158 "ZONEVENTILATION:DESIGNFLOWRATE",
1159 Name=name + '_unoccupied_ventilation',
1160 Zone_or_ZoneList_Name=zone_name,
1161 Schedule_Name=unoccupied_schedule_name,
1162 Ventilation_Type="Natural",
1163 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
1164 Air_Changes_per_Hour=unoccupied_air_exchange,
1165 )
1166 idf.newidfobject(
1167 "ZONEVENTILATION:DESIGNFLOWRATE",
1168 Name=name + '_unoccupied_ventilation_increased',
1169 Zone_or_ZoneList_Name=zone_name,
1170 Schedule_Name=unoccupied_schedule_name,
1171 Ventilation_Type="Natural",
1172 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
1173 Air_Changes_per_Hour=max(
1174 unoccupied_increased_exchange-unoccupied_air_exchange, 0),
1175 Minimum_Indoor_Temperature=23,
1176 Maximum_Indoor_Temperature=26,
1177 Minimum_Outdoor_Temperature=15,
1178 Delta_Temperature=0,
1179 )
1180 idf.newidfobject(
1181 "ZONEVENTILATION:DESIGNFLOWRATE",
1182 Name=name + '_unoccupied_ventilation_increased_overheating',
1183 Zone_or_ZoneList_Name=zone_name,
1184 Schedule_Name=unoccupied_schedule_name,
1185 Ventilation_Type="Natural",
1186 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
1187 Air_Changes_per_Hour=max(
1188 unoccupied_increased_exchange-unoccupied_air_exchange, 0),
1189 Minimum_Indoor_Temperature=26,
1190 # Minimum_Outdoor_Temperature=10,
1191 Delta_Temperature=0,
1192 )
1193 else:
1194 # use bim2sim standard zone ventilation based on TEASER
1195 # templates
1196 idf.newidfobject(
1197 "ZONEVENTILATION:DESIGNFLOWRATE",
1198 Name=name + '_winter',
1199 Zone_or_ZoneList_Name=zone_name,
1200 Schedule_Name="Continuous",
1201 Ventilation_Type="Natural",
1202 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
1203 Air_Changes_per_Hour=space.winter_reduction_infiltration[0],
1204 Minimum_Outdoor_Temperature=
1205 space.winter_reduction_infiltration[1] - 273.15,
1206 Maximum_Outdoor_Temperature=
1207 space.winter_reduction_infiltration[2] - 273.15,
1208 )
1210 idf.newidfobject(
1211 "ZONEVENTILATION:DESIGNFLOWRATE",
1212 Name=name + '_summer',
1213 Zone_or_ZoneList_Name=zone_name,
1214 Schedule_Name="Continuous",
1215 Ventilation_Type="Natural",
1216 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
1217 Air_Changes_per_Hour=space.max_summer_infiltration[0],
1218 Minimum_Outdoor_Temperature
1219 =space.max_summer_infiltration[1] - 273.15,
1220 Maximum_Outdoor_Temperature
1221 =space.max_summer_infiltration[2] - 273.15,
1222 )
1224 idf.newidfobject(
1225 "ZONEVENTILATION:DESIGNFLOWRATE",
1226 Name=name + '_overheating',
1227 Zone_or_ZoneList_Name=zone_name,
1228 Schedule_Name="Continuous",
1229 Ventilation_Type="Natural",
1230 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
1231 # calculation of overheating infiltration is a simplification
1232 # compared to the corresponding TEASER implementation which
1233 # dynamically computes thresholds for overheating infiltration
1234 # based on the zone temperature and additional factors.
1235 Air_Changes_per_Hour=space.max_overheating_infiltration[0],
1236 Minimum_Outdoor_Temperature
1237 =space.max_summer_infiltration[2] - 273.15,
1238 )
1239 else:
1240 # use bim2sim standard zone ventilation based on TEASER templates
1241 idf.newidfobject(
1242 "ZONEVENTILATION:DESIGNFLOWRATE",
1243 Name=name + '_winter',
1244 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name,
1245 Schedule_Name="Continuous",
1246 Ventilation_Type="Natural",
1247 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
1248 Air_Changes_per_Hour=space.winter_reduction_infiltration[0],
1249 Minimum_Outdoor_Temperature=
1250 space.winter_reduction_infiltration[1] - 273.15,
1251 Maximum_Outdoor_Temperature=
1252 space.winter_reduction_infiltration[2] - 273.15,
1253 )
1255 idf.newidfobject(
1256 "ZONEVENTILATION:DESIGNFLOWRATE",
1257 Name=name + '_summer',
1258 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name,
1259 Schedule_Name="Continuous",
1260 Ventilation_Type="Natural",
1261 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
1262 Air_Changes_per_Hour=space.max_summer_infiltration[0],
1263 Minimum_Outdoor_Temperature
1264 =space.max_summer_infiltration[1] - 273.15,
1265 Maximum_Outdoor_Temperature
1266 =space.max_summer_infiltration[2] - 273.15,
1267 )
1269 idf.newidfobject(
1270 "ZONEVENTILATION:DESIGNFLOWRATE",
1271 Name=name + '_overheating',
1272 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name,
1273 Schedule_Name="Continuous",
1274 Ventilation_Type="Natural",
1275 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
1276 # calculation of overheating infiltration is a simplification
1277 # compared to the corresponding TEASER implementation which
1278 # dynamically computes thresholds for overheating infiltration
1279 # based on the zone temperature and additional factors.
1280 Air_Changes_per_Hour=space.max_overheating_infiltration[0],
1281 Minimum_Outdoor_Temperature
1282 =space.max_summer_infiltration[2] - 273.15,
1283 )
1285 def set_day_hvac_template(self, idf: IDF, space: ThermalZone, name: str):
1286 """Set 24 hour hvac template.
1288 This function sets idf schedules with 24hour schedules for heating and
1289 cooling.
1291 Args:
1292 idf: idf file object
1293 space: ThermalZone
1294 name: IDF Thermostat Name
1295 """
1296 htg_schedule_name = "Schedule " + "Heating " + space.usage.replace(
1297 ',', '')
1298 self.set_day_week_year_schedule(idf, space.heating_profile[:24],
1299 'heating_profile',
1300 htg_schedule_name)
1302 clg_schedule_name = "Schedule " + "Cooling " + space.usage.replace(
1303 ',', '')
1304 self.set_day_week_year_schedule(idf, space.cooling_profile[:24],
1305 'cooling_profile',
1306 clg_schedule_name)
1307 stat = idf.newidfobject(
1308 "HVACTEMPLATE:THERMOSTAT",
1309 Name=name,
1310 Heating_Setpoint_Schedule_Name=htg_schedule_name,
1311 Cooling_Setpoint_Schedule_Name=clg_schedule_name
1312 )
1313 return stat
1315 def set_hvac_template(self, idf: IDF, name: str,
1316 heating_sp: Union[int, float],
1317 cooling_sp: Union[int, float],
1318 mode='setback'):
1319 """Set heating and cooling templates (manually).
1321 This function manually sets heating and cooling templates.
1323 Args:
1324 idf: idf file object
1325 heating_sp: float or int for heating set point
1326 cooling_sp: float or int for cooling set point
1327 name: IDF Thermostat Name
1328 """
1329 if cooling_sp < 20:
1330 cooling_sp = 26
1331 elif cooling_sp < 24:
1332 cooling_sp = 23
1334 setback_htg = 18 # "T_threshold_heating"
1335 setback_clg = 26 # "T_threshold_cooling"
1337 # ensure setback temperature actually performs a setback on temperature
1338 if setback_htg > heating_sp:
1339 setback_htg = heating_sp
1340 if setback_clg < cooling_sp:
1341 setback_clg = cooling_sp
1343 if mode == "setback":
1344 htg_alldays = self._define_schedule_part('Alldays',
1345 [('5:00', setback_htg),
1346 ('21:00', heating_sp),
1347 ('24:00', setback_htg)])
1348 clg_alldays = self._define_schedule_part('Alldays',
1349 [('5:00', setback_clg),
1350 ('21:00', cooling_sp),
1351 ('24:00', setback_clg)])
1352 htg_name = "H_SetBack_" + str(heating_sp)
1353 clg_name = "C_SetBack_" + str(cooling_sp)
1354 if idf.getobject("SCHEDULE:COMPACT", htg_name) is None:
1355 self.write_schedule(idf, htg_name, [htg_alldays, ])
1356 else:
1357 idf.getobject("SCHEDULE:COMPACT", htg_name)
1358 if idf.getobject("SCHEDULE:COMPACT", clg_name) is None:
1359 self.write_schedule(idf, clg_name, [clg_alldays, ])
1360 else:
1361 idf.getobject("SCHEDULE:COMPACT", clg_name)
1362 stat = idf.newidfobject(
1363 "HVACTEMPLATE:THERMOSTAT",
1364 Name="STAT_" + name,
1365 Heating_Setpoint_Schedule_Name=htg_name,
1366 Cooling_Setpoint_Schedule_Name=clg_name,
1367 )
1369 if mode == "constant":
1370 stat = idf.newidfobject(
1371 "HVACTEMPLATE:THERMOSTAT",
1372 Name="STAT_" + name,
1373 Constant_Heating_Setpoint=heating_sp,
1374 Constant_Cooling_Setpoint=cooling_sp,
1375 )
1376 return stat
1378 @staticmethod
1379 def write_schedule(idf: IDF, sched_name: str, sched_part_list: list):
1380 """Write schedules to idf.
1382 This function writes a schedule to the idf. Only used for manual
1383 setup of schedules (combined with set_hvac_template).
1385 Args:
1386 idf: idf file object
1387 sched_name: str with name of the schedule
1388 sched_part_list: list of schedule parts (cf. function
1389 _define_schedule_part)
1390 """
1391 sched_list = {}
1392 field_count = 1
1393 for parts in sched_part_list:
1394 field_count += 1
1395 sched_list.update({'Field_' + str(field_count): 'For: ' + parts[0]})
1396 part = parts[1]
1397 for set in part:
1398 field_count += 1
1399 sched_list.update(
1400 {'Field_' + str(field_count): 'Until: ' + str(set[0])})
1401 field_count += 1
1402 sched_list.update({'Field_' + str(field_count): str(set[1])})
1403 if idf.getobject("SCHEDULETYPELIMITS", "Temperature") is None:
1404 idf.newidfobject("SCHEDULETYPELIMITS", Name="Temperature")
1406 sched = idf.newidfobject(
1407 "SCHEDULE:COMPACT",
1408 Name=sched_name,
1409 Schedule_Type_Limits_Name="Temperature",
1410 Field_1="Through: 12/31",
1411 **sched_list
1412 )
1413 return sched
1415 @staticmethod
1416 def _define_schedule_part(
1417 days: str, til_time_temp: list[tuple[str, Union[int, float]]]):
1418 """Defines a part of a schedule.
1420 Args:
1421 days: string: Weekdays, Weekends, Alldays, AllOtherDays, Saturdays,
1422 Sundays, ...
1423 til_time_temp: List of tuples
1424 (until-time format 'hh:mm' (24h) as str),
1425 temperature until this time in Celsius),
1426 e.g. (05:00, 18)
1427 """
1428 return [days, til_time_temp]
1430 @staticmethod
1431 def add_shadings(elements: dict, idf: IDF):
1432 """Add shading boundaries to idf.
1434 Args:
1435 elements: dict[guid: element]
1436 idf: idf file object
1437 """
1438 logger.info("Add Shadings ...")
1439 spatials = []
1440 ext_spatial_elem = filter_elements(elements, ExternalSpatialElement)
1441 for elem in ext_spatial_elem:
1442 for sb in elem.space_boundaries:
1443 spatials.append(sb)
1444 if not spatials:
1445 return
1446 pure_spatials = []
1447 description_list = [s.ifc.Description for s in spatials]
1448 descriptions = list(dict.fromkeys(description_list))
1449 shades_included = ("Shading:Building" or "Shading:Site") in descriptions
1451 # check if ifc has dedicated shading space boundaries included and
1452 # append them to pure_spatials for further processing
1453 if shades_included:
1454 for s in spatials:
1455 if s.ifc.Description in ["Shading:Building", "Shading:Site"]:
1456 pure_spatials.append(s)
1457 # if no shading boundaries are included in ifc, derive these from the
1458 # set of given space boundaries and append them to pure_spatials for
1459 # further processing
1460 else:
1461 for s in spatials:
1462 # only consider almost horizontal 2b shapes (roof-like SBs)
1463 if s.level_description == '2b':
1464 angle = math.degrees(
1465 gp_Dir(s.bound_normal).Angle(gp_Dir(gp_XYZ(0, 0, 1))))
1466 if not ((-45 < angle < 45) or (135 < angle < 225)):
1467 continue
1468 if s.related_bound and s.related_bound.ifc.RelatingSpace.is_a(
1469 'IfcSpace'):
1470 continue
1471 pure_spatials.append(s)
1473 # create idf shadings from set of pure_spatials
1474 for s in pure_spatials:
1475 obj = idf.newidfobject('SHADING:BUILDING:DETAILED',
1476 Name=s.guid,
1477 )
1478 obj_pnts = PyOCCTools.get_points_of_face(s.bound_shape)
1479 obj_coords = []
1480 for pnt in obj_pnts:
1481 co = tuple(round(p, 3) for p in pnt.Coord())
1482 obj_coords.append(co)
1483 obj.setcoords(obj_coords)
1485 def add_shading_control(self, shading_type, elements,
1486 idf, solar=150):
1487 """Add a default shading control to IDF.
1488 Two criteria must be met such that the window shades are set: the
1489 indoor air temperature must exceed a certain temperature and the solar
1490 radiation [W/m²] must be greater than a certain heat flow.
1491 Args:
1492 shading_type: shading type, 'Interior' or 'Exterior'
1493 elements: elements
1494 idf: idf
1495 solar: solar radiation on window surface [W/m²]
1496 """
1497 zones = filter_elements(elements, ThermalZone)
1499 for zone in zones:
1500 zone_name = zone.guid
1501 zone_openings = [sb for sb in zone.space_boundaries if
1502 isinstance(sb.bound_element, Window)]
1503 if not zone_openings:
1504 continue
1505 fenestration_dict = {}
1506 for i, opening in enumerate(zone_openings):
1507 fenestration_dict.update({'Fenestration_Surface_' + str(
1508 i+1) + '_Name': opening.guid})
1509 shade_control_name = "ShadeControl_" + zone_name
1510 opening_obj = idf.getobject(
1511 'FENESTRATIONSURFACE:DETAILED', zone_openings[
1512 0].guid)
1513 if opening_obj:
1514 construction_name = opening_obj.Construction_Name + "_" + \
1515 shading_type
1516 else:
1517 continue
1518 if not idf.getobject(
1519 "WINDOWSHADINGCONTROL", shade_control_name):
1520 # temperature setpoint for indoor air temperature [°C], set to
1521 # 2K higher than the maximum heating profile temperature within
1522 # the current thermal zone.
1523 idf.newidfobject("WINDOWSHADINGCONTROL",
1524 Name=shade_control_name,
1525 Zone_Name=zone_name,
1526 Shading_Type=shading_type+"Shade",
1527 Construction_with_Shading_Name=construction_name,
1528 Shading_Control_Type=
1529 'OnIfHighZoneAirTempAndHighSolarOnWindow',
1530 # only close blinds if heating setpoint
1531 # temperature is already exceeded (save energy)
1532 Setpoint=max(zone.heating_profile)+2 - 273.15,
1533 Setpoint_2=solar,
1534 Multiple_Surface_Control_Type='Group',
1535 **fenestration_dict
1536 )
1538 @staticmethod
1539 def set_simulation_control(sim_settings: EnergyPlusSimSettings, idf):
1540 """Set simulation control parameters.
1542 This function sets general simulation control parameters. These can
1543 be easily overwritten in the exported idf.
1544 Args:
1545 sim_settings: EnergyPlusSimSettings
1546 idf: idf file object
1547 """
1548 logger.info("Set Simulation Control ...")
1549 for sim_control in idf.idfobjects["SIMULATIONCONTROL"]:
1550 if sim_settings.system_sizing or sim_settings.weather_file_for_sizing:
1551 sim_control.Do_System_Sizing_Calculation = 'Yes'
1552 else:
1553 sim_control.Do_System_Sizing_Calculation = 'No'
1554 if sim_settings.run_for_sizing_periods or not sim_settings.run_full_simulation:
1555 sim_control.Run_Simulation_for_Sizing_Periods = 'Yes'
1556 else:
1557 sim_control.Run_Simulation_for_Sizing_Periods = 'No'
1558 if sim_settings.run_for_weather_period:
1559 sim_control.Run_Simulation_for_Weather_File_Run_Periods = 'Yes'
1560 else:
1561 sim_control.Run_Simulation_for_Weather_File_Run_Periods = 'No'
1562 if sim_settings.set_run_period or sim_settings.run_full_simulation:
1563 sim_control.Run_Simulation_for_Weather_File_Run_Periods = 'Yes'
1564 else:
1565 sim_control.Run_Simulation_for_Weather_File_Run_Periods = 'No'
1567 if sim_settings.set_run_period:
1568 for run_period in idf.idfobjects["RUNPERIOD"]:
1569 run_period.Begin_Month = sim_settings.run_period_start_month
1570 run_period.Begin_Day_of_Month = (
1571 sim_settings.run_period_start_day)
1572 run_period.End_Month = sim_settings.run_period_end_month
1573 run_period.End_Day_of_Month = sim_settings.run_period_end_day
1575 for building in idf.idfobjects['BUILDING']:
1576 building.Solar_Distribution = sim_settings.solar_distribution
1578 @staticmethod
1579 def set_ground_temperature(idf: IDF, t_ground: ureg.Quantity):
1580 """Set the ground temperature in the idf.
1582 Args:
1583 idf: idf file object
1584 t_ground: ground temperature as ureg.Quantity
1585 """
1586 logger.info("Set ground temperature...")
1588 string = '_Ground_Temperature'
1589 month_list = ['January', 'February', 'March', 'April', 'May', 'June',
1590 'July', 'August', 'September', 'October',
1591 'November', 'December']
1592 temp_dict = {}
1593 for month in month_list:
1594 temp_dict.update({month + string: t_ground.to(ureg.degC).m})
1595 idf.newidfobject("SITE:GROUNDTEMPERATURE:BUILDINGSURFACE", **temp_dict)
1597 @staticmethod
1598 def set_output_variables(idf: IDF, sim_settings: EnergyPlusSimSettings):
1599 """Set user defined output variables in the idf file.
1601 Args:
1602 idf: idf file object
1603 sim_settings: BIM2SIM simulation settings
1604 """
1605 logger.info("Set output variables ...")
1607 # general output settings. May be moved to general settings
1608 out_control = idf.idfobjects['OUTPUTCONTROL:TABLE:STYLE']
1609 out_control[0].Column_Separator = sim_settings.output_format
1610 out_control[0].Unit_Conversion = sim_settings.unit_conversion
1612 # remove all existing output variables with reporting frequency
1613 # "Timestep"
1614 out_var = [v for v in idf.idfobjects['OUTPUT:VARIABLE']
1615 if v.Reporting_Frequency.upper() == "TIMESTEP"]
1616 for var in out_var:
1617 idf.removeidfobject(var)
1618 if 'output_outdoor_conditions' in sim_settings.output_keys:
1619 idf.newidfobject(
1620 "OUTPUT:VARIABLE",
1621 Variable_Name="Site Outdoor Air Drybulb Temperature",
1622 Reporting_Frequency="Hourly",
1623 )
1624 idf.newidfobject(
1625 "OUTPUT:VARIABLE",
1626 Variable_Name="Site Outdoor Air Humidity Ratio",
1627 Reporting_Frequency="Hourly",
1628 )
1629 idf.newidfobject(
1630 "OUTPUT:VARIABLE",
1631 Variable_Name="Site Outdoor Air Relative Humidity",
1632 Reporting_Frequency="Hourly",
1633 )
1634 idf.newidfobject(
1635 "OUTPUT:VARIABLE",
1636 Variable_Name="Site Outdoor Air Barometric Pressure",
1637 Reporting_Frequency="Hourly",
1638 )
1639 idf.newidfobject(
1640 "OUTPUT:VARIABLE",
1641 Variable_Name="Site Diffuse Solar Radiation Rate per Area",
1642 Reporting_Frequency="Hourly",
1643 )
1644 idf.newidfobject(
1645 "OUTPUT:VARIABLE",
1646 Variable_Name="Site Direct Solar Radiation Rate per Area",
1647 Reporting_Frequency="Hourly",
1648 )
1649 idf.newidfobject(
1650 "OUTPUT:VARIABLE",
1651 Variable_Name="Site Ground Temperature",
1652 Reporting_Frequency="Hourly",
1653 )
1654 idf.newidfobject(
1655 "OUTPUT:VARIABLE",
1656 Variable_Name="Site Wind Speed",
1657 Reporting_Frequency="Hourly",
1658 )
1659 idf.newidfobject(
1660 "OUTPUT:VARIABLE",
1661 Variable_Name="Site Wind Direction",
1662 Reporting_Frequency="Hourly",
1663 )
1664 if 'output_zone_temperature' in sim_settings.output_keys:
1665 idf.newidfobject(
1666 "OUTPUT:VARIABLE",
1667 Variable_Name="Zone Mean Air Temperature",
1668 Reporting_Frequency="Hourly",
1669 )
1670 idf.newidfobject(
1671 "OUTPUT:VARIABLE",
1672 Variable_Name="Zone Operative Temperature",
1673 Reporting_Frequency="Hourly",
1674 )
1675 idf.newidfobject(
1676 "OUTPUT:VARIABLE",
1677 Variable_Name="Zone Air Relative Humidity",
1678 Reporting_Frequency="Hourly",
1679 )
1680 if 'output_internal_gains' in sim_settings.output_keys:
1681 idf.newidfobject(
1682 "OUTPUT:VARIABLE",
1683 Variable_Name="Zone People Occupant Count",
1684 Reporting_Frequency="Hourly",
1685 )
1686 idf.newidfobject(
1687 "OUTPUT:VARIABLE",
1688 Variable_Name="Zone People Total Heating Rate",
1689 Reporting_Frequency="Hourly",
1690 )
1691 idf.newidfobject(
1692 "OUTPUT:VARIABLE",
1693 Variable_Name="Zone Electric Equipment Total Heating Rate",
1694 Reporting_Frequency="Hourly",
1695 )
1696 idf.newidfobject(
1697 "OUTPUT:VARIABLE",
1698 Variable_Name="Zone Lights Total Heating Rate",
1699 Reporting_Frequency="Hourly",
1700 )
1701 idf.newidfobject(
1702 "OUTPUT:VARIABLE",
1703 Variable_Name="Zone Total Internal Total Heating Rate",
1704 Reporting_Frequency="Hourly",
1705 )
1706 if 'output_zone' in sim_settings.output_keys:
1707 idf.newidfobject(
1708 "OUTPUT:VARIABLE",
1709 Variable_Name="Zone Thermostat Heating Setpoint Temperature",
1710 Reporting_Frequency="Hourly",
1711 )
1712 idf.newidfobject(
1713 "OUTPUT:VARIABLE",
1714 Variable_Name="Zone Thermostat Cooling Setpoint Temperature",
1715 Reporting_Frequency="Hourly",
1716 )
1717 idf.newidfobject(
1718 "OUTPUT:VARIABLE",
1719 Variable_Name="Zone Ideal Loads Supply Air Total Heating "
1720 "Energy",
1721 Reporting_Frequency="Hourly",
1722 )
1723 idf.newidfobject(
1724 "OUTPUT:VARIABLE",
1725 Variable_Name="Zone Ideal Loads Supply Air Total Cooling "
1726 "Energy",
1727 Reporting_Frequency="Hourly",
1728 )
1729 idf.newidfobject(
1730 "OUTPUT:VARIABLE",
1731 Variable_Name="Zone Ideal Loads Outdoor Air Total Cooling Rate",
1732 Reporting_Frequency="Hourly",
1733 )
1734 idf.newidfobject(
1735 "OUTPUT:VARIABLE",
1736 Variable_Name="Zone Ideal Loads Outdoor Air Total Heating Rate",
1737 Reporting_Frequency="Hourly",
1738 )
1739 idf.newidfobject(
1740 "OUTPUT:VARIABLE",
1741 Variable_Name="Zone Ideal Loads Supply Air Total Cooling Rate",
1742 Reporting_Frequency="Hourly",
1743 )
1744 idf.newidfobject(
1745 "OUTPUT:VARIABLE",
1746 Variable_Name="Zone Ideal Loads Supply Air Total Heating Rate",
1747 Reporting_Frequency="Hourly",
1748 )
1749 idf.newidfobject(
1750 "OUTPUT:VARIABLE",
1751 Variable_Name="Zone Windows Total Heat Gain Rate",
1752 Reporting_Frequency="Hourly",
1753 )
1754 idf.newidfobject(
1755 "OUTPUT:VARIABLE",
1756 Variable_Name="Zone Windows Total Heat Gain Energy",
1757 Reporting_Frequency="Hourly",
1758 )
1759 idf.newidfobject(
1760 "OUTPUT:VARIABLE",
1761 Variable_Name="Zone Windows Total Transmitted Solar Radiation "
1762 "Energy",
1763 Reporting_Frequency="Hourly",
1764 )
1765 if 'output_infiltration' in sim_settings.output_keys:
1766 idf.newidfobject(
1767 "OUTPUT:VARIABLE",
1768 Variable_Name="Zone Infiltration Sensible Heat Gain Energy",
1769 Reporting_Frequency="Hourly",
1770 )
1771 idf.newidfobject(
1772 "OUTPUT:VARIABLE",
1773 Variable_Name="Zone Infiltration Sensible Heat Loss Energy",
1774 Reporting_Frequency="Hourly",
1775 )
1776 idf.newidfobject(
1777 "OUTPUT:VARIABLE",
1778 Variable_Name="Zone Infiltration Air Change Rate",
1779 Reporting_Frequency="Hourly",
1780 )
1781 idf.newidfobject(
1782 "OUTPUT:VARIABLE",
1783 Variable_Name="Zone Ventilation Air Change Rate",
1784 Reporting_Frequency="Hourly",
1785 )
1786 idf.newidfobject(
1787 "OUTPUT:VARIABLE",
1788 Variable_Name="Zone Ventilation Standard Density Volume Flow "
1789 "Rate",
1790 Reporting_Frequency="Hourly",
1791 )
1792 idf.newidfobject(
1793 "OUTPUT:VARIABLE",
1794 Variable_Name="Zone Ventilation Total Heat Gain Energy",
1795 Reporting_Frequency="Hourly",
1796 )
1797 idf.newidfobject(
1798 "OUTPUT:VARIABLE",
1799 Variable_Name="Zone Ventilation Total Heat Loss Energy",
1800 Reporting_Frequency="Hourly",
1801 )
1802 idf.newidfobject(
1803 "OUTPUT:VARIABLE",
1804 Variable_Name="Zone Mechanical Ventilation Air Changes per "
1805 "Hour",
1806 Reporting_Frequency="Hourly",
1807 )
1808 idf.newidfobject(
1809 "OUTPUT:VARIABLE",
1810 Variable_Name="Zone Mechanical Ventilation Standard Density "
1811 "Volume Flow Rate",
1812 Reporting_Frequency="Hourly",
1813 )
1816 if 'output_meters' in sim_settings.output_keys:
1817 idf.newidfobject(
1818 "OUTPUT:METER",
1819 Key_Name="Heating:EnergyTransfer",
1820 Reporting_Frequency="Hourly",
1821 )
1822 idf.newidfobject(
1823 "OUTPUT:METER",
1824 Key_Name="Cooling:EnergyTransfer",
1825 Reporting_Frequency="Hourly",
1826 )
1827 idf.newidfobject(
1828 "OUTPUT:METER",
1829 Key_Name="DistrictHeating:HVAC",
1830 Reporting_Frequency="Hourly",
1831 )
1832 idf.newidfobject(
1833 "OUTPUT:METER",
1834 Key_Name="DistrictCooling:HVAC",
1835 Reporting_Frequency="Hourly",
1836 )
1837 if 'output_dxf' in sim_settings.output_keys:
1838 idf.newidfobject("OUTPUT:SURFACES:DRAWING",
1839 Report_Type="DXF")
1840 if sim_settings.cfd_export:
1841 idf.newidfobject(
1842 "OUTPUT:VARIABLE",
1843 Variable_Name="Surface Inside Face Temperature",
1844 Reporting_Frequency="Hourly",
1845 )
1846 idf.newidfobject(
1847 "OUTPUT:VARIABLE",
1848 Variable_Name=
1849 "Surface Inside Face Conduction Heat Transfer Rate per Area",
1850 Reporting_Frequency="Hourly",
1851 )
1852 idf.newidfobject(
1853 "OUTPUT:VARIABLE",
1854 Variable_Name=
1855 "Surface Inside Face Conduction Heat Transfer Rate",
1856 Reporting_Frequency="Hourly",
1857 )
1858 idf.newidfobject(
1859 "OUTPUT:VARIABLE",
1860 Variable_Name=
1861 "Surface Window Net Heat Transfer Rate",
1862 Reporting_Frequency="Hourly",
1863 )
1864 idf.newidfobject("OUTPUT:DIAGNOSTICS",
1865 Key_1="DisplayAdvancedReportVariables",
1866 Key_2="DisplayExtraWarnings")
1867 return idf
1869 @staticmethod
1870 def export_geom_to_idf(sim_settings: EnergyPlusSimSettings,
1871 elements: dict, idf: IDF):
1872 """Write space boundary geometry to idf.
1874 This function converts the space boundary bound_shape from
1875 OpenCascade to idf geometry.
1877 Args:
1878 elements: dict[guid: element]
1879 idf: idf file object
1880 """
1881 logger.info("Export IDF geometry")
1882 bounds = filter_elements(elements, SpaceBoundary)
1883 for bound in bounds:
1884 idfp = IdfObject(sim_settings, bound, idf)
1885 if idfp.skip_bound:
1886 idf.popidfobject(idfp.key, -1)
1887 logger.warning(
1888 "Boundary with the GUID %s (%s) is skipped (due to "
1889 "missing boundary conditions)!",
1890 idfp.name, idfp.surface_type)
1891 continue
1892 bounds_2b = filter_elements(elements, SpaceBoundary2B)
1893 for b_bound in bounds_2b:
1894 idfp = IdfObject(sim_settings, b_bound, idf)
1895 if idfp.skip_bound:
1896 logger.warning(
1897 "Boundary with the GUID %s (%s) is skipped (due to "
1898 "missing boundary conditions)!",
1899 idfp.name, idfp.surface_type)
1900 continue
1902 @staticmethod
1903 def idf_validity_check(idf):
1904 """Perform idf validity check and simple fixes.
1906 This function performs a basic validity check of the resulting idf.
1907 It removes openings from adiabatic surfaces and very small surfaces.
1909 Args:
1910 idf: idf file object
1911 """
1912 logger.info('Start IDF Validity Checker')
1914 # remove erroneous fenestration surfaces which do may crash
1915 # EnergyPlus simulation
1916 fenestrations = idf.idfobjects['FENESTRATIONSURFACE:DETAILED']
1918 # Create a list of fenestrations to remove
1919 to_remove = []
1921 for fenestration in fenestrations:
1922 should_remove = False
1924 # Check for missing building surface reference
1925 if not fenestration.Building_Surface_Name:
1926 should_remove = True
1927 else:
1928 # Check if the referenced surface is adiabatic
1929 building_surface = idf.getobject(
1930 'BUILDINGSURFACE:DETAILED',
1931 fenestration.Building_Surface_Name
1932 )
1933 if building_surface and building_surface.Outside_Boundary_Condition == 'Adiabatic':
1934 should_remove = True
1936 if should_remove:
1937 to_remove.append(fenestration)
1939 # Remove the collected fenestrations
1940 for fenestration in to_remove:
1941 logger.info('Removed Fenestration: %s' % fenestration.Name)
1942 idf.removeidfobject(fenestration)
1944 # Check if shading control elements contain unavailable fenestration
1945 fenestration_updated = idf.idfobjects['FENESTRATIONSURFACE:DETAILED']
1946 shading_control = idf.idfobjects['WINDOWSHADINGCONTROL']
1947 fenestration_guids = [fe.Name for fe in fenestration_updated]
1948 for shc in shading_control:
1949 # create a list with current fenestration guids (only available
1950 # fenestration)
1951 fenestration_guids_new = []
1952 skipped_fenestration = False # flag for unavailable fenestration
1953 for attr_name in dir(shc):
1954 if ('Fenestration_Surface' in attr_name):
1955 if (getattr(shc, attr_name) in
1956 fenestration_guids):
1957 fenestration_guids_new.append(getattr(shc, attr_name))
1958 elif (getattr(shc, attr_name) not in
1959 fenestration_guids) and getattr(shc, attr_name):
1960 skipped_fenestration = True
1961 # if the shading control element containes unavailable
1962 # fenestration objects, the shading control must be updated to
1963 # prevent errors in simulation
1964 if fenestration_guids_new and skipped_fenestration:
1965 fenestration_dict = {}
1966 for i, guid in enumerate(fenestration_guids_new):
1967 fenestration_dict.update({'Fenestration_Surface_' + str(
1968 i + 1) + '_Name': guid})
1969 # remove previous shading control from idf and create a new one
1970 # removing individual attributes of the shading element
1971 # caused errors, so new shading control is created
1972 idf.removeidfobject(shc)
1973 idf.newidfobject("WINDOWSHADINGCONTROL", Name=shc.Name,
1974 Zone_Name=shc.Zone_Name,
1975 Shading_Type=shc.Shading_Type,
1976 Construction_with_Shading_Name=
1977 shc.Construction_with_Shading_Name,
1978 Shading_Control_Type=shc.Shading_Control_Type,
1979 Setpoint=shc.Setpoint,
1980 Setpoint_2=shc.Setpoint_2,
1981 Multiple_Surface_Control_Type=
1982 shc.Multiple_Surface_Control_Type,
1983 **fenestration_dict)
1984 logger.info('Updated Shading Control due to unavailable '
1985 'fenestration: %s' % shc.Name)
1987 # check for small building surfaces and remove them
1988 sfs = idf.getsurfaces()
1989 small_area_obj = [s for s in sfs
1990 if PyOCCTools.get_shape_area(
1991 PyOCCTools.make_faces_from_pnts(s.coords)) < 1e-2]
1993 for obj in small_area_obj:
1994 logger.info('Removed small area: %s' % obj.Name)
1995 idf.removeidfobject(obj)
1997 # check for small shading surfaces and remove them
1998 shadings = idf.getshadingsurfaces()
1999 small_area_obj = [s for s in shadings if PyOCCTools.get_shape_area(
2000 PyOCCTools.make_faces_from_pnts(s.coords)) < 1e-2]
2002 for obj in small_area_obj:
2003 logger.info('Removed small area: %s' % obj.Name)
2004 idf.removeidfobject(obj)
2006 # Check for building surfaces holding default window materials
2007 bsd = idf.idfobjects['BUILDINGSURFACE:DETAILED']
2008 for sf in bsd:
2009 if sf.Construction_Name == 'BS Exterior Window':
2010 logger.info(
2011 'Surface due to invalid material: %s' % sf.Name)
2012 idf.removeidfobject(sf)
2013 logger.info('IDF Validity Checker done')
2016class IdfObject:
2017 """Create idf elements for surfaces.
2019 This class holds all data required for the idf setup of
2020 BUILDINGSURFACE:DETAILED and FENESTRATIONSURFACE:DETAILED.
2021 This includes further methods for processing the preprocessed information
2022 from the BIM2SIM process for the use in idf (e.g., surface type mapping).
2023 """
2025 def __init__(self, sim_settings, inst_obj, idf):
2026 self.name = inst_obj.guid
2027 self.building_surface_name = None
2028 self.key = None
2029 self.out_bound_cond = ''
2030 self.out_bound_cond_obj = ''
2031 self.sun_exposed = ''
2032 self.wind_exposed = ''
2033 self.surface_type = None
2034 self.physical = inst_obj.physical
2035 self.construction_name = None
2036 self.related_bound = inst_obj.related_bound
2037 self.this_bound = inst_obj
2038 self.skip_bound = False
2039 self.bound_shape = inst_obj.bound_shape
2040 self.add_window_shade = False
2041 if not hasattr(inst_obj.bound_thermal_zone, 'guid'):
2042 self.skip_bound = True
2043 return
2044 self.zone_name = inst_obj.bound_thermal_zone.guid
2045 if inst_obj.parent_bound:
2046 self.key = "FENESTRATIONSURFACE:DETAILED"
2047 if sim_settings.add_window_shading == 'Interior':
2048 self.add_window_shade = 'Interior'
2049 elif sim_settings.add_window_shading == 'Exterior':
2050 self.add_window_shade = 'Exterior'
2051 else:
2052 self.key = "BUILDINGSURFACE:DETAILED"
2053 if inst_obj.parent_bound:
2054 self.building_surface_name = inst_obj.parent_bound.guid
2055 self.map_surface_types(inst_obj)
2056 self.map_boundary_conditions(inst_obj)
2057 self.set_preprocessed_construction_name()
2058 # only set a construction name if this construction is available
2059 if not self.construction_name \
2060 or not (idf.getobject("CONSTRUCTION", self.construction_name)
2061 or idf.getobject("CONSTRUCTION:AIRBOUNDARY",
2062 self.construction_name)):
2063 self.set_construction_name()
2064 obj = self.set_idfobject_attributes(idf)
2065 if obj is not None:
2066 self.set_idfobject_coordinates(obj, idf, inst_obj)
2067 else:
2068 pass
2070 def set_construction_name(self):
2071 """Set default construction names.
2073 This function sets default constructions for all idf surface types.
2074 Should only be used if no construction is available for the current
2075 object.
2076 """
2077 if self.surface_type == "Wall":
2078 self.construction_name = "Project Wall"
2079 elif self.surface_type == "Roof":
2080 self.construction_name = "Project Flat Roof"
2081 elif self.surface_type == "Ceiling":
2082 self.construction_name = "Project Ceiling"
2083 elif self.surface_type == "Floor":
2084 self.construction_name = "Project Floor"
2085 elif self.surface_type == "Door":
2086 self.construction_name = "Project Door"
2087 elif self.surface_type == "Window":
2088 self.construction_name = "Project External Window"
2090 def set_preprocessed_construction_name(self):
2091 """Set preprocessed constructions.
2093 This function sets constructions of idf surfaces to preprocessed
2094 constructions. Virtual space boundaries are set to be an air wall
2095 (not defined in preprocessing).
2096 """
2097 # set air wall for virtual bounds
2098 if not self.physical:
2099 if self.out_bound_cond == "Surface":
2100 self.construction_name = "Air Wall"
2101 else:
2102 rel_elem = self.this_bound.bound_element
2103 if not rel_elem:
2104 return
2105 if any([isinstance(rel_elem, window) for window in
2106 all_subclasses(Window, include_self=True)]):
2107 self.construction_name = 'Window_WM_' + \
2108 rel_elem.layerset.layers[
2109 0].material.name \
2110 + '_' + str(
2111 rel_elem.layerset.layers[0].thickness.to(ureg.metre).m)
2112 else:
2113 self.construction_name = (rel_elem.key.replace(
2114 "Disaggregated", "") + '_' + str(len(
2115 rel_elem.layerset.layers)) + '_' + '_'.join(
2116 [str(l.thickness.to(ureg.metre).m) for l in
2117 rel_elem.layerset.layers]))
2119 def set_idfobject_coordinates(self, obj, idf: IDF,
2120 inst_obj: Union[SpaceBoundary,
2121 SpaceBoundary2B]):
2122 """Export surface coordinates.
2124 This function exports the surface coordinates from the BIM2SIM Space
2125 Boundary instance to idf.
2126 Circular shapes and shapes with more than 120 vertices
2127 (BuildingSurfaces) or more than 4 vertices (fenestration) are
2128 simplified.
2130 Args:
2131 obj: idf-surface object (buildingSurface:Detailed or fenestration)
2132 idf: idf file object
2133 inst_obj: SpaceBoundary instance
2134 """
2135 # write bound_shape to obj
2136 obj_pnts = PyOCCTools.get_points_of_face(self.bound_shape)
2137 obj_coords = []
2138 for pnt in obj_pnts:
2139 co = tuple(round(p, 3) for p in pnt.Coord())
2140 obj_coords.append(co)
2141 try:
2142 obj.setcoords(obj_coords)
2143 except Exception as ex:
2144 logger.warning(f"Unexpected {ex=}. Setting coordinates for "
2145 f"{inst_obj.guid} failed. This element is not "
2146 f"exported."
2147 f"{type(ex)=}")
2148 self.skip_bound = True
2149 return
2150 circular_shape = self.get_circular_shape(obj_pnts)
2151 try:
2152 if (3 <= len(obj_coords) <= 120
2153 and self.key == "BUILDINGSURFACE:DETAILED") \
2154 or (3 <= len(obj_coords) <= 4
2155 and self.key == "FENESTRATIONSURFACE:DETAILED"):
2156 obj.setcoords(obj_coords)
2157 elif circular_shape is True and self.surface_type != 'Door':
2158 self.process_circular_shapes(idf, obj_coords, obj, inst_obj)
2159 else:
2160 self.process_other_shapes(inst_obj, obj)
2161 except Exception as ex:
2162 logger.warning(f"Unexpected {ex=}. Setting coordinates for "
2163 f"{inst_obj.guid} failed. This element is not "
2164 f"exported."
2165 f"{type(ex)=}")
2167 def set_idfobject_attributes(self, idf: IDF):
2168 """Writes precomputed surface attributes to idf.
2170 Args:
2171 idf: the idf file
2172 """
2173 if self.surface_type is not None:
2174 if self.key == "BUILDINGSURFACE:DETAILED":
2175 if self.surface_type.lower() in {"DOOR".lower(),
2176 "Window".lower()}:
2177 self.surface_type = "Wall"
2178 obj = idf.newidfobject(
2179 self.key,
2180 Name=self.name,
2181 Surface_Type=self.surface_type,
2182 Construction_Name=self.construction_name,
2183 Outside_Boundary_Condition=self.out_bound_cond,
2184 Outside_Boundary_Condition_Object=self.out_bound_cond_obj,
2185 Zone_Name=self.zone_name,
2186 Sun_Exposure=self.sun_exposed,
2187 Wind_Exposure=self.wind_exposed,
2188 )
2189 else:
2190 obj = idf.newidfobject(
2191 self.key,
2192 Name=self.name,
2193 Surface_Type=self.surface_type,
2194 Construction_Name=self.construction_name,
2195 Building_Surface_Name=self.building_surface_name,
2196 Outside_Boundary_Condition_Object=self.out_bound_cond_obj,
2197 )
2198 return obj
2200 def map_surface_types(self, inst_obj: Union[SpaceBoundary,
2201 SpaceBoundary2B]):
2202 """Map surface types.
2204 This function maps the attributes of a SpaceBoundary instance to idf
2205 surface type.
2207 Args:
2208 inst_obj: SpaceBoundary instance
2209 """
2210 # TODO use bim2sim elements mapping instead of ifc.is_a()
2211 # TODO update to new disaggregations
2212 elem = inst_obj.bound_element
2213 surface_type = None
2214 if elem is not None:
2215 if any([isinstance(elem, wall) for wall in all_subclasses(Wall,
2216 include_self=True)]):
2217 surface_type = 'Wall'
2218 elif any([isinstance(elem, door) for door in all_subclasses(Door,
2219 include_self=True)]):
2220 surface_type = "Door"
2221 elif any([isinstance(elem, window) for window in all_subclasses(
2222 Window, include_self=True)]):
2223 surface_type = "Window"
2224 elif any([isinstance(elem, roof) for roof in all_subclasses(Roof,
2225 include_self=True)]):
2226 surface_type = "Roof"
2227 elif any([isinstance(elem, slab) for slab in all_subclasses(Slab,
2228 include_self=True)]):
2229 if any([isinstance(elem, floor) for floor in all_subclasses(
2230 GroundFloor, include_self=True)]):
2231 surface_type = "Floor"
2232 elif any([isinstance(elem, floor) for floor in all_subclasses(
2233 InnerFloor, include_self=True)]):
2234 if inst_obj.top_bottom == BoundaryOrientation.bottom:
2235 surface_type = "Floor"
2236 elif inst_obj.top_bottom == BoundaryOrientation.top:
2237 surface_type = "Ceiling"
2238 elif inst_obj.top_bottom == BoundaryOrientation.vertical:
2239 surface_type = "Wall"
2240 logger.warning(f"InnerFloor with vertical orientation "
2241 f"found, exported as wall, "
2242 f"GUID: {inst_obj.guid}.")
2243 else:
2244 logger.warning(f"InnerFloor was not correctly matched "
2245 f"to surface type for GUID: "
2246 f"{inst_obj.guid}.")
2247 surface_type = "Floor"
2248 # elif elem.ifc is not None:
2249 # if elem.ifc.is_a("IfcBeam"):
2250 # if not PyOCCTools.compare_direction_of_normals(
2251 # inst_obj.bound_normal, gp_XYZ(0, 0, 1)):
2252 # surface_type = 'Wall'
2253 # else:
2254 # surface_type = 'Ceiling'
2255 # elif elem.ifc.is_a('IfcColumn'):
2256 # surface_type = 'Wall'
2257 elif inst_obj.top_bottom == BoundaryOrientation.bottom:
2258 surface_type = "Floor"
2259 elif inst_obj.top_bottom == BoundaryOrientation.top:
2260 surface_type = "Ceiling"
2261 if inst_obj.related_bound is None or inst_obj.is_external:
2262 surface_type = "Roof"
2263 elif inst_obj.top_bottom == BoundaryOrientation.vertical:
2264 surface_type = "Wall"
2265 else:
2266 if not PyOCCTools.compare_direction_of_normals(
2267 inst_obj.bound_normal, gp_XYZ(0, 0, 1)):
2268 surface_type = 'Wall'
2269 elif inst_obj.top_bottom == BoundaryOrientation.bottom:
2270 surface_type = "Floor"
2271 elif inst_obj.top_bottom == BoundaryOrientation.top:
2272 surface_type = "Ceiling"
2273 if inst_obj.related_bound is None or inst_obj.is_external:
2274 surface_type = "Roof"
2275 else:
2276 logger.warning(f"No surface type matched for {inst_obj}!")
2277 elif not inst_obj.physical:
2278 if not PyOCCTools.compare_direction_of_normals(
2279 inst_obj.bound_normal, gp_XYZ(0, 0, 1)):
2280 surface_type = 'Wall'
2281 else:
2282 if inst_obj.top_bottom == BoundaryOrientation.bottom:
2283 surface_type = "Floor"
2284 elif inst_obj.top_bottom == BoundaryOrientation.top:
2285 surface_type = "Ceiling"
2286 else:
2287 logger.warning(f"No surface type matched for {inst_obj}!")
2289 self.surface_type = surface_type
2291 def map_boundary_conditions(self, inst_obj: Union[SpaceBoundary,
2292 SpaceBoundary2B]):
2293 """Map boundary conditions.
2295 This function maps the boundary conditions of a SpaceBoundary instance
2296 to the idf space boundary conditions.
2298 Args:
2299 inst_obj: SpaceBoundary instance
2300 """
2301 if inst_obj.level_description == '2b' \
2302 or inst_obj.related_adb_bound is not None:
2303 self.out_bound_cond = 'Adiabatic'
2304 self.sun_exposed = 'NoSun'
2305 self.wind_exposed = 'NoWind'
2306 elif (hasattr(inst_obj.ifc, 'CorrespondingBoundary')
2307 and ((inst_obj.ifc.CorrespondingBoundary is not None) and (
2308 inst_obj.ifc.CorrespondingBoundary.InternalOrExternalBoundary.upper()
2309 == 'EXTERNAL_EARTH'))
2310 and (self.key == "BUILDINGSURFACE:DETAILED")
2311 and not (len(inst_obj.opening_bounds) > 0)):
2312 self.out_bound_cond = "Ground"
2313 self.sun_exposed = 'NoSun'
2314 self.wind_exposed = 'NoWind'
2315 elif inst_obj.is_external and inst_obj.physical \
2316 and not self.surface_type == 'Floor':
2317 self.out_bound_cond = 'Outdoors'
2318 self.sun_exposed = 'SunExposed'
2319 self.wind_exposed = 'WindExposed'
2320 self.out_bound_cond_obj = ''
2321 elif self.surface_type == "Floor" and \
2322 (inst_obj.related_bound is None
2323 or inst_obj.related_bound.ifc.RelatingSpace.is_a(
2324 'IfcExternalSpatialElement')):
2325 self.out_bound_cond = "Ground"
2326 self.sun_exposed = 'NoSun'
2327 self.wind_exposed = 'NoWind'
2328 elif inst_obj.related_bound is not None \
2329 and not inst_obj.related_bound.ifc.RelatingSpace.is_a(
2330 'IfcExternalSpatialElement'):
2331 self.out_bound_cond = 'Surface'
2332 self.out_bound_cond_obj = inst_obj.related_bound.guid
2333 self.sun_exposed = 'NoSun'
2334 self.wind_exposed = 'NoWind'
2335 elif self.key == "FENESTRATIONSURFACE:DETAILED":
2336 self.out_bound_cond = 'Outdoors'
2337 self.sun_exposed = 'SunExposed'
2338 self.wind_exposed = 'WindExposed'
2339 self.out_bound_cond_obj = ''
2340 elif self.related_bound is None:
2341 self.out_bound_cond = 'Outdoors'
2342 self.sun_exposed = 'SunExposed'
2343 self.wind_exposed = 'WindExposed'
2344 self.out_bound_cond_obj = ''
2345 else:
2346 self.skip_bound = True
2348 @staticmethod
2349 def get_circular_shape(obj_pnts: list[tuple]) -> bool:
2350 """Check if a shape is circular.
2352 This function checks if a SpaceBoundary has a circular shape.
2354 Args:
2355 obj_pnts: SpaceBoundary vertices (list of coordinate tuples)
2356 Returns:
2357 True if shape is circular
2358 """
2359 circular_shape = False
2360 # compute if shape is circular:
2361 if len(obj_pnts) > 4:
2362 pnt = obj_pnts[0]
2363 pnt2 = obj_pnts[1]
2364 distance_prev = pnt.Distance(pnt2)
2365 pnt = pnt2
2366 for pnt2 in obj_pnts[2:]:
2367 distance = pnt.Distance(pnt2)
2368 if (distance_prev - distance) ** 2 < 0.01:
2369 circular_shape = True
2370 pnt = pnt2
2371 distance_prev = distance
2372 else:
2373 continue
2374 return circular_shape
2376 @staticmethod
2377 def process_circular_shapes(idf: IDF, obj_coords: list[tuple], obj,
2378 inst_obj: Union[SpaceBoundary, SpaceBoundary2B]
2379 ):
2380 """Simplify circular space boundaries.
2382 This function processes circular boundary shapes. It converts circular
2383 shapes to triangular shapes.
2385 Args:
2386 idf: idf file object
2387 obj_coords: coordinates of an idf object
2388 obj: idf object
2389 inst_obj: SpaceBoundary instance
2390 """
2391 drop_count = int(len(obj_coords) / 8)
2392 drop_list = obj_coords[0::drop_count]
2393 pnt = drop_list[0]
2394 counter = 0
2395 # del inst_obj.__dict__['bound_center']
2396 for pnt2 in drop_list[1:]:
2397 counter += 1
2398 new_obj = idf.copyidfobject(obj)
2399 new_obj.Name = str(obj.Name) + '_' + str(counter)
2400 fc = PyOCCTools.make_faces_from_pnts(
2401 [pnt, pnt2, inst_obj.bound_center.Coord()])
2402 fcsc = PyOCCTools.scale_face(fc, 0.99)
2403 new_pnts = PyOCCTools.get_points_of_face(fcsc)
2404 new_coords = []
2405 for pnt in new_pnts:
2406 new_coords.append(pnt.Coord())
2407 new_obj.setcoords(new_coords)
2408 pnt = pnt2
2409 new_obj = idf.copyidfobject(obj)
2410 new_obj.Name = str(obj.Name) + '_' + str(counter + 1)
2411 fc = PyOCCTools.make_faces_from_pnts(
2412 [drop_list[-1], drop_list[0], inst_obj.bound_center.Coord()])
2413 fcsc = PyOCCTools.scale_face(fc, 0.99)
2414 new_pnts = PyOCCTools.get_points_of_face(fcsc)
2415 new_coords = []
2416 for pnt in new_pnts:
2417 new_coords.append(pnt.Coord())
2418 new_obj.setcoords(new_coords)
2419 idf.removeidfobject(obj)
2421 @staticmethod
2422 def process_other_shapes(inst_obj: Union[SpaceBoundary, SpaceBoundary2B],
2423 obj):
2424 """Simplify non-circular shapes.
2426 This function processes non-circular shapes with too many vertices
2427 by approximation of the shape utilizing the UV-Bounds from OCC
2428 (more than 120 vertices for BUILDINGSURFACE:DETAILED
2429 and more than 4 vertices for FENESTRATIONSURFACE:DETAILED)
2431 Args:
2432 inst_obj: SpaceBoundary Instance
2433 obj: idf object
2434 """
2435 # print("TOO MANY EDGES")
2436 obj_pnts = []
2437 exp = TopExp_Explorer(inst_obj.bound_shape, TopAbs_FACE)
2438 face = topods_Face(exp.Current())
2439 umin, umax, vmin, vmax = breptools_UVBounds(face)
2440 surf = BRep_Tool.Surface(face)
2441 plane = Handle_Geom_Plane_DownCast(surf)
2442 plane = gp_Pln(plane.Location(), plane.Axis().Direction())
2443 new_face = BRepBuilderAPI_MakeFace(plane,
2444 umin,
2445 umax,
2446 vmin,
2447 vmax).Face().Reversed()
2448 face_exp = TopExp_Explorer(new_face, TopAbs_WIRE)
2449 w_exp = BRepTools_WireExplorer(topods_Wire(face_exp.Current()))
2450 while w_exp.More():
2451 wire_vert = w_exp.CurrentVertex()
2452 obj_pnts.append(BRep_Tool.Pnt(wire_vert))
2453 w_exp.Next()
2454 obj_coords = []
2455 for pnt in obj_pnts:
2456 obj_coords.append(pnt.Coord())
2457 obj.setcoords(obj_coords)