Coverage for bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/task/ep_create_idf.py: 0%
781 statements
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
« prev ^ index » next coverage.py v7.6.12, created at 2025-03-12 17:09 +0000
1from __future__ import annotations
3import logging
4import math
5import os
6from pathlib import Path, PosixPath
7from typing import Union, TYPE_CHECKING
9from OCC.Core.BRep import BRep_Tool
10from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace
11from OCC.Core.BRepTools import breptools_UVBounds, BRepTools_WireExplorer
12from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_WIRE
13from OCC.Core.TopExp import TopExp_Explorer
14from OCC.Core.TopoDS import topods_Face, topods_Wire
15from OCC.Core._Geom import Handle_Geom_Plane_DownCast
17from OCC.Core.gp import gp_Dir, gp_XYZ, gp_Pln
18from geomeppy import IDF
20from bim2sim.elements.base_elements import IFCBased
21from bim2sim.elements.bps_elements import (ExternalSpatialElement,
22 SpaceBoundary2B, ThermalZone, Storey,
23 Layer, Window, SpaceBoundary, Wall,
24 Door, Roof, Slab, InnerFloor,
25 GroundFloor)
26from bim2sim.elements.mapping.units import ureg
27from bim2sim.project import FolderStructure
28from bim2sim.tasks.base import ITask
29from bim2sim.utilities.common_functions import filter_elements, \
30 get_spaces_with_bounds, all_subclasses
31from bim2sim.utilities.pyocc_tools import PyOCCTools
32from bim2sim.utilities.types import BoundaryOrientation
34if TYPE_CHECKING:
35 from bim2sim.plugins.PluginEnergyPlus.bim2sim_energyplus import \
36 EnergyPlusSimSettings
38logger = logging.getLogger(__name__)
41class CreateIdf(ITask):
42 """Create an EnergyPlus Input file.
44 Task to create an EnergyPlus Input file based on the for EnergyPlus
45 preprocessed space boundary geometries. See detailed explanation in the run
46 function below.
47 """
49 reads = ('elements', 'weather_file',)
50 touches = ('idf', 'sim_results_path')
52 def __init__(self, playground):
53 super().__init__(playground)
54 self.idf = None
56 def run(self, elements: dict, weather_file: Path) -> tuple[IDF, Path]:
57 """Execute all methods to export an IDF from BIM2SIM.
59 This task includes all functions for exporting EnergyPlus Input files
60 (idf) based on the previously preprocessed SpaceBoundary geometry from
61 the ep_geom_preprocessing task. Geometric preprocessing (includes
62 EnergyPlus-specific space boundary enrichment) must be executed
63 before this task.
64 In this task, first, the IDF itself is initialized. Then, the zones,
65 materials and constructions, shadings and control parameters are set
66 in the idf. Within the export of the idf, the final mapping of the
67 bim2sim elements and the idf components is executed. Shading control
68 is added if required, and the ground temperature of the building
69 surrounding ground is set, as well as the output variables of the
70 simulation. Finally, the generated idf is validated, and minor
71 corrections are performed, e.g., tiny surfaces are deleted that
72 would cause errors during the EnergyPlus Simulation run.
74 Args:
75 elements (dict): dictionary in the format dict[guid: element],
76 holds preprocessed elements including space boundaries.
77 weather_file (Path): path to weather file in .epw data format
78 Returns:
79 idf (IDF): EnergyPlus input file
80 sim_results_path (Path): path to the simulation results.
81 """
82 logger.info("IDF generation started ...")
83 idf, sim_results_path = self.init_idf(self.playground.sim_settings,
84 self.paths, weather_file,
85 self.prj_name)
86 self.init_zone(self.playground.sim_settings, elements, idf)
87 self.init_zonelist(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.")
108 return idf, sim_results_path
110 @staticmethod
111 def init_idf(sim_settings: EnergyPlusSimSettings, paths: FolderStructure,
112 weather_file: PosixPath, ifc_name: str) -> IDF:
113 """ Initialize the EnergyPlus input file.
115 Initialize the EnergyPlus input file (idf) with general idf settings
116 and set default weather
117 data.
119 Args:
120 sim_settings: EnergyPlusSimSettings
121 paths: BIM2SIM FolderStructure
122 weather_file: PosixPath to *.epw weather file
123 ifc_name: str of name of ifc
124 Returns:
125 idf file of type IDF
126 """
127 logger.info("Initialize the idf ...")
128 # set the installation path for the EnergyPlus installation
129 ep_install_path = sim_settings.ep_install_path
130 # set the plugin path of the PluginEnergyPlus within the BIM2SIM Tool
131 plugin_ep_path = str(Path(__file__).parent.parent.parent)
132 # set Energy+.idd as base for new idf
133 IDF.setiddname(ep_install_path / 'Energy+.idd')
134 # initialize the idf with a minimal idf setup
135 idf = IDF(plugin_ep_path + '/data/Minimal.idf')
136 sim_results_path = paths.export/'EnergyPlus'/'SimResults'
137 export_path = sim_results_path / ifc_name
138 if not os.path.exists(export_path):
139 os.makedirs(export_path)
140 idf.idfname = export_path / str(ifc_name + '.idf')
141 # load and set basic compact schedules and ScheduleTypeLimits
142 schedules_idf = IDF(plugin_ep_path + '/data/Schedules.idf')
143 schedules = schedules_idf.idfobjects["Schedule:Compact".upper()]
144 sch_typelim = schedules_idf.idfobjects["ScheduleTypeLimits".upper()]
145 for s in schedules:
146 idf.copyidfobject(s)
147 for t in sch_typelim:
148 idf.copyidfobject(t)
149 # set weather file
150 idf.epw = str(weather_file)
151 return idf, sim_results_path
153 def init_zone(self, sim_settings: EnergyPlusSimSettings, elements: dict,
154 idf: IDF):
155 """Initialize zone settings.
157 Creates one idf zone per space and sets heating and cooling
158 templates, infiltration and internal loads (occupancy (people),
159 equipment, lighting).
161 Args:
162 sim_settings: BIM2SIM simulation settings
163 elements: dict[guid: element]
164 idf: idf file object
165 """
166 logger.info("Init thermal zones ...")
167 spaces = get_spaces_with_bounds(elements)
168 for space in spaces:
169 if space.space_shape_volume:
170 volume = space.space_shape_volume.to(ureg.meter ** 3).m
171 # for some shapes, shape volume calculation might not work
172 else:
173 volume = space.volume.to(ureg.meter ** 3).m
174 zone = idf.newidfobject(
175 'ZONE',
176 Name=space.ifc.GlobalId,
177 Volume=volume
178 )
179 self.set_heating_and_cooling(idf, zone_name=zone.Name, space=space)
180 self.set_infiltration(
181 idf, name=zone.Name, zone_name=zone.Name, space=space,
182 ep_version=sim_settings.ep_version)
183 if (not self.playground.sim_settings.cooling_tz_overwrite and
184 self.playground.sim_settings.add_natural_ventilation):
185 self.set_natural_ventilation(
186 idf, name=zone.Name, zone_name=zone.Name, space=space,
187 ep_version=sim_settings.ep_version)
188 self.set_people(sim_settings, idf, name=zone.Name,
189 zone_name=zone.Name, space=space)
190 self.set_equipment(sim_settings, idf, name=zone.Name,
191 zone_name=zone.Name, space=space)
192 self.set_lights(sim_settings, idf, name=zone.Name, zone_name=zone.Name,
193 space=space)
195 @staticmethod
196 def init_zonelist(
197 idf: IDF,
198 name: str = None,
199 zones_in_list: list[str] = None):
200 """Initialize zone lists.
202 Inits a list of zones in the idf. If the zones_in_list is not set,
203 all zones are assigned to a general zone, unless the number of total
204 zones is greater than 20 (max. allowed number of zones in a zonelist
205 in an idf).
207 Args:
208 idf: idf file object
209 name: str with name of zone list
210 zones_in_list: list with the guids of the zones to be included in
211 the list
212 """
213 if zones_in_list is None:
214 # assign all zones to one list unless the total number of zones
215 # is larger than 20.
216 idf_zones = idf.idfobjects["ZONE"]
217 if len(idf_zones) > 20:
218 return
219 else:
220 # assign all zones with the zone names that are included in
221 # zones_in_list to the zonelist.
222 all_idf_zones = idf.idfobjects["ZONE"]
223 idf_zones = [zone for zone in all_idf_zones if zone.Name
224 in zones_in_list]
225 if len(idf_zones) > 20:
226 return
227 if len(idf_zones) == 0:
228 return
229 if name is None:
230 name = "All_Zones"
231 zs = {}
232 for i, z in enumerate(idf_zones):
233 zs.update({"Zone_" + str(i + 1) + "_Name": z.Name})
234 idf.newidfobject("ZONELIST", Name=name, **zs)
236 def init_zonegroups(self, elements: dict, idf: IDF):
237 """Assign one zonegroup per storey.
239 Args:
240 elements: dict[guid: element]
241 idf: idf file object
242 """
243 spaces = get_spaces_with_bounds(elements)
244 # assign storeys to spaces (ThermalZone)
245 for space in spaces:
246 if space.storeys:
247 space.storey = space.storeys[0] # Zone can only have one storey
248 else:
249 space.storey = None
250 # add zonelist per storey
251 storeys = filter_elements(elements, Storey)
252 for st in storeys:
253 space_ids = []
254 for space in st.thermal_zones:
255 if not space in spaces:
256 continue
257 space_ids.append(space.guid)
258 self.init_zonelist(idf, name=st.ifc.Name, zones_in_list=space_ids)
260 # add zonelist for All_Zones
261 zone_lists = [zlist for zlist in idf.idfobjects["ZONELIST"]
262 if zlist.Name != "All_Zones"]
264 # add zonegroup for each zonegroup in zone_lists.
265 for zlist in zone_lists:
266 idf.newidfobject("ZONEGROUP",
267 Name=zlist.Name,
268 Zone_List_Name=zlist.Name,
269 Zone_List_Multiplier=1
270 )
271 @staticmethod
272 def check_preprocessed_materials_and_constructions(rel_elem, layers):
273 """Check if preprocessed materials and constructions are valid."""
274 correct_preprocessing = False
275 # check if thickness and material parameters are available from
276 # preprocessing
277 if all(layer.thickness for layer in layers):
278 for layer in rel_elem.layerset.layers:
279 if None in (layer.material.thermal_conduc,
280 layer.material.spec_heat_capacity,
281 layer.material.density):
282 return correct_preprocessing
283 elif 0 in (layer.material.thermal_conduc.m,
284 layer.material.spec_heat_capacity.m,
285 layer.material.density.m):
286 return correct_preprocessing
287 else:
288 pass
290 correct_preprocessing = True
292 return correct_preprocessing
294 def get_preprocessed_materials_and_constructions(
295 self, sim_settings: EnergyPlusSimSettings, elements: dict, idf: IDF):
296 """Get preprocessed materials and constructions.
298 This function sets preprocessed construction and material for
299 building surfaces and fenestration. For virtual bounds, an air
300 boundary construction is set.
302 Args:
303 sim_settings: BIM2SIM simulation settings
304 elements: dict[guid: element]
305 idf: idf file object
306 """
307 logger.info("Get predefined materials and construction ...")
308 bounds = filter_elements(elements, 'SpaceBoundary')
309 for bound in bounds:
310 rel_elem = bound.bound_element
311 if not rel_elem:
312 continue
313 if not any([isinstance(rel_elem, window) for window in
314 all_subclasses(Window, include_self=True)]):
315 # set construction for all but fenestration
316 if self.check_preprocessed_materials_and_constructions(
317 rel_elem, rel_elem.layerset.layers):
318 self.set_preprocessed_construction_elem(
319 rel_elem, rel_elem.layerset.layers, idf)
320 for layer in rel_elem.layerset.layers:
321 self.set_preprocessed_material_elem(layer, idf)
322 else:
323 logger.warning("No preprocessed construction and "
324 "material found for space boundary %s on "
325 "related building element %s. Using "
326 "default values instead.",
327 bound.guid, rel_elem.guid)
328 else:
329 # set construction elements for windows
330 self.set_preprocessed_window_material_elem(
331 rel_elem, idf, sim_settings.add_window_shading)
333 # Add air boundaries as construction as a material for virtual bounds
334 if sim_settings.ep_version in ["9-2-0", "9-4-0"]:
335 idf.newidfobject("CONSTRUCTION:AIRBOUNDARY",
336 Name='Air Wall',
337 Solar_and_Daylighting_Method='GroupedZones',
338 Radiant_Exchange_Method='GroupedZones',
339 Air_Exchange_Method='SimpleMixing',
340 Simple_Mixing_Air_Changes_per_Hour=0.5,
341 )
342 else:
343 idf.newidfobject("CONSTRUCTION:AIRBOUNDARY",
344 Name='Air Wall',
345 Air_Exchange_Method='SimpleMixing',
346 Simple_Mixing_Air_Changes_per_Hour=0.5,
347 )
349 @staticmethod
350 def set_preprocessed_construction_elem(
351 rel_elem: IFCBased,
352 layers: list[Layer],
353 idf: IDF):
354 """Write preprocessed constructions to idf.
356 This function uses preprocessed data to define idf construction
357 elements.
359 Args:
360 rel_elem: any subclass of IFCBased (e.g., Wall)
361 layers: list of Layer
362 idf: idf file object
363 """
364 construction_name = (rel_elem.key.replace('Disaggregated', '') + '_'
365 + str(len(layers)) + '_' + '_' \
366 .join([str(l.thickness.to(ureg.metre).m) for l in layers]))
367 # todo: find a unique key for construction name
368 if idf.getobject("CONSTRUCTION", construction_name) is None:
369 outer_layer = layers[-1]
370 other_layer_list = layers[:-1]
371 other_layer_list.reverse()
372 other_layers = {}
373 for i, l in enumerate(other_layer_list):
374 other_layers.update(
375 {'Layer_' + str(i + 2): l.material.name + "_" + str(
376 l.thickness.to(ureg.metre).m)})
377 idf.newidfobject("CONSTRUCTION",
378 Name=construction_name,
379 Outside_Layer=outer_layer.material.name + "_" +
380 str(outer_layer.thickness.to(
381 ureg.metre).m),
382 **other_layers
383 )
385 @staticmethod
386 def set_preprocessed_material_elem(layer: Layer, idf: IDF):
387 """Set a preprocessed material element.
389 Args:
390 layer: Layer Instance
391 idf: idf file object
392 """
393 material_name = layer.material.name + "_" + str(
394 layer.thickness.to(ureg.metre).m)
395 if idf.getobject("MATERIAL", material_name):
396 return
397 specific_heat = \
398 layer.material.spec_heat_capacity.to(ureg.joule / ureg.kelvin /
399 ureg.kilogram).m
400 if specific_heat < 100:
401 specific_heat = 100
402 conductivity = layer.material.thermal_conduc.to(
403 ureg.W / (ureg.m * ureg.K)).m
404 density = layer.material.density.to(ureg.kg / ureg.m ** 3).m
405 if conductivity == 0:
406 logger.error(f"Conductivity of {layer.material} is 0. Simulation "
407 f"will crash, please correct input or resulting idf "
408 f"file.")
409 if density == 0:
410 logger.error(f"Density of {layer.material} is 0. Simulation "
411 f"will crash, please correct input or resulting idf "
412 f"file.")
413 idf.newidfobject("MATERIAL",
414 Name=material_name,
415 Roughness="MediumRough",
416 Thickness=layer.thickness.to(ureg.metre).m,
417 Conductivity=conductivity,
418 Density=density,
419 Specific_Heat=specific_heat
420 )
422 @staticmethod
423 def set_preprocessed_window_material_elem(rel_elem: Window,
424 idf: IDF,
425 add_window_shading: False):
426 """Set preprocessed window material.
428 This function constructs windows with a
429 WindowMaterial:SimpleGlazingSystem consisting of the outermost layer
430 of the providing related element. This is a simplification, needs to
431 be extended to hold multilayer window constructions.
433 Args:
434 rel_elem: Window instance
435 idf: idf file object
436 add_window_shading: Add window shading (options: 'None',
437 'Interior', 'Exterior')
438 """
439 material_name = \
440 'WM_' + rel_elem.layerset.layers[0].material.name + '_' \
441 + str(rel_elem.layerset.layers[0].thickness.to(ureg.m).m)
442 if idf.getobject("WINDOWMATERIAL:SIMPLEGLAZINGSYSTEM", material_name):
443 return
444 if rel_elem.u_value.to(ureg.W / ureg.K / ureg.meter ** 2).m > 0:
445 ufactor = 1 / (0.04 + 1 / rel_elem.u_value.to(ureg.W / ureg.K /
446 ureg.meter ** 2).m
447 + 0.13)
448 else:
449 ufactor = 1 / (0.04 + rel_elem.layerset.layers[0].thickness.to(
450 ureg.metre).m /
451 rel_elem.layerset.layers[
452 0].material.thermal_conduc.to(
453 ureg.W / (ureg.m * ureg.K)).m +
454 0.13)
455 if rel_elem.g_value >= 1:
456 old_g_value = rel_elem.g_value
457 rel_elem.g_value = 0.999
458 logger.warning("G-Value was set to %f, "
459 "but has to be smaller than 1, so overwritten by %f",
460 old_g_value, rel_elem.g_value)
462 idf.newidfobject("WINDOWMATERIAL:SIMPLEGLAZINGSYSTEM",
463 Name=material_name,
464 UFactor=ufactor,
465 Solar_Heat_Gain_Coefficient=rel_elem.g_value,
466 # Visible_Transmittance=0.8 # optional
467 )
468 if add_window_shading:
469 default_shading_name = "DefaultWindowShade"
470 if not idf.getobject("WINDOWMATERIAL:SHADE", default_shading_name):
471 idf.newidfobject("WINDOWMATERIAL:SHADE",
472 Name=default_shading_name,
473 Solar_Transmittance=0.3,
474 Solar_Reflectance=0.5,
475 Visible_Transmittance=0.3,
476 Visible_Reflectance=0.5,
477 Infrared_Hemispherical_Emissivity=0.9,
478 Infrared_Transmittance=0.05,
479 Thickness=0.003,
480 Conductivity=0.1)
481 construction_name = 'Window_' + material_name + "_" \
482 + add_window_shading
483 if idf.getobject("CONSTRUCTION", construction_name) is None:
484 if add_window_shading == 'Interior':
485 idf.newidfobject("CONSTRUCTION",
486 Name=construction_name,
487 Outside_Layer=material_name,
488 Layer_2=default_shading_name
489 )
490 else:
491 idf.newidfobject("CONSTRUCTION",
492 Name=construction_name,
493 Outside_Layer=default_shading_name,
494 Layer_2=material_name
495 )
496 # todo: enable use of multilayer windows
497 # set construction without shading anyways
498 construction_name = 'Window_' + material_name
499 if idf.getobject("CONSTRUCTION", construction_name) is None:
500 idf.newidfobject("CONSTRUCTION",
501 Name=construction_name,
502 Outside_Layer=material_name
503 )
505 def set_heating_and_cooling(self, idf: IDF, zone_name: str,
506 space: ThermalZone):
507 """Set heating and cooling parameters.
509 This function sets heating and cooling parameters based on the data
510 available from BIM2SIM Preprocessing (either IFC-based or
511 Template-based).
513 Args:
514 idf: idf file object
515 zone_name: str
516 space: ThermalZone instance
517 """
518 stat_name = "STATS " + space.usage.replace(',', '')
519 if idf.getobject("HVACTEMPLATE:THERMOSTAT", stat_name) is None:
520 stat = self.set_day_hvac_template(idf, space, stat_name)
521 else:
522 stat = idf.getobject("HVACTEMPLATE:THERMOSTAT", stat_name)
524 cooling_availability = "Off"
525 heating_availability = "Off"
527 if space.with_cooling:
528 cooling_availability = "On"
529 if space.with_heating:
530 heating_availability = "On"
532 idf.newidfobject(
533 "HVACTEMPLATE:ZONE:IDEALLOADSAIRSYSTEM",
534 Zone_Name=zone_name,
535 Template_Thermostat_Name=stat.Name,
536 Heating_Availability_Schedule_Name=heating_availability,
537 Cooling_Availability_Schedule_Name=cooling_availability
538 )
540 def set_people(self, sim_settings: EnergyPlusSimSettings, idf: IDF, name: str,
541 zone_name: str, space: ThermalZone):
542 """Set occupancy schedules.
544 This function sets schedules and internal loads from people (occupancy)
545 based on the BIM2SIM Preprocessing, i.e. based on IFC data if
546 available or on templates.
548 Args:
549 sim_settings: BIM2SIM simulation settings
550 idf: idf file object
551 name: name of the new people idf object
552 zone_name: name of zone or zone_list
553 space: ThermalZone instance
554 """
555 schedule_name = "Schedule " + "People " + space.usage.replace(',', '')
556 profile_name = 'persons_profile'
557 self.set_day_week_year_schedule(idf, space.persons_profile[:24],
558 profile_name, schedule_name)
559 # set default activity schedule
560 if idf.getobject("SCHEDULETYPELIMITS", "Any Number") is None:
561 idf.newidfobject("SCHEDULETYPELIMITS", Name="Any Number")
562 activity_schedule_name = "Schedule Activity " + str(
563 space.fixed_heat_flow_rate_persons)
564 if idf.getobject("SCHEDULE:COMPACT", activity_schedule_name) is None:
565 idf.newidfobject("SCHEDULE:COMPACT",
566 Name=activity_schedule_name,
567 Schedule_Type_Limits_Name="Any Number",
568 Field_1="Through: 12/31",
569 Field_2="For: Alldays",
570 Field_3="Until: 24:00",
571 Field_4=space.fixed_heat_flow_rate_persons.to(
572 ureg.watt).m#*1.8 # in W/Person
573 ) # other method for Field_4 (not used here)
574 # ="persons_profile"*"activity_degree_persons"*58,1*1,8
575 # (58.1 W/(m2*met), 1.8m2/Person)
576 if sim_settings.ep_version in ["9-2-0", "9-4-0"]:
577 idf.newidfobject(
578 "PEOPLE",
579 Name=name,
580 Zone_or_ZoneList_Name=zone_name,
581 Number_of_People_Calculation_Method="People/Area",
582 People_per_Zone_Floor_Area=space.persons,
583 Activity_Level_Schedule_Name=activity_schedule_name,
584 Number_of_People_Schedule_Name=schedule_name,
585 Fraction_Radiant=space.ratio_conv_rad_persons
586 )
587 else:
588 idf.newidfobject(
589 "PEOPLE",
590 Name=name,
591 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name,
592 Number_of_People_Calculation_Method="People/Area",
593 People_per_Floor_Area=space.persons,
594 Activity_Level_Schedule_Name=activity_schedule_name,
595 Number_of_People_Schedule_Name=schedule_name,
596 Fraction_Radiant=space.ratio_conv_rad_persons
597 )
599 @staticmethod
600 def set_day_week_year_schedule(idf: IDF, schedule: list[float],
601 profile_name: str,
602 schedule_name: str):
603 """Set day, week and year schedule (hourly).
605 This function sets an hourly day, week and year schedule.
607 Args:
608 idf: idf file object
609 schedule: list of float values for the schedule (e.g.,
610 temperatures, loads)
611 profile_name: string
612 schedule_name: str
613 """
614 if idf.getobject("SCHEDULE:DAY:HOURLY", name=schedule_name) is None:
615 limits_name = 'Fraction'
616 hours = {}
617 if profile_name in {'heating_profile', 'cooling_profile'}:
618 limits_name = 'Temperature'
619 if idf.getobject("SCHEDULETYPELIMITS", "Temperature") is None:
620 idf.newidfobject("SCHEDULETYPELIMITS", Name="Temperature")
621 for i, l in enumerate(schedule[:24]):
622 if profile_name in {'heating_profile', 'cooling_profile'}:
623 # convert Kelvin to Celsius for EnergyPlus Export
624 if schedule[i] > 270:
625 schedule[i] = schedule[i] - 273.15
626 hours.update({'Hour_' + str(i + 1): schedule[i]})
627 idf.newidfobject("SCHEDULE:DAY:HOURLY", Name=schedule_name,
628 Schedule_Type_Limits_Name=limits_name, **hours)
629 if idf.getobject("SCHEDULE:WEEK:COMPACT", name=schedule_name) is None:
630 idf.newidfobject("SCHEDULE:WEEK:COMPACT", Name=schedule_name,
631 DayType_List_1="AllDays",
632 ScheduleDay_Name_1=schedule_name)
633 if idf.getobject("SCHEDULE:YEAR", name=schedule_name) is None:
634 idf.newidfobject("SCHEDULE:YEAR", Name=schedule_name,
635 Schedule_Type_Limits_Name=limits_name,
636 ScheduleWeek_Name_1=schedule_name,
637 Start_Month_1=1,
638 Start_Day_1=1,
639 End_Month_1=12,
640 End_Day_1=31)
642 def set_equipment(self, sim_settings: EnergyPlusSimSettings, idf: IDF,
643 name: str, zone_name: str,
644 space: ThermalZone):
645 """Set internal loads from equipment.
647 This function sets schedules and internal loads from equipment based
648 on the BIM2SIM Preprocessing, i.e. based on IFC data if available or on
649 templates.
651 Args:
652 sim_settings: BIM2SIM simulation settings
653 idf: idf file object
654 name: name of the new people idf object
655 zone_name: name of zone or zone_list
656 space: ThermalZone instance
657 """
658 schedule_name = "Schedule " + "Equipment " + space.usage.replace(',',
659 '')
660 profile_name = 'machines_profile'
661 self.set_day_week_year_schedule(idf, space.machines_profile[:24],
662 profile_name, schedule_name)
663 if sim_settings.ep_version in ["9-2-0", "9-4-0"]:
664 idf.newidfobject(
665 "ELECTRICEQUIPMENT",
666 Name=name,
667 Zone_or_ZoneList_Name=zone_name,
668 Schedule_Name=schedule_name,
669 Design_Level_Calculation_Method="Watts/Area",
670 Watts_per_Zone_Floor_Area=space.machines.to(
671 ureg.watt / ureg.meter ** 2).m
672 )
673 else:
674 idf.newidfobject(
675 "ELECTRICEQUIPMENT",
676 Name=name,
677 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name,
678 Schedule_Name=schedule_name,
679 Design_Level_Calculation_Method="Watts/Area",
680 Watts_per_Zone_Floor_Area=space.machines.to(
681 ureg.watt / ureg.meter ** 2).m
682 )
684 def set_lights(self, sim_settings: EnergyPlusSimSettings, idf: IDF, name: str,
685 zone_name: str, space: ThermalZone):
686 """Set internal loads from lighting.
688 This function sets schedules and lighting based on the
689 BIM2SIM Preprocessing, i.e. based on IFC data if available or on
690 templates.
692 Args:
693 sim_settings: BIM2SIM simulation settings
694 idf: idf file object
695 name: name of the new people idf object
696 zone_name: name of zone or zone_list
697 space: ThermalZone instance
698 """
699 schedule_name = "Schedule " + "Lighting " + space.usage.replace(',', '')
700 profile_name = 'lighting_profile'
701 self.set_day_week_year_schedule(idf, space.lighting_profile[:24],
702 profile_name, schedule_name)
703 mode = "Watts/Area"
704 watts_per_zone_floor_area = space.lighting_power.to(
705 ureg.watt / ureg.meter ** 2).m
706 return_air_fraction = 0.0
707 fraction_radiant = 0.42 # fraction radiant: cf. Table 1.28 in
708 # InputOutputReference EnergyPlus (Version 9.4.0), p. 506
709 fraction_visible = 0.18 # Todo: fractions do not match with .json
710 # Data. Maybe set by user-input later
711 if sim_settings.ep_version in ["9-2-0", "9-4-0"]:
712 idf.newidfobject(
713 "LIGHTS",
714 Name=name,
715 Zone_or_ZoneList_Name=zone_name,
716 Schedule_Name=schedule_name,
717 Design_Level_Calculation_Method=mode,
718 Watts_per_Zone_Floor_Area=watts_per_zone_floor_area,
719 Return_Air_Fraction=return_air_fraction,
720 Fraction_Radiant=fraction_radiant,
721 Fraction_Visible=fraction_visible
722 )
723 else:
724 idf.newidfobject(
725 "LIGHTS",
726 Name=name,
727 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name,
728 Schedule_Name=schedule_name,
729 Design_Level_Calculation_Method=mode,
730 Watts_per_Zone_Floor_Area=watts_per_zone_floor_area,
731 Return_Air_Fraction=return_air_fraction,
732 Fraction_Radiant=fraction_radiant,
733 Fraction_Visible=fraction_visible
734 )
736 @staticmethod
737 def set_infiltration(idf: IDF,
738 name: str, zone_name: str,
739 space: ThermalZone, ep_version: str):
740 """Set infiltration rate.
742 This function sets the infiltration rate per space based on the
743 BIM2SIM preprocessing values (IFC-based if available or
744 template-based).
746 Args:
747 idf: idf file object
748 name: name of the new people idf object
749 zone_name: name of zone or zone_list
750 space: ThermalZone instance
751 ep_version: Used version of EnergyPlus
752 """
753 if ep_version in ["9-2-0", "9-4-0"]:
754 idf.newidfobject(
755 "ZONEINFILTRATION:DESIGNFLOWRATE",
756 Name=name,
757 Zone_or_ZoneList_Name=zone_name,
758 Schedule_Name="Continuous",
759 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
760 Air_Changes_per_Hour=space.base_infiltration
761 )
762 else:
763 idf.newidfobject(
764 "ZONEINFILTRATION:DESIGNFLOWRATE",
765 Name=name,
766 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name,
767 Schedule_Name="Continuous",
768 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
769 Air_Changes_per_Hour=space.base_infiltration
770 )
772 @staticmethod
773 def set_natural_ventilation(idf: IDF, name: str, zone_name: str,
774 space: ThermalZone, ep_version):
775 """Set natural ventilation.
777 This function sets the natural ventilation per space based on the
778 BIM2SIM preprocessing values (IFC-based if available or
779 template-based). Natural ventilation is defined for winter, summer
780 and overheating cases, setting the air change per hours and minimum
781 and maximum outdoor temperature if applicable.
783 Args:
784 idf: idf file object
785 name: name of the new people idf object
786 zone_name: name of zone or zone_list
787 space: ThermalZone instance
788 ep_version: Used version of EnergyPlus
790 """
791 if ep_version in ["9-2-0", "9-4-0"]:
792 idf.newidfobject(
793 "ZONEVENTILATION:DESIGNFLOWRATE",
794 Name=name + '_winter',
795 Zone_or_ZoneList_Name=zone_name,
796 Schedule_Name="Continuous",
797 Ventilation_Type="Natural",
798 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
799 Air_Changes_per_Hour=space.winter_reduction_infiltration[0],
800 Minimum_Outdoor_Temperature=
801 space.winter_reduction_infiltration[1] - 273.15,
802 Maximum_Outdoor_Temperature=
803 space.winter_reduction_infiltration[2] - 273.15,
804 )
806 idf.newidfobject(
807 "ZONEVENTILATION:DESIGNFLOWRATE",
808 Name=name + '_summer',
809 Zone_or_ZoneList_Name=zone_name,
810 Schedule_Name="Continuous",
811 Ventilation_Type="Natural",
812 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
813 Air_Changes_per_Hour=space.max_summer_infiltration[0],
814 Minimum_Outdoor_Temperature
815 =space.max_summer_infiltration[1] - 273.15,
816 Maximum_Outdoor_Temperature
817 =space.max_summer_infiltration[2] - 273.15,
818 )
820 idf.newidfobject(
821 "ZONEVENTILATION:DESIGNFLOWRATE",
822 Name=name + '_overheating',
823 Zone_or_ZoneList_Name=zone_name,
824 Schedule_Name="Continuous",
825 Ventilation_Type="Natural",
826 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
827 # calculation of overheating infiltration is a simplification
828 # compared to the corresponding TEASER implementation which
829 # dynamically computes thresholds for overheating infiltration
830 # based on the zone temperature and additional factors.
831 Air_Changes_per_Hour=space.max_overheating_infiltration[0],
832 Minimum_Outdoor_Temperature
833 =space.max_summer_infiltration[2] - 273.15,
834 )
835 else:
836 idf.newidfobject(
837 "ZONEVENTILATION:DESIGNFLOWRATE",
838 Name=name + '_winter',
839 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name,
840 Schedule_Name="Continuous",
841 Ventilation_Type="Natural",
842 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
843 Air_Changes_per_Hour=space.winter_reduction_infiltration[0],
844 Minimum_Outdoor_Temperature=
845 space.winter_reduction_infiltration[1] - 273.15,
846 Maximum_Outdoor_Temperature=
847 space.winter_reduction_infiltration[2] - 273.15,
848 )
850 idf.newidfobject(
851 "ZONEVENTILATION:DESIGNFLOWRATE",
852 Name=name + '_summer',
853 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name,
854 Schedule_Name="Continuous",
855 Ventilation_Type="Natural",
856 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
857 Air_Changes_per_Hour=space.max_summer_infiltration[0],
858 Minimum_Outdoor_Temperature
859 =space.max_summer_infiltration[1] - 273.15,
860 Maximum_Outdoor_Temperature
861 =space.max_summer_infiltration[2] - 273.15,
862 )
864 idf.newidfobject(
865 "ZONEVENTILATION:DESIGNFLOWRATE",
866 Name=name + '_overheating',
867 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name,
868 Schedule_Name="Continuous",
869 Ventilation_Type="Natural",
870 Design_Flow_Rate_Calculation_Method="AirChanges/Hour",
871 # calculation of overheating infiltration is a simplification
872 # compared to the corresponding TEASER implementation which
873 # dynamically computes thresholds for overheating infiltration
874 # based on the zone temperature and additional factors.
875 Air_Changes_per_Hour=space.max_overheating_infiltration[0],
876 Minimum_Outdoor_Temperature
877 =space.max_summer_infiltration[2] - 273.15,
878 )
880 def set_day_hvac_template(self, idf: IDF, space: ThermalZone, name: str):
881 """Set 24 hour hvac template.
883 This function sets idf schedules with 24hour schedules for heating and
884 cooling.
886 Args:
887 idf: idf file object
888 space: ThermalZone
889 name: IDF Thermostat Name
890 """
891 htg_schedule_name = "Schedule " + "Heating " + space.usage.replace(
892 ',', '')
893 self.set_day_week_year_schedule(idf, space.heating_profile[:24],
894 'heating_profile',
895 htg_schedule_name)
897 clg_schedule_name = "Schedule " + "Cooling " + space.usage.replace(
898 ',', '')
899 self.set_day_week_year_schedule(idf, space.cooling_profile[:24],
900 'cooling_profile',
901 clg_schedule_name)
902 stat = idf.newidfobject(
903 "HVACTEMPLATE:THERMOSTAT",
904 Name=name,
905 Heating_Setpoint_Schedule_Name=htg_schedule_name,
906 Cooling_Setpoint_Schedule_Name=clg_schedule_name
907 )
908 return stat
910 def set_hvac_template(self, idf: IDF, name: str,
911 heating_sp: Union[int, float],
912 cooling_sp: Union[int, float],
913 mode='setback'):
914 """Set heating and cooling templates (manually).
916 This function manually sets heating and cooling templates.
918 Args:
919 idf: idf file object
920 heating_sp: float or int for heating set point
921 cooling_sp: float or int for cooling set point
922 name: IDF Thermostat Name
923 """
924 if cooling_sp < 20:
925 cooling_sp = 26
926 elif cooling_sp < 24:
927 cooling_sp = 23
929 setback_htg = 18 # "T_threshold_heating"
930 setback_clg = 26 # "T_threshold_cooling"
932 # ensure setback temperature actually performs a setback on temperature
933 if setback_htg > heating_sp:
934 setback_htg = heating_sp
935 if setback_clg < cooling_sp:
936 setback_clg = cooling_sp
938 if mode == "setback":
939 htg_alldays = self._define_schedule_part('Alldays',
940 [('5:00', setback_htg),
941 ('21:00', heating_sp),
942 ('24:00', setback_htg)])
943 clg_alldays = self._define_schedule_part('Alldays',
944 [('5:00', setback_clg),
945 ('21:00', cooling_sp),
946 ('24:00', setback_clg)])
947 htg_name = "H_SetBack_" + str(heating_sp)
948 clg_name = "C_SetBack_" + str(cooling_sp)
949 if idf.getobject("SCHEDULE:COMPACT", htg_name) is None:
950 self.write_schedule(idf, htg_name, [htg_alldays, ])
951 else:
952 idf.getobject("SCHEDULE:COMPACT", htg_name)
953 if idf.getobject("SCHEDULE:COMPACT", clg_name) is None:
954 self.write_schedule(idf, clg_name, [clg_alldays, ])
955 else:
956 idf.getobject("SCHEDULE:COMPACT", clg_name)
957 stat = idf.newidfobject(
958 "HVACTEMPLATE:THERMOSTAT",
959 Name="STAT_" + name,
960 Heating_Setpoint_Schedule_Name=htg_name,
961 Cooling_Setpoint_Schedule_Name=clg_name,
962 )
964 if mode == "constant":
965 stat = idf.newidfobject(
966 "HVACTEMPLATE:THERMOSTAT",
967 Name="STAT_" + name,
968 Constant_Heating_Setpoint=heating_sp,
969 Constant_Cooling_Setpoint=cooling_sp,
970 )
971 return stat
973 @staticmethod
974 def write_schedule(idf: IDF, sched_name: str, sched_part_list: list):
975 """Write schedules to idf.
977 This function writes a schedule to the idf. Only used for manual
978 setup of schedules (combined with set_hvac_template).
980 Args:
981 idf: idf file object
982 sched_name: str with name of the schedule
983 sched_part_list: list of schedule parts (cf. function
984 _define_schedule_part)
985 """
986 sched_list = {}
987 field_count = 1
988 for parts in sched_part_list:
989 field_count += 1
990 sched_list.update({'Field_' + str(field_count): 'For: ' + parts[0]})
991 part = parts[1]
992 for set in part:
993 field_count += 1
994 sched_list.update(
995 {'Field_' + str(field_count): 'Until: ' + str(set[0])})
996 field_count += 1
997 sched_list.update({'Field_' + str(field_count): str(set[1])})
998 if idf.getobject("SCHEDULETYPELIMITS", "Temperature") is None:
999 idf.newidfobject("SCHEDULETYPELIMITS", Name="Temperature")
1001 sched = idf.newidfobject(
1002 "SCHEDULE:COMPACT",
1003 Name=sched_name,
1004 Schedule_Type_Limits_Name="Temperature",
1005 Field_1="Through: 12/31",
1006 **sched_list
1007 )
1008 return sched
1010 @staticmethod
1011 def _define_schedule_part(
1012 days: str, til_time_temp: list[tuple[str, Union[int, float]]]):
1013 """Defines a part of a schedule.
1015 Args:
1016 days: string: Weekdays, Weekends, Alldays, AllOtherDays, Saturdays,
1017 Sundays, ...
1018 til_time_temp: List of tuples
1019 (until-time format 'hh:mm' (24h) as str),
1020 temperature until this time in Celsius),
1021 e.g. (05:00, 18)
1022 """
1023 return [days, til_time_temp]
1025 @staticmethod
1026 def add_shadings(elements: dict, idf: IDF):
1027 """Add shading boundaries to idf.
1029 Args:
1030 elements: dict[guid: element]
1031 idf: idf file object
1032 """
1033 logger.info("Add Shadings ...")
1034 spatials = []
1035 ext_spatial_elem = filter_elements(elements, ExternalSpatialElement)
1036 for elem in ext_spatial_elem:
1037 for sb in elem.space_boundaries:
1038 spatials.append(sb)
1039 if not spatials:
1040 return
1041 pure_spatials = []
1042 description_list = [s.ifc.Description for s in spatials]
1043 descriptions = list(dict.fromkeys(description_list))
1044 shades_included = ("Shading:Building" or "Shading:Site") in descriptions
1046 # check if ifc has dedicated shading space boundaries included and
1047 # append them to pure_spatials for further processing
1048 if shades_included:
1049 for s in spatials:
1050 if s.ifc.Description in ["Shading:Building", "Shading:Site"]:
1051 pure_spatials.append(s)
1052 # if no shading boundaries are included in ifc, derive these from the
1053 # set of given space boundaries and append them to pure_spatials for
1054 # further processing
1055 else:
1056 for s in spatials:
1057 # only consider almost horizontal 2b shapes (roof-like SBs)
1058 if s.level_description == '2b':
1059 angle = math.degrees(
1060 gp_Dir(s.bound_normal).Angle(gp_Dir(gp_XYZ(0, 0, 1))))
1061 if not ((-45 < angle < 45) or (135 < angle < 225)):
1062 continue
1063 if s.related_bound and s.related_bound.ifc.RelatingSpace.is_a(
1064 'IfcSpace'):
1065 continue
1066 pure_spatials.append(s)
1068 # create idf shadings from set of pure_spatials
1069 for s in pure_spatials:
1070 obj = idf.newidfobject('SHADING:BUILDING:DETAILED',
1071 Name=s.guid,
1072 )
1073 obj_pnts = PyOCCTools.get_points_of_face(s.bound_shape)
1074 obj_coords = []
1075 for pnt in obj_pnts:
1076 co = tuple(round(p, 3) for p in pnt.Coord())
1077 obj_coords.append(co)
1078 obj.setcoords(obj_coords)
1080 def add_shading_control(self, shading_type, elements,
1081 idf, outdoor_temp=22, solar=40):
1082 """Add a default shading control to IDF.
1083 Two criteria must be met such that the window shades are set: the
1084 outdoor temperature must exceed a certain temperature and the solar
1085 radiation [W/m²] must be greater than a certain heat flow.
1086 Args:
1087 shading_type: shading type, 'Interior' or 'Exterior'
1088 elements: elements
1089 idf: idf
1090 outdoor_temp: outdoor temperature [°C]
1091 solar: solar radiation on window surface [W/m²]
1092 """
1093 zones = filter_elements(elements, ThermalZone)
1095 for zone in zones:
1096 zone_name = zone.guid
1097 zone_openings = [sb for sb in zone.space_boundaries if
1098 isinstance(sb.bound_element, Window)]
1099 if not zone_openings:
1100 continue
1101 fenestration_dict = {}
1102 for i, opening in enumerate(zone_openings):
1103 fenestration_dict.update({'Fenestration_Surface_' + str(
1104 i+1) + '_Name': opening.guid})
1105 shade_control_name = "ShadeControl_" + zone_name
1106 opening_obj = idf.getobject(
1107 'FENESTRATIONSURFACE:DETAILED', zone_openings[
1108 0].guid)
1109 if opening_obj:
1110 construction_name = opening_obj.Construction_Name + "_" + \
1111 shading_type
1112 else:
1113 continue
1114 if not idf.getobject(
1115 "WINDOWSHADINGCONTROL", shade_control_name):
1116 idf.newidfobject("WINDOWSHADINGCONTROL",
1117 Name=shade_control_name,
1118 Zone_Name=zone_name,
1119 Shading_Type=shading_type+"Shade",
1120 Construction_with_Shading_Name=construction_name,
1121 Shading_Control_Type=
1122 'OnIfHighOutdoorAirTempAndHighSolarOnWindow',
1123 Setpoint=outdoor_temp,
1124 Setpoint_2=solar,
1125 Multiple_Surface_Control_Type='Group',
1126 **fenestration_dict
1127 )
1129 @staticmethod
1130 def set_simulation_control(sim_settings: EnergyPlusSimSettings, idf):
1131 """Set simulation control parameters.
1133 This function sets general simulation control parameters. These can
1134 be easily overwritten in the exported idf.
1135 Args:
1136 sim_settings: EnergyPlusSimSettings
1137 idf: idf file object
1138 """
1139 logger.info("Set Simulation Control ...")
1140 for sim_control in idf.idfobjects["SIMULATIONCONTROL"]:
1141 if sim_settings.system_sizing:
1142 sim_control.Do_System_Sizing_Calculation = 'Yes'
1143 else:
1144 sim_control.Do_System_Sizing_Calculation = 'No'
1145 if sim_settings.run_for_sizing_periods:
1146 sim_control.Run_Simulation_for_Sizing_Periods = 'Yes'
1147 else:
1148 sim_control.Run_Simulation_for_Sizing_Periods = 'No'
1149 if sim_settings.run_for_weather_period:
1150 sim_control.Run_Simulation_for_Weather_File_Run_Periods = 'Yes'
1151 else:
1152 sim_control.Run_Simulation_for_Weather_File_Run_Periods = 'No'
1153 if sim_settings.set_run_period:
1154 sim_control.Run_Simulation_for_Weather_File_Run_Periods = 'Yes'
1156 if sim_settings.set_run_period:
1157 for run_period in idf.idfobjects["RUNPERIOD"]:
1158 run_period.Begin_Month = sim_settings.run_period_start_month
1159 run_period.Begin_Day_of_Month = (
1160 sim_settings.run_period_start_day)
1161 run_period.End_Month = sim_settings.run_period_end_month
1162 run_period.End_Day_of_Month = sim_settings.run_period_end_day
1164 for building in idf.idfobjects['BUILDING']:
1165 building.Solar_Distribution = sim_settings.solar_distribution
1167 @staticmethod
1168 def set_ground_temperature(idf: IDF, t_ground: ureg.Quantity):
1169 """Set the ground temperature in the idf.
1171 Args:
1172 idf: idf file object
1173 t_ground: ground temperature as ureg.Quantity
1174 """
1175 logger.info("Set ground temperature...")
1177 string = '_Ground_Temperature'
1178 month_list = ['January', 'February', 'March', 'April', 'May', 'June',
1179 'July', 'August', 'September', 'October',
1180 'November', 'December']
1181 temp_dict = {}
1182 for month in month_list:
1183 temp_dict.update({month + string: t_ground.to(ureg.degC).m})
1184 idf.newidfobject("SITE:GROUNDTEMPERATURE:BUILDINGSURFACE", **temp_dict)
1186 @staticmethod
1187 def set_output_variables(idf: IDF, sim_settings: EnergyPlusSimSettings):
1188 """Set user defined output variables in the idf file.
1190 Args:
1191 idf: idf file object
1192 sim_settings: BIM2SIM simulation settings
1193 """
1194 logger.info("Set output variables ...")
1196 # general output settings. May be moved to general settings
1197 out_control = idf.idfobjects['OUTPUTCONTROL:TABLE:STYLE']
1198 out_control[0].Column_Separator = sim_settings.output_format
1199 out_control[0].Unit_Conversion = sim_settings.unit_conversion
1201 # remove all existing output variables with reporting frequency
1202 # "Timestep"
1203 out_var = [v for v in idf.idfobjects['OUTPUT:VARIABLE']
1204 if v.Reporting_Frequency.upper() == "TIMESTEP"]
1205 for var in out_var:
1206 idf.removeidfobject(var)
1207 if 'output_outdoor_conditions' in sim_settings.output_keys:
1208 idf.newidfobject(
1209 "OUTPUT:VARIABLE",
1210 Variable_Name="Site Outdoor Air Drybulb Temperature",
1211 Reporting_Frequency="Hourly",
1212 )
1213 idf.newidfobject(
1214 "OUTPUT:VARIABLE",
1215 Variable_Name="Site Outdoor Air Humidity Ratio",
1216 Reporting_Frequency="Hourly",
1217 )
1218 idf.newidfobject(
1219 "OUTPUT:VARIABLE",
1220 Variable_Name="Site Outdoor Air Relative Humidity",
1221 Reporting_Frequency="Hourly",
1222 )
1223 idf.newidfobject(
1224 "OUTPUT:VARIABLE",
1225 Variable_Name="Site Outdoor Air Barometric Pressure",
1226 Reporting_Frequency="Hourly",
1227 )
1228 idf.newidfobject(
1229 "OUTPUT:VARIABLE",
1230 Variable_Name="Site Diffuse Solar Radiation Rate per Area",
1231 Reporting_Frequency="Hourly",
1232 )
1233 idf.newidfobject(
1234 "OUTPUT:VARIABLE",
1235 Variable_Name="Site Direct Solar Radiation Rate per Area",
1236 Reporting_Frequency="Hourly",
1237 )
1238 idf.newidfobject(
1239 "OUTPUT:VARIABLE",
1240 Variable_Name="Site Ground Temperature",
1241 Reporting_Frequency="Hourly",
1242 )
1243 idf.newidfobject(
1244 "OUTPUT:VARIABLE",
1245 Variable_Name="Site Wind Speed",
1246 Reporting_Frequency="Hourly",
1247 )
1248 idf.newidfobject(
1249 "OUTPUT:VARIABLE",
1250 Variable_Name="Site Wind Direction",
1251 Reporting_Frequency="Hourly",
1252 )
1253 if 'output_zone_temperature' in sim_settings.output_keys:
1254 idf.newidfobject(
1255 "OUTPUT:VARIABLE",
1256 Variable_Name="Zone Mean Air Temperature",
1257 Reporting_Frequency="Hourly",
1258 )
1259 idf.newidfobject(
1260 "OUTPUT:VARIABLE",
1261 Variable_Name="Zone Operative Temperature",
1262 Reporting_Frequency="Hourly",
1263 )
1264 idf.newidfobject(
1265 "OUTPUT:VARIABLE",
1266 Variable_Name="Zone Air Relative Humidity",
1267 Reporting_Frequency="Hourly",
1268 )
1269 if 'output_internal_gains' in sim_settings.output_keys:
1270 idf.newidfobject(
1271 "OUTPUT:VARIABLE",
1272 Variable_Name="Zone People Occupant Count",
1273 Reporting_Frequency="Hourly",
1274 )
1275 idf.newidfobject(
1276 "OUTPUT:VARIABLE",
1277 Variable_Name="Zone People Total Heating Rate",
1278 Reporting_Frequency="Hourly",
1279 )
1280 idf.newidfobject(
1281 "OUTPUT:VARIABLE",
1282 Variable_Name="Zone Electric Equipment Total Heating Rate",
1283 Reporting_Frequency="Hourly",
1284 )
1285 idf.newidfobject(
1286 "OUTPUT:VARIABLE",
1287 Variable_Name="Zone Lights Total Heating Rate",
1288 Reporting_Frequency="Hourly",
1289 )
1290 if 'output_zone' in sim_settings.output_keys:
1291 idf.newidfobject(
1292 "OUTPUT:VARIABLE",
1293 Variable_Name="Zone Thermostat Heating Setpoint Temperature",
1294 Reporting_Frequency="Hourly",
1295 )
1296 idf.newidfobject(
1297 "OUTPUT:VARIABLE",
1298 Variable_Name="Zone Thermostat Cooling Setpoint Temperature",
1299 Reporting_Frequency="Hourly",
1300 )
1301 idf.newidfobject(
1302 "OUTPUT:VARIABLE",
1303 Variable_Name="Zone Ideal Loads Zone Total Cooling Rate",
1304 Reporting_Frequency="Hourly",
1305 )
1306 idf.newidfobject(
1307 "OUTPUT:VARIABLE",
1308 Variable_Name="Zone Ideal Loads Zone Total Heating Rate",
1309 Reporting_Frequency="Hourly",
1310 )
1311 idf.newidfobject(
1312 "OUTPUT:VARIABLE",
1313 Variable_Name="Zone Ideal Loads Zone Total Heating Energy",
1314 Reporting_Frequency="Hourly",
1315 )
1316 idf.newidfobject(
1317 "OUTPUT:VARIABLE",
1318 Variable_Name="Zone Ideal Loads Zone Total Cooling Energy",
1319 Reporting_Frequency="Hourly",
1320 )
1321 idf.newidfobject(
1322 "OUTPUT:VARIABLE",
1323 Variable_Name="Zone Windows Total Heat Gain Rate",
1324 Reporting_Frequency="Hourly",
1325 )
1326 idf.newidfobject(
1327 "OUTPUT:VARIABLE",
1328 Variable_Name="Zone Windows Total Heat Gain Energy",
1329 Reporting_Frequency="Hourly",
1330 )
1331 idf.newidfobject(
1332 "OUTPUT:VARIABLE",
1333 Variable_Name="Zone Windows Total Transmitted Solar Radiation "
1334 "Energy",
1335 Reporting_Frequency="Hourly",
1336 )
1337 idf.newidfobject(
1338 "OUTPUT:VARIABLE",
1339 Variable_Name="Zone Air System Sensible Heating Energy",
1340 Reporting_Frequency="Hourly",
1341 )
1342 idf.newidfobject(
1343 "OUTPUT:VARIABLE",
1344 Variable_Name="Zone Air System Sensible Cooling Energy",
1345 Reporting_Frequency="Hourly",
1346 )
1347 if 'output_infiltration' in sim_settings.output_keys:
1348 idf.newidfobject(
1349 "OUTPUT:VARIABLE",
1350 Variable_Name="Zone Infiltration Sensible Heat Gain Energy",
1351 Reporting_Frequency="Hourly",
1352 )
1353 idf.newidfobject(
1354 "OUTPUT:VARIABLE",
1355 Variable_Name="Zone Infiltration Sensible Heat Loss Energy",
1356 Reporting_Frequency="Hourly",
1357 )
1358 idf.newidfobject(
1359 "OUTPUT:VARIABLE",
1360 Variable_Name="Zone Infiltration Air Change Rate",
1361 Reporting_Frequency="Hourly",
1362 )
1363 idf.newidfobject(
1364 "OUTPUT:VARIABLE",
1365 Variable_Name="Zone Ventilation Air Change Rate",
1366 Reporting_Frequency="Hourly",
1367 )
1368 idf.newidfobject(
1369 "OUTPUT:VARIABLE",
1370 Variable_Name="Zone Ventilation Standard Density Volume Flow Rate",
1371 Reporting_Frequency="Hourly",
1372 )
1373 idf.newidfobject(
1374 "OUTPUT:VARIABLE",
1375 Variable_Name="Zone Ventilation Total Heat Gain Energy",
1376 Reporting_Frequency="Hourly",
1377 )
1378 idf.newidfobject(
1379 "OUTPUT:VARIABLE",
1380 Variable_Name="Zone Ventilation Total Heat Loss Energy",
1381 Reporting_Frequency="Hourly",
1382 )
1384 if 'output_meters' in sim_settings.output_keys:
1385 idf.newidfobject(
1386 "OUTPUT:METER",
1387 Key_Name="Heating:EnergyTransfer",
1388 Reporting_Frequency="Hourly",
1389 )
1390 idf.newidfobject(
1391 "OUTPUT:METER",
1392 Key_Name="Cooling:EnergyTransfer",
1393 Reporting_Frequency="Hourly",
1394 )
1395 if 'output_dxf' in sim_settings.output_keys:
1396 idf.newidfobject("OUTPUT:SURFACES:DRAWING",
1397 Report_Type="DXF")
1398 if sim_settings.cfd_export:
1399 idf.newidfobject(
1400 "OUTPUT:VARIABLE",
1401 Variable_Name="Surface Inside Face Temperature",
1402 Reporting_Frequency="Hourly",
1403 )
1404 idf.newidfobject("OUTPUT:DIAGNOSTICS",
1405 Key_1="DisplayAdvancedReportVariables",
1406 Key_2="DisplayExtraWarnings")
1407 return idf
1409 @staticmethod
1410 def export_geom_to_idf(sim_settings: EnergyPlusSimSettings,
1411 elements: dict, idf: IDF):
1412 """Write space boundary geometry to idf.
1414 This function converts the space boundary bound_shape from
1415 OpenCascade to idf geometry.
1417 Args:
1418 elements: dict[guid: element]
1419 idf: idf file object
1420 """
1421 logger.info("Export IDF geometry")
1422 bounds = filter_elements(elements, SpaceBoundary)
1423 for bound in bounds:
1424 idfp = IdfObject(sim_settings, bound, idf)
1425 if idfp.skip_bound:
1426 idf.popidfobject(idfp.key, -1)
1427 logger.warning(
1428 "Boundary with the GUID %s (%s) is skipped (due to "
1429 "missing boundary conditions)!",
1430 idfp.name, idfp.surface_type)
1431 continue
1432 bounds_2b = filter_elements(elements, SpaceBoundary2B)
1433 for b_bound in bounds_2b:
1434 idfp = IdfObject(sim_settings, b_bound, idf)
1435 if idfp.skip_bound:
1436 logger.warning(
1437 "Boundary with the GUID %s (%s) is skipped (due to "
1438 "missing boundary conditions)!",
1439 idfp.name, idfp.surface_type)
1440 continue
1442 @staticmethod
1443 def idf_validity_check(idf):
1444 """Perform idf validity check and simple fixes.
1446 This function performs a basic validity check of the resulting idf.
1447 It removes openings from adiabatic surfaces and very small surfaces.
1449 Args:
1450 idf: idf file object
1451 """
1452 logger.info('Start IDF Validity Checker')
1454 # remove erroneous fenestration surfaces which do may crash
1455 # EnergyPlus simulation
1456 fenestrations = idf.idfobjects['FENESTRATIONSURFACE:DETAILED']
1458 # Create a list of fenestrations to remove
1459 to_remove = []
1461 for fenestration in fenestrations:
1462 should_remove = False
1464 # Check for missing building surface reference
1465 if not fenestration.Building_Surface_Name:
1466 should_remove = True
1467 else:
1468 # Check if the referenced surface is adiabatic
1469 building_surface = idf.getobject(
1470 'BUILDINGSURFACE:DETAILED',
1471 fenestration.Building_Surface_Name
1472 )
1473 if building_surface and building_surface.Outside_Boundary_Condition == 'Adiabatic':
1474 should_remove = True
1476 if should_remove:
1477 to_remove.append(fenestration)
1479 # Remove the collected fenestrations
1480 for fenestration in to_remove:
1481 logger.info('Removed Fenestration: %s' % fenestration.Name)
1482 idf.removeidfobject(fenestration)
1484 # Check if shading control elements contain unavailable fenestration
1485 fenestration_updated = idf.idfobjects['FENESTRATIONSURFACE:DETAILED']
1486 shading_control = idf.idfobjects['WINDOWSHADINGCONTROL']
1487 fenestration_guids = [fe.Name for fe in fenestration_updated]
1488 for shc in shading_control:
1489 # create a list with current fenestration guids (only available
1490 # fenestration)
1491 fenestration_guids_new = []
1492 skipped_fenestration = False # flag for unavailable fenestration
1493 for attr_name in dir(shc):
1494 if ('Fenestration_Surface' in attr_name):
1495 if (getattr(shc, attr_name) in
1496 fenestration_guids):
1497 fenestration_guids_new.append(getattr(shc, attr_name))
1498 elif (getattr(shc, attr_name) not in
1499 fenestration_guids) and getattr(shc, attr_name):
1500 skipped_fenestration = True
1501 # if the shading control element containes unavailable
1502 # fenestration objects, the shading control must be updated to
1503 # prevent errors in simulation
1504 if fenestration_guids_new and skipped_fenestration:
1505 fenestration_dict = {}
1506 for i, guid in enumerate(fenestration_guids_new):
1507 fenestration_dict.update({'Fenestration_Surface_' + str(
1508 i + 1) + '_Name': guid})
1509 # remove previous shading control from idf and create a new one
1510 # removing individual attributes of the shading element
1511 # caused errors, so new shading control is created
1512 idf.removeidfobject(shc)
1513 idf.newidfobject("WINDOWSHADINGCONTROL", Name=shc.Name,
1514 Zone_Name=shc.Zone_Name,
1515 Shading_Type=shc.Shading_Type,
1516 Construction_with_Shading_Name=
1517 shc.Construction_with_Shading_Name,
1518 Shading_Control_Type=shc.Shading_Control_Type,
1519 Setpoint=shc.Setpoint,
1520 Setpoint_2=shc.Setpoint_2,
1521 Multiple_Surface_Control_Type=
1522 shc.Multiple_Surface_Control_Type,
1523 **fenestration_dict)
1524 logger.info('Updated Shading Control due to unavailable '
1525 'fenestration: %s' % shc.Name)
1527 # check for small building surfaces and remove them
1528 sfs = idf.getsurfaces()
1529 small_area_obj = [s for s in sfs
1530 if PyOCCTools.get_shape_area(
1531 PyOCCTools.make_faces_from_pnts(s.coords)) < 1e-2]
1533 for obj in small_area_obj:
1534 logger.info('Removed small area: %s' % obj.Name)
1535 idf.removeidfobject(obj)
1537 # check for small shading surfaces and remove them
1538 shadings = idf.getshadingsurfaces()
1539 small_area_obj = [s for s in shadings if PyOCCTools.get_shape_area(
1540 PyOCCTools.make_faces_from_pnts(s.coords)) < 1e-2]
1542 for obj in small_area_obj:
1543 logger.info('Removed small area: %s' % obj.Name)
1544 idf.removeidfobject(obj)
1546 # Check for building surfaces holding default window materials
1547 bsd = idf.idfobjects['BUILDINGSURFACE:DETAILED']
1548 for sf in bsd:
1549 if sf.Construction_Name == 'BS Exterior Window':
1550 logger.info(
1551 'Surface due to invalid material: %s' % sf.Name)
1552 idf.removeidfobject(sf)
1553 logger.info('IDF Validity Checker done')
1556class IdfObject:
1557 """Create idf elements for surfaces.
1559 This class holds all data required for the idf setup of
1560 BUILDINGSURFACE:DETAILED and FENESTRATIONSURFACE:DETAILED.
1561 This includes further methods for processing the preprocessed information
1562 from the BIM2SIM process for the use in idf (e.g., surface type mapping).
1563 """
1565 def __init__(self, sim_settings, inst_obj, idf):
1566 self.name = inst_obj.guid
1567 self.building_surface_name = None
1568 self.key = None
1569 self.out_bound_cond = ''
1570 self.out_bound_cond_obj = ''
1571 self.sun_exposed = ''
1572 self.wind_exposed = ''
1573 self.surface_type = None
1574 self.physical = inst_obj.physical
1575 self.construction_name = None
1576 self.related_bound = inst_obj.related_bound
1577 self.this_bound = inst_obj
1578 self.skip_bound = False
1579 self.bound_shape = inst_obj.bound_shape
1580 self.add_window_shade = False
1581 if not hasattr(inst_obj.bound_thermal_zone, 'guid'):
1582 self.skip_bound = True
1583 return
1584 self.zone_name = inst_obj.bound_thermal_zone.guid
1585 if inst_obj.parent_bound:
1586 self.key = "FENESTRATIONSURFACE:DETAILED"
1587 if sim_settings.add_window_shading == 'Interior':
1588 self.add_window_shade = 'Interior'
1589 elif sim_settings.add_window_shading == 'Exterior':
1590 self.add_window_shade = 'Exterior'
1591 else:
1592 self.key = "BUILDINGSURFACE:DETAILED"
1593 if inst_obj.parent_bound:
1594 self.building_surface_name = inst_obj.parent_bound.guid
1595 self.map_surface_types(inst_obj)
1596 self.map_boundary_conditions(inst_obj)
1597 self.set_preprocessed_construction_name()
1598 # only set a construction name if this construction is available
1599 if not self.construction_name \
1600 or not (idf.getobject("CONSTRUCTION", self.construction_name)
1601 or idf.getobject("CONSTRUCTION:AIRBOUNDARY",
1602 self.construction_name)):
1603 self.set_construction_name()
1604 obj = self.set_idfobject_attributes(idf)
1605 if obj is not None:
1606 self.set_idfobject_coordinates(obj, idf, inst_obj)
1607 else:
1608 pass
1610 def set_construction_name(self):
1611 """Set default construction names.
1613 This function sets default constructions for all idf surface types.
1614 Should only be used if no construction is available for the current
1615 object.
1616 """
1617 if self.surface_type == "Wall":
1618 self.construction_name = "Project Wall"
1619 elif self.surface_type == "Roof":
1620 self.construction_name = "Project Flat Roof"
1621 elif self.surface_type == "Ceiling":
1622 self.construction_name = "Project Ceiling"
1623 elif self.surface_type == "Floor":
1624 self.construction_name = "Project Floor"
1625 elif self.surface_type == "Door":
1626 self.construction_name = "Project Door"
1627 elif self.surface_type == "Window":
1628 self.construction_name = "Project External Window"
1630 def set_preprocessed_construction_name(self):
1631 """Set preprocessed constructions.
1633 This function sets constructions of idf surfaces to preprocessed
1634 constructions. Virtual space boundaries are set to be an air wall
1635 (not defined in preprocessing).
1636 """
1637 # set air wall for virtual bounds
1638 if not self.physical:
1639 if self.out_bound_cond == "Surface":
1640 self.construction_name = "Air Wall"
1641 else:
1642 rel_elem = self.this_bound.bound_element
1643 if not rel_elem:
1644 return
1645 if any([isinstance(rel_elem, window) for window in
1646 all_subclasses(Window, include_self=True)]):
1647 self.construction_name = 'Window_WM_' + \
1648 rel_elem.layerset.layers[
1649 0].material.name \
1650 + '_' + str(
1651 rel_elem.layerset.layers[0].thickness.to(ureg.metre).m)
1652 else:
1653 self.construction_name = (rel_elem.key.replace(
1654 "Disaggregated", "") + '_' + str(len(
1655 rel_elem.layerset.layers)) + '_' + '_'.join(
1656 [str(l.thickness.to(ureg.metre).m) for l in
1657 rel_elem.layerset.layers]))
1659 def set_idfobject_coordinates(self, obj, idf: IDF,
1660 inst_obj: Union[SpaceBoundary,
1661 SpaceBoundary2B]):
1662 """Export surface coordinates.
1664 This function exports the surface coordinates from the BIM2SIM Space
1665 Boundary instance to idf.
1666 Circular shapes and shapes with more than 120 vertices
1667 (BuildingSurfaces) or more than 4 vertices (fenestration) are
1668 simplified.
1670 Args:
1671 obj: idf-surface object (buildingSurface:Detailed or fenestration)
1672 idf: idf file object
1673 inst_obj: SpaceBoundary instance
1674 """
1675 # write bound_shape to obj
1676 obj_pnts = PyOCCTools.get_points_of_face(self.bound_shape)
1677 obj_coords = []
1678 for pnt in obj_pnts:
1679 co = tuple(round(p, 3) for p in pnt.Coord())
1680 obj_coords.append(co)
1681 try:
1682 obj.setcoords(obj_coords)
1683 except Exception as ex:
1684 logger.warning(f"Unexpected {ex=}. Setting coordinates for "
1685 f"{inst_obj.guid} failed. This element is not "
1686 f"exported."
1687 f"{type(ex)=}")
1688 self.skip_bound = True
1689 return
1690 circular_shape = self.get_circular_shape(obj_pnts)
1691 try:
1692 if (3 <= len(obj_coords) <= 120
1693 and self.key == "BUILDINGSURFACE:DETAILED") \
1694 or (3 <= len(obj_coords) <= 4
1695 and self.key == "FENESTRATIONSURFACE:DETAILED"):
1696 obj.setcoords(obj_coords)
1697 elif circular_shape is True and self.surface_type != 'Door':
1698 self.process_circular_shapes(idf, obj_coords, obj, inst_obj)
1699 else:
1700 self.process_other_shapes(inst_obj, obj)
1701 except Exception as ex:
1702 logger.warning(f"Unexpected {ex=}. Setting coordinates for "
1703 f"{inst_obj.guid} failed. This element is not "
1704 f"exported."
1705 f"{type(ex)=}")
1707 def set_idfobject_attributes(self, idf: IDF):
1708 """Writes precomputed surface attributes to idf.
1710 Args:
1711 idf: the idf file
1712 """
1713 if self.surface_type is not None:
1714 if self.key == "BUILDINGSURFACE:DETAILED":
1715 if self.surface_type.lower() in {"DOOR".lower(),
1716 "Window".lower()}:
1717 self.surface_type = "Wall"
1718 obj = idf.newidfobject(
1719 self.key,
1720 Name=self.name,
1721 Surface_Type=self.surface_type,
1722 Construction_Name=self.construction_name,
1723 Outside_Boundary_Condition=self.out_bound_cond,
1724 Outside_Boundary_Condition_Object=self.out_bound_cond_obj,
1725 Zone_Name=self.zone_name,
1726 Sun_Exposure=self.sun_exposed,
1727 Wind_Exposure=self.wind_exposed,
1728 )
1729 else:
1730 obj = idf.newidfobject(
1731 self.key,
1732 Name=self.name,
1733 Surface_Type=self.surface_type,
1734 Construction_Name=self.construction_name,
1735 Building_Surface_Name=self.building_surface_name,
1736 Outside_Boundary_Condition_Object=self.out_bound_cond_obj,
1737 )
1738 return obj
1740 def map_surface_types(self, inst_obj: Union[SpaceBoundary,
1741 SpaceBoundary2B]):
1742 """Map surface types.
1744 This function maps the attributes of a SpaceBoundary instance to idf
1745 surface type.
1747 Args:
1748 inst_obj: SpaceBoundary instance
1749 """
1750 # TODO use bim2sim elements mapping instead of ifc.is_a()
1751 # TODO update to new disaggregations
1752 elem = inst_obj.bound_element
1753 surface_type = None
1754 if elem is not None:
1755 if any([isinstance(elem, wall) for wall in all_subclasses(Wall,
1756 include_self=True)]):
1757 surface_type = 'Wall'
1758 elif any([isinstance(elem, door) for door in all_subclasses(Door,
1759 include_self=True)]):
1760 surface_type = "Door"
1761 elif any([isinstance(elem, window) for window in all_subclasses(
1762 Window, include_self=True)]):
1763 surface_type = "Window"
1764 elif any([isinstance(elem, roof) for roof in all_subclasses(Roof,
1765 include_self=True)]):
1766 surface_type = "Roof"
1767 elif any([isinstance(elem, slab) for slab in all_subclasses(Slab,
1768 include_self=True)]):
1769 if any([isinstance(elem, floor) for floor in all_subclasses(
1770 GroundFloor, include_self=True)]):
1771 surface_type = "Floor"
1772 elif any([isinstance(elem, floor) for floor in all_subclasses(
1773 InnerFloor, include_self=True)]):
1774 if inst_obj.top_bottom == BoundaryOrientation.bottom:
1775 surface_type = "Floor"
1776 elif inst_obj.top_bottom == BoundaryOrientation.top:
1777 surface_type = "Ceiling"
1778 elif inst_obj.top_bottom == BoundaryOrientation.vertical:
1779 surface_type = "Wall"
1780 logger.warning(f"InnerFloor with vertical orientation "
1781 f"found, exported as wall, "
1782 f"GUID: {inst_obj.guid}.")
1783 else:
1784 logger.warning(f"InnerFloor was not correctly matched "
1785 f"to surface type for GUID: "
1786 f"{inst_obj.guid}.")
1787 surface_type = "Floor"
1788 # elif elem.ifc is not None:
1789 # if elem.ifc.is_a("IfcBeam"):
1790 # if not PyOCCTools.compare_direction_of_normals(
1791 # inst_obj.bound_normal, gp_XYZ(0, 0, 1)):
1792 # surface_type = 'Wall'
1793 # else:
1794 # surface_type = 'Ceiling'
1795 # elif elem.ifc.is_a('IfcColumn'):
1796 # surface_type = 'Wall'
1797 elif inst_obj.top_bottom == BoundaryOrientation.bottom:
1798 surface_type = "Floor"
1799 elif inst_obj.top_bottom == BoundaryOrientation.top:
1800 surface_type = "Ceiling"
1801 if inst_obj.related_bound is None or inst_obj.is_external:
1802 surface_type = "Roof"
1803 elif inst_obj.top_bottom == BoundaryOrientation.vertical:
1804 surface_type = "Wall"
1805 else:
1806 if not PyOCCTools.compare_direction_of_normals(
1807 inst_obj.bound_normal, gp_XYZ(0, 0, 1)):
1808 surface_type = 'Wall'
1809 elif inst_obj.top_bottom == BoundaryOrientation.bottom:
1810 surface_type = "Floor"
1811 elif inst_obj.top_bottom == BoundaryOrientation.top:
1812 surface_type = "Ceiling"
1813 if inst_obj.related_bound is None or inst_obj.is_external:
1814 surface_type = "Roof"
1815 else:
1816 logger.warning(f"No surface type matched for {inst_obj}!")
1817 elif not inst_obj.physical:
1818 if not PyOCCTools.compare_direction_of_normals(
1819 inst_obj.bound_normal, gp_XYZ(0, 0, 1)):
1820 surface_type = 'Wall'
1821 else:
1822 if inst_obj.top_bottom == BoundaryOrientation.bottom:
1823 surface_type = "Floor"
1824 elif inst_obj.top_bottom == BoundaryOrientation.top:
1825 surface_type = "Ceiling"
1826 else:
1827 logger.warning(f"No surface type matched for {inst_obj}!")
1829 self.surface_type = surface_type
1831 def map_boundary_conditions(self, inst_obj: Union[SpaceBoundary,
1832 SpaceBoundary2B]):
1833 """Map boundary conditions.
1835 This function maps the boundary conditions of a SpaceBoundary instance
1836 to the idf space boundary conditions.
1838 Args:
1839 inst_obj: SpaceBoundary instance
1840 """
1841 if inst_obj.level_description == '2b' \
1842 or inst_obj.related_adb_bound is not None:
1843 self.out_bound_cond = 'Adiabatic'
1844 self.sun_exposed = 'NoSun'
1845 self.wind_exposed = 'NoWind'
1846 elif (hasattr(inst_obj.ifc, 'CorrespondingBoundary')
1847 and ((inst_obj.ifc.CorrespondingBoundary is not None) and (
1848 inst_obj.ifc.CorrespondingBoundary.InternalOrExternalBoundary.upper()
1849 == 'EXTERNAL_EARTH'))
1850 and (self.key == "BUILDINGSURFACE:DETAILED")
1851 and not (len(inst_obj.opening_bounds) > 0)):
1852 self.out_bound_cond = "Ground"
1853 self.sun_exposed = 'NoSun'
1854 self.wind_exposed = 'NoWind'
1855 elif inst_obj.is_external and inst_obj.physical \
1856 and not self.surface_type == 'Floor':
1857 self.out_bound_cond = 'Outdoors'
1858 self.sun_exposed = 'SunExposed'
1859 self.wind_exposed = 'WindExposed'
1860 self.out_bound_cond_obj = ''
1861 elif self.surface_type == "Floor" and \
1862 (inst_obj.related_bound is None
1863 or inst_obj.related_bound.ifc.RelatingSpace.is_a(
1864 'IfcExternalSpatialElement')):
1865 self.out_bound_cond = "Ground"
1866 self.sun_exposed = 'NoSun'
1867 self.wind_exposed = 'NoWind'
1868 elif inst_obj.related_bound is not None \
1869 and not inst_obj.related_bound.ifc.RelatingSpace.is_a(
1870 'IfcExternalSpatialElement'):
1871 self.out_bound_cond = 'Surface'
1872 self.out_bound_cond_obj = inst_obj.related_bound.guid
1873 self.sun_exposed = 'NoSun'
1874 self.wind_exposed = 'NoWind'
1875 elif self.key == "FENESTRATIONSURFACE:DETAILED":
1876 self.out_bound_cond = 'Outdoors'
1877 self.sun_exposed = 'SunExposed'
1878 self.wind_exposed = 'WindExposed'
1879 self.out_bound_cond_obj = ''
1880 elif self.related_bound is None:
1881 self.out_bound_cond = 'Outdoors'
1882 self.sun_exposed = 'SunExposed'
1883 self.wind_exposed = 'WindExposed'
1884 self.out_bound_cond_obj = ''
1885 else:
1886 self.skip_bound = True
1888 @staticmethod
1889 def get_circular_shape(obj_pnts: list[tuple]) -> bool:
1890 """Check if a shape is circular.
1892 This function checks if a SpaceBoundary has a circular shape.
1894 Args:
1895 obj_pnts: SpaceBoundary vertices (list of coordinate tuples)
1896 Returns:
1897 True if shape is circular
1898 """
1899 circular_shape = False
1900 # compute if shape is circular:
1901 if len(obj_pnts) > 4:
1902 pnt = obj_pnts[0]
1903 pnt2 = obj_pnts[1]
1904 distance_prev = pnt.Distance(pnt2)
1905 pnt = pnt2
1906 for pnt2 in obj_pnts[2:]:
1907 distance = pnt.Distance(pnt2)
1908 if (distance_prev - distance) ** 2 < 0.01:
1909 circular_shape = True
1910 pnt = pnt2
1911 distance_prev = distance
1912 else:
1913 continue
1914 return circular_shape
1916 @staticmethod
1917 def process_circular_shapes(idf: IDF, obj_coords: list[tuple], obj,
1918 inst_obj: Union[SpaceBoundary, SpaceBoundary2B]
1919 ):
1920 """Simplify circular space boundaries.
1922 This function processes circular boundary shapes. It converts circular
1923 shapes to triangular shapes.
1925 Args:
1926 idf: idf file object
1927 obj_coords: coordinates of an idf object
1928 obj: idf object
1929 inst_obj: SpaceBoundary instance
1930 """
1931 drop_count = int(len(obj_coords) / 8)
1932 drop_list = obj_coords[0::drop_count]
1933 pnt = drop_list[0]
1934 counter = 0
1935 # del inst_obj.__dict__['bound_center']
1936 for pnt2 in drop_list[1:]:
1937 counter += 1
1938 new_obj = idf.copyidfobject(obj)
1939 new_obj.Name = str(obj.Name) + '_' + str(counter)
1940 fc = PyOCCTools.make_faces_from_pnts(
1941 [pnt, pnt2, inst_obj.bound_center.Coord()])
1942 fcsc = PyOCCTools.scale_face(fc, 0.99)
1943 new_pnts = PyOCCTools.get_points_of_face(fcsc)
1944 new_coords = []
1945 for pnt in new_pnts:
1946 new_coords.append(pnt.Coord())
1947 new_obj.setcoords(new_coords)
1948 pnt = pnt2
1949 new_obj = idf.copyidfobject(obj)
1950 new_obj.Name = str(obj.Name) + '_' + str(counter + 1)
1951 fc = PyOCCTools.make_faces_from_pnts(
1952 [drop_list[-1], drop_list[0], inst_obj.bound_center.Coord()])
1953 fcsc = PyOCCTools.scale_face(fc, 0.99)
1954 new_pnts = PyOCCTools.get_points_of_face(fcsc)
1955 new_coords = []
1956 for pnt in new_pnts:
1957 new_coords.append(pnt.Coord())
1958 new_obj.setcoords(new_coords)
1959 idf.removeidfobject(obj)
1961 @staticmethod
1962 def process_other_shapes(inst_obj: Union[SpaceBoundary, SpaceBoundary2B],
1963 obj):
1964 """Simplify non-circular shapes.
1966 This function processes non-circular shapes with too many vertices
1967 by approximation of the shape utilizing the UV-Bounds from OCC
1968 (more than 120 vertices for BUILDINGSURFACE:DETAILED
1969 and more than 4 vertices for FENESTRATIONSURFACE:DETAILED)
1971 Args:
1972 inst_obj: SpaceBoundary Instance
1973 obj: idf object
1974 """
1975 # print("TOO MANY EDGES")
1976 obj_pnts = []
1977 exp = TopExp_Explorer(inst_obj.bound_shape, TopAbs_FACE)
1978 face = topods_Face(exp.Current())
1979 umin, umax, vmin, vmax = breptools_UVBounds(face)
1980 surf = BRep_Tool.Surface(face)
1981 plane = Handle_Geom_Plane_DownCast(surf)
1982 plane = gp_Pln(plane.Location(), plane.Axis().Direction())
1983 new_face = BRepBuilderAPI_MakeFace(plane,
1984 umin,
1985 umax,
1986 vmin,
1987 vmax).Face().Reversed()
1988 face_exp = TopExp_Explorer(new_face, TopAbs_WIRE)
1989 w_exp = BRepTools_WireExplorer(topods_Wire(face_exp.Current()))
1990 while w_exp.More():
1991 wire_vert = w_exp.CurrentVertex()
1992 obj_pnts.append(BRep_Tool.Pnt(wire_vert))
1993 w_exp.Next()
1994 obj_coords = []
1995 for pnt in obj_pnts:
1996 obj_coords.append(pnt.Coord())
1997 obj.setcoords(obj_coords)