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