Coverage for bim2sim/plugins/PluginEnergyPlus/bim2sim_energyplus/task/ep_create_idf.py: 0%

784 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-05-16 08:28 +0000

1from __future__ import annotations 

2 

3import logging 

4import math 

5import os 

6from pathlib import Path, PosixPath 

7from typing import Union, TYPE_CHECKING 

8 

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 

16 

17from OCC.Core.gp import gp_Dir, gp_XYZ, gp_Pln 

18from geomeppy import IDF 

19 

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 

33 

34if TYPE_CHECKING: 

35 from bim2sim.plugins.PluginEnergyPlus.bim2sim_energyplus import \ 

36 EnergyPlusSimSettings 

37 

38logger = logging.getLogger(__name__) 

39 

40 

41class CreateIdf(ITask): 

42 """Create an EnergyPlus Input file. 

43 

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 """ 

48 

49 reads = ('elements', 'weather_file',) 

50 touches = ('idf', 'sim_results_path') 

51 

52 def __init__(self, playground): 

53 super().__init__(playground) 

54 self.idf = None 

55 

56 def run(self, elements: dict, weather_file: Path) -> tuple[IDF, Path]: 

57 """Execute all methods to export an IDF from BIM2SIM. 

58 

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. 

73 

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.") 

107 

108 return idf, sim_results_path 

109 

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. 

114 

115 Initialize the EnergyPlus input file (idf) with general idf settings 

116 and set default weather 

117 data. 

118 

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 

152 

153 def init_zone(self, sim_settings: EnergyPlusSimSettings, elements: dict, 

154 idf: IDF): 

155 """Initialize zone settings. 

156 

157 Creates one idf zone per space and sets heating and cooling 

158 templates, infiltration and internal loads (occupancy (people), 

159 equipment, lighting). 

160 

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) 

194 if sim_settings.building_rotation_overwrite != 0: 

195 idf.idfobjects['BUILDING'][0].North_Axis = ( 

196 sim_settings.building_rotation_overwrite) 

197 idf.idfobjects['GLOBALGEOMETRYRULES'][0].Coordinate_System =\ 

198 'Relative' 

199 

200 @staticmethod 

201 def init_zonelist( 

202 idf: IDF, 

203 name: str = None, 

204 zones_in_list: list[str] = None): 

205 """Initialize zone lists. 

206 

207 Inits a list of zones in the idf. If the zones_in_list is not set, 

208 all zones are assigned to a general zone, unless the number of total 

209 zones is greater than 20 (max. allowed number of zones in a zonelist 

210 in an idf). 

211 

212 Args: 

213 idf: idf file object 

214 name: str with name of zone list 

215 zones_in_list: list with the guids of the zones to be included in 

216 the list 

217 """ 

218 if zones_in_list is None: 

219 # assign all zones to one list unless the total number of zones 

220 # is larger than 20. 

221 idf_zones = idf.idfobjects["ZONE"] 

222 if len(idf_zones) > 20: 

223 return 

224 else: 

225 # assign all zones with the zone names that are included in 

226 # zones_in_list to the zonelist. 

227 all_idf_zones = idf.idfobjects["ZONE"] 

228 idf_zones = [zone for zone in all_idf_zones if zone.Name 

229 in zones_in_list] 

230 if len(idf_zones) > 20: 

231 return 

232 if len(idf_zones) == 0: 

233 return 

234 if name is None: 

235 name = "All_Zones" 

236 zs = {} 

237 for i, z in enumerate(idf_zones): 

238 zs.update({"Zone_" + str(i + 1) + "_Name": z.Name}) 

239 idf.newidfobject("ZONELIST", Name=name, **zs) 

240 

241 def init_zonegroups(self, elements: dict, idf: IDF): 

242 """Assign one zonegroup per storey. 

243 

244 Args: 

245 elements: dict[guid: element] 

246 idf: idf file object 

247 """ 

248 spaces = get_spaces_with_bounds(elements) 

249 # assign storeys to spaces (ThermalZone) 

250 for space in spaces: 

251 if space.storeys: 

252 space.storey = space.storeys[0] # Zone can only have one storey 

253 else: 

254 space.storey = None 

255 # add zonelist per storey 

256 storeys = filter_elements(elements, Storey) 

257 for st in storeys: 

258 space_ids = [] 

259 for space in st.thermal_zones: 

260 if not space in spaces: 

261 continue 

262 space_ids.append(space.guid) 

263 self.init_zonelist(idf, name=st.ifc.Name, zones_in_list=space_ids) 

264 

265 # add zonelist for All_Zones 

266 zone_lists = [zlist for zlist in idf.idfobjects["ZONELIST"] 

267 if zlist.Name != "All_Zones"] 

268 

269 # add zonegroup for each zonegroup in zone_lists. 

270 for zlist in zone_lists: 

271 idf.newidfobject("ZONEGROUP", 

272 Name=zlist.Name, 

273 Zone_List_Name=zlist.Name, 

274 Zone_List_Multiplier=1 

275 ) 

276 @staticmethod 

277 def check_preprocessed_materials_and_constructions(rel_elem, layers): 

278 """Check if preprocessed materials and constructions are valid.""" 

279 correct_preprocessing = False 

280 # check if thickness and material parameters are available from 

281 # preprocessing 

282 if all(layer.thickness for layer in layers): 

283 for layer in rel_elem.layerset.layers: 

284 if None in (layer.material.thermal_conduc, 

285 layer.material.spec_heat_capacity, 

286 layer.material.density): 

287 return correct_preprocessing 

288 elif 0 in (layer.material.thermal_conduc.m, 

289 layer.material.spec_heat_capacity.m, 

290 layer.material.density.m): 

291 return correct_preprocessing 

292 else: 

293 pass 

294 

295 correct_preprocessing = True 

296 

297 return correct_preprocessing 

298 

299 def get_preprocessed_materials_and_constructions( 

300 self, sim_settings: EnergyPlusSimSettings, elements: dict, idf: IDF): 

301 """Get preprocessed materials and constructions. 

302 

303 This function sets preprocessed construction and material for 

304 building surfaces and fenestration. For virtual bounds, an air 

305 boundary construction is set. 

306 

307 Args: 

308 sim_settings: BIM2SIM simulation settings 

309 elements: dict[guid: element] 

310 idf: idf file object 

311 """ 

312 logger.info("Get predefined materials and construction ...") 

313 bounds = filter_elements(elements, 'SpaceBoundary') 

314 for bound in bounds: 

315 rel_elem = bound.bound_element 

316 if not rel_elem: 

317 continue 

318 if not any([isinstance(rel_elem, window) for window in 

319 all_subclasses(Window, include_self=True)]): 

320 # set construction for all but fenestration 

321 if self.check_preprocessed_materials_and_constructions( 

322 rel_elem, rel_elem.layerset.layers): 

323 self.set_preprocessed_construction_elem( 

324 rel_elem, rel_elem.layerset.layers, idf) 

325 for layer in rel_elem.layerset.layers: 

326 self.set_preprocessed_material_elem(layer, idf) 

327 else: 

328 logger.warning("No preprocessed construction and " 

329 "material found for space boundary %s on " 

330 "related building element %s. Using " 

331 "default values instead.", 

332 bound.guid, rel_elem.guid) 

333 else: 

334 # set construction elements for windows 

335 self.set_preprocessed_window_material_elem( 

336 rel_elem, idf, sim_settings.add_window_shading) 

337 

338 # Add air boundaries as construction as a material for virtual bounds 

339 if sim_settings.ep_version in ["9-2-0", "9-4-0"]: 

340 idf.newidfobject("CONSTRUCTION:AIRBOUNDARY", 

341 Name='Air Wall', 

342 Solar_and_Daylighting_Method='GroupedZones', 

343 Radiant_Exchange_Method='GroupedZones', 

344 Air_Exchange_Method='SimpleMixing', 

345 Simple_Mixing_Air_Changes_per_Hour=0.5, 

346 ) 

347 else: 

348 idf.newidfobject("CONSTRUCTION:AIRBOUNDARY", 

349 Name='Air Wall', 

350 Air_Exchange_Method='SimpleMixing', 

351 Simple_Mixing_Air_Changes_per_Hour=0.5, 

352 ) 

353 

354 @staticmethod 

355 def set_preprocessed_construction_elem( 

356 rel_elem: IFCBased, 

357 layers: list[Layer], 

358 idf: IDF): 

359 """Write preprocessed constructions to idf. 

360 

361 This function uses preprocessed data to define idf construction 

362 elements. 

363 

364 Args: 

365 rel_elem: any subclass of IFCBased (e.g., Wall) 

366 layers: list of Layer 

367 idf: idf file object 

368 """ 

369 construction_name = (rel_elem.key.replace('Disaggregated', '') + '_' 

370 + str(len(layers)) + '_' + '_' \ 

371 .join([str(l.thickness.to(ureg.metre).m) for l in layers])) 

372 # todo: find a unique key for construction name 

373 if idf.getobject("CONSTRUCTION", construction_name) is None: 

374 outer_layer = layers[-1] 

375 other_layer_list = layers[:-1] 

376 other_layer_list.reverse() 

377 other_layers = {} 

378 for i, l in enumerate(other_layer_list): 

379 other_layers.update( 

380 {'Layer_' + str(i + 2): l.material.name + "_" + str( 

381 l.thickness.to(ureg.metre).m)}) 

382 idf.newidfobject("CONSTRUCTION", 

383 Name=construction_name, 

384 Outside_Layer=outer_layer.material.name + "_" + 

385 str(outer_layer.thickness.to( 

386 ureg.metre).m), 

387 **other_layers 

388 ) 

389 

390 @staticmethod 

391 def set_preprocessed_material_elem(layer: Layer, idf: IDF): 

392 """Set a preprocessed material element. 

393 

394 Args: 

395 layer: Layer Instance 

396 idf: idf file object 

397 """ 

398 material_name = layer.material.name + "_" + str( 

399 layer.thickness.to(ureg.metre).m) 

400 if idf.getobject("MATERIAL", material_name): 

401 return 

402 specific_heat = \ 

403 layer.material.spec_heat_capacity.to(ureg.joule / ureg.kelvin / 

404 ureg.kilogram).m 

405 if specific_heat < 100: 

406 specific_heat = 100 

407 conductivity = layer.material.thermal_conduc.to( 

408 ureg.W / (ureg.m * ureg.K)).m 

409 density = layer.material.density.to(ureg.kg / ureg.m ** 3).m 

410 if conductivity == 0: 

411 logger.error(f"Conductivity of {layer.material} is 0. Simulation " 

412 f"will crash, please correct input or resulting idf " 

413 f"file.") 

414 if density == 0: 

415 logger.error(f"Density of {layer.material} is 0. Simulation " 

416 f"will crash, please correct input or resulting idf " 

417 f"file.") 

418 idf.newidfobject("MATERIAL", 

419 Name=material_name, 

420 Roughness="MediumRough", 

421 Thickness=layer.thickness.to(ureg.metre).m, 

422 Conductivity=conductivity, 

423 Density=density, 

424 Specific_Heat=specific_heat 

425 ) 

426 

427 @staticmethod 

428 def set_preprocessed_window_material_elem(rel_elem: Window, 

429 idf: IDF, 

430 add_window_shading: False): 

431 """Set preprocessed window material. 

432 

433 This function constructs windows with a 

434 WindowMaterial:SimpleGlazingSystem consisting of the outermost layer 

435 of the providing related element. This is a simplification, needs to 

436 be extended to hold multilayer window constructions. 

437 

438 Args: 

439 rel_elem: Window instance 

440 idf: idf file object 

441 add_window_shading: Add window shading (options: 'None', 

442 'Interior', 'Exterior') 

443 """ 

444 material_name = \ 

445 'WM_' + rel_elem.layerset.layers[0].material.name + '_' \ 

446 + str(rel_elem.layerset.layers[0].thickness.to(ureg.m).m) 

447 if idf.getobject("WINDOWMATERIAL:SIMPLEGLAZINGSYSTEM", material_name): 

448 return 

449 if rel_elem.u_value.to(ureg.W / ureg.K / ureg.meter ** 2).m > 0: 

450 ufactor = 1 / (0.04 + 1 / rel_elem.u_value.to(ureg.W / ureg.K / 

451 ureg.meter ** 2).m 

452 + 0.13) 

453 else: 

454 ufactor = 1 / (0.04 + rel_elem.layerset.layers[0].thickness.to( 

455 ureg.metre).m / 

456 rel_elem.layerset.layers[ 

457 0].material.thermal_conduc.to( 

458 ureg.W / (ureg.m * ureg.K)).m + 

459 0.13) 

460 if rel_elem.g_value >= 1: 

461 old_g_value = rel_elem.g_value 

462 rel_elem.g_value = 0.999 

463 logger.warning("G-Value was set to %f, " 

464 "but has to be smaller than 1, so overwritten by %f", 

465 old_g_value, rel_elem.g_value) 

466 

467 idf.newidfobject("WINDOWMATERIAL:SIMPLEGLAZINGSYSTEM", 

468 Name=material_name, 

469 UFactor=ufactor, 

470 Solar_Heat_Gain_Coefficient=rel_elem.g_value, 

471 # Visible_Transmittance=0.8 # optional 

472 ) 

473 if add_window_shading: 

474 default_shading_name = "DefaultWindowShade" 

475 if not idf.getobject("WINDOWMATERIAL:SHADE", default_shading_name): 

476 idf.newidfobject("WINDOWMATERIAL:SHADE", 

477 Name=default_shading_name, 

478 Solar_Transmittance=0.3, 

479 Solar_Reflectance=0.5, 

480 Visible_Transmittance=0.3, 

481 Visible_Reflectance=0.5, 

482 Infrared_Hemispherical_Emissivity=0.9, 

483 Infrared_Transmittance=0.05, 

484 Thickness=0.003, 

485 Conductivity=0.1) 

486 construction_name = 'Window_' + material_name + "_" \ 

487 + add_window_shading 

488 if idf.getobject("CONSTRUCTION", construction_name) is None: 

489 if add_window_shading == 'Interior': 

490 idf.newidfobject("CONSTRUCTION", 

491 Name=construction_name, 

492 Outside_Layer=material_name, 

493 Layer_2=default_shading_name 

494 ) 

495 else: 

496 idf.newidfobject("CONSTRUCTION", 

497 Name=construction_name, 

498 Outside_Layer=default_shading_name, 

499 Layer_2=material_name 

500 ) 

501 # todo: enable use of multilayer windows 

502 # set construction without shading anyways 

503 construction_name = 'Window_' + material_name 

504 if idf.getobject("CONSTRUCTION", construction_name) is None: 

505 idf.newidfobject("CONSTRUCTION", 

506 Name=construction_name, 

507 Outside_Layer=material_name 

508 ) 

509 

510 def set_heating_and_cooling(self, idf: IDF, zone_name: str, 

511 space: ThermalZone): 

512 """Set heating and cooling parameters. 

513 

514 This function sets heating and cooling parameters based on the data 

515 available from BIM2SIM Preprocessing (either IFC-based or 

516 Template-based). 

517 

518 Args: 

519 idf: idf file object 

520 zone_name: str 

521 space: ThermalZone instance 

522 """ 

523 stat_name = "STATS " + space.usage.replace(',', '') 

524 if idf.getobject("HVACTEMPLATE:THERMOSTAT", stat_name) is None: 

525 stat = self.set_day_hvac_template(idf, space, stat_name) 

526 else: 

527 stat = idf.getobject("HVACTEMPLATE:THERMOSTAT", stat_name) 

528 

529 cooling_availability = "Off" 

530 heating_availability = "Off" 

531 

532 if space.with_cooling: 

533 cooling_availability = "On" 

534 if space.with_heating: 

535 heating_availability = "On" 

536 

537 idf.newidfobject( 

538 "HVACTEMPLATE:ZONE:IDEALLOADSAIRSYSTEM", 

539 Zone_Name=zone_name, 

540 Template_Thermostat_Name=stat.Name, 

541 Heating_Availability_Schedule_Name=heating_availability, 

542 Cooling_Availability_Schedule_Name=cooling_availability 

543 ) 

544 

545 def set_people(self, sim_settings: EnergyPlusSimSettings, idf: IDF, name: str, 

546 zone_name: str, space: ThermalZone): 

547 """Set occupancy schedules. 

548 

549 This function sets schedules and internal loads from people (occupancy) 

550 based on the BIM2SIM Preprocessing, i.e. based on IFC data if 

551 available or on templates. 

552 

553 Args: 

554 sim_settings: BIM2SIM simulation settings 

555 idf: idf file object 

556 name: name of the new people idf object 

557 zone_name: name of zone or zone_list 

558 space: ThermalZone instance 

559 """ 

560 schedule_name = "Schedule " + "People " + space.usage.replace(',', '') 

561 profile_name = 'persons_profile' 

562 self.set_day_week_year_schedule(idf, space.persons_profile[:24], 

563 profile_name, schedule_name) 

564 # set default activity schedule 

565 if idf.getobject("SCHEDULETYPELIMITS", "Any Number") is None: 

566 idf.newidfobject("SCHEDULETYPELIMITS", Name="Any Number") 

567 activity_schedule_name = "Schedule Activity " + str( 

568 space.fixed_heat_flow_rate_persons) 

569 if idf.getobject("SCHEDULE:COMPACT", activity_schedule_name) is None: 

570 idf.newidfobject("SCHEDULE:COMPACT", 

571 Name=activity_schedule_name, 

572 Schedule_Type_Limits_Name="Any Number", 

573 Field_1="Through: 12/31", 

574 Field_2="For: Alldays", 

575 Field_3="Until: 24:00", 

576 Field_4=space.fixed_heat_flow_rate_persons.to( 

577 ureg.watt).m#*1.8 # in W/Person 

578 ) # other method for Field_4 (not used here) 

579 # ="persons_profile"*"activity_degree_persons"*58,1*1,8 

580 # (58.1 W/(m2*met), 1.8m2/Person) 

581 if sim_settings.ep_version in ["9-2-0", "9-4-0"]: 

582 idf.newidfobject( 

583 "PEOPLE", 

584 Name=name, 

585 Zone_or_ZoneList_Name=zone_name, 

586 Number_of_People_Calculation_Method="People/Area", 

587 People_per_Zone_Floor_Area=space.persons, 

588 Activity_Level_Schedule_Name=activity_schedule_name, 

589 Number_of_People_Schedule_Name=schedule_name, 

590 Fraction_Radiant=space.ratio_conv_rad_persons 

591 ) 

592 else: 

593 idf.newidfobject( 

594 "PEOPLE", 

595 Name=name, 

596 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name, 

597 Number_of_People_Calculation_Method="People/Area", 

598 People_per_Floor_Area=space.persons, 

599 Activity_Level_Schedule_Name=activity_schedule_name, 

600 Number_of_People_Schedule_Name=schedule_name, 

601 Fraction_Radiant=space.ratio_conv_rad_persons 

602 ) 

603 

604 @staticmethod 

605 def set_day_week_year_schedule(idf: IDF, schedule: list[float], 

606 profile_name: str, 

607 schedule_name: str): 

608 """Set day, week and year schedule (hourly). 

609 

610 This function sets an hourly day, week and year schedule. 

611 

612 Args: 

613 idf: idf file object 

614 schedule: list of float values for the schedule (e.g., 

615 temperatures, loads) 

616 profile_name: string 

617 schedule_name: str 

618 """ 

619 if idf.getobject("SCHEDULE:DAY:HOURLY", name=schedule_name) is None: 

620 limits_name = 'Fraction' 

621 hours = {} 

622 if profile_name in {'heating_profile', 'cooling_profile'}: 

623 limits_name = 'Temperature' 

624 if idf.getobject("SCHEDULETYPELIMITS", "Temperature") is None: 

625 idf.newidfobject("SCHEDULETYPELIMITS", Name="Temperature") 

626 for i, l in enumerate(schedule[:24]): 

627 if profile_name in {'heating_profile', 'cooling_profile'}: 

628 # convert Kelvin to Celsius for EnergyPlus Export 

629 if schedule[i] > 270: 

630 schedule[i] = schedule[i] - 273.15 

631 hours.update({'Hour_' + str(i + 1): schedule[i]}) 

632 idf.newidfobject("SCHEDULE:DAY:HOURLY", Name=schedule_name, 

633 Schedule_Type_Limits_Name=limits_name, **hours) 

634 if idf.getobject("SCHEDULE:WEEK:COMPACT", name=schedule_name) is None: 

635 idf.newidfobject("SCHEDULE:WEEK:COMPACT", Name=schedule_name, 

636 DayType_List_1="AllDays", 

637 ScheduleDay_Name_1=schedule_name) 

638 if idf.getobject("SCHEDULE:YEAR", name=schedule_name) is None: 

639 idf.newidfobject("SCHEDULE:YEAR", Name=schedule_name, 

640 Schedule_Type_Limits_Name=limits_name, 

641 ScheduleWeek_Name_1=schedule_name, 

642 Start_Month_1=1, 

643 Start_Day_1=1, 

644 End_Month_1=12, 

645 End_Day_1=31) 

646 

647 def set_equipment(self, sim_settings: EnergyPlusSimSettings, idf: IDF, 

648 name: str, zone_name: str, 

649 space: ThermalZone): 

650 """Set internal loads from equipment. 

651 

652 This function sets schedules and internal loads from equipment based 

653 on the BIM2SIM Preprocessing, i.e. based on IFC data if available or on 

654 templates. 

655 

656 Args: 

657 sim_settings: BIM2SIM simulation settings 

658 idf: idf file object 

659 name: name of the new people idf object 

660 zone_name: name of zone or zone_list 

661 space: ThermalZone instance 

662 """ 

663 schedule_name = "Schedule " + "Equipment " + space.usage.replace(',', 

664 '') 

665 profile_name = 'machines_profile' 

666 self.set_day_week_year_schedule(idf, space.machines_profile[:24], 

667 profile_name, schedule_name) 

668 if sim_settings.ep_version in ["9-2-0", "9-4-0"]: 

669 idf.newidfobject( 

670 "ELECTRICEQUIPMENT", 

671 Name=name, 

672 Zone_or_ZoneList_Name=zone_name, 

673 Schedule_Name=schedule_name, 

674 Design_Level_Calculation_Method="Watts/Area", 

675 Watts_per_Zone_Floor_Area=space.machines.to( 

676 ureg.watt / ureg.meter ** 2).m 

677 ) 

678 else: 

679 idf.newidfobject( 

680 "ELECTRICEQUIPMENT", 

681 Name=name, 

682 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name, 

683 Schedule_Name=schedule_name, 

684 Design_Level_Calculation_Method="Watts/Area", 

685 Watts_per_Zone_Floor_Area=space.machines.to( 

686 ureg.watt / ureg.meter ** 2).m 

687 ) 

688 

689 def set_lights(self, sim_settings: EnergyPlusSimSettings, idf: IDF, name: str, 

690 zone_name: str, space: ThermalZone): 

691 """Set internal loads from lighting. 

692 

693 This function sets schedules and lighting based on the 

694 BIM2SIM Preprocessing, i.e. based on IFC data if available or on 

695 templates. 

696 

697 Args: 

698 sim_settings: BIM2SIM simulation settings 

699 idf: idf file object 

700 name: name of the new people idf object 

701 zone_name: name of zone or zone_list 

702 space: ThermalZone instance 

703 """ 

704 schedule_name = "Schedule " + "Lighting " + space.usage.replace(',', '') 

705 profile_name = 'lighting_profile' 

706 self.set_day_week_year_schedule(idf, space.lighting_profile[:24], 

707 profile_name, schedule_name) 

708 mode = "Watts/Area" 

709 watts_per_zone_floor_area = space.lighting_power.to( 

710 ureg.watt / ureg.meter ** 2).m 

711 return_air_fraction = 0.0 

712 fraction_radiant = 0.42 # fraction radiant: cf. Table 1.28 in 

713 # InputOutputReference EnergyPlus (Version 9.4.0), p. 506 

714 fraction_visible = 0.18 # Todo: fractions do not match with .json 

715 # Data. Maybe set by user-input later 

716 if sim_settings.ep_version in ["9-2-0", "9-4-0"]: 

717 idf.newidfobject( 

718 "LIGHTS", 

719 Name=name, 

720 Zone_or_ZoneList_Name=zone_name, 

721 Schedule_Name=schedule_name, 

722 Design_Level_Calculation_Method=mode, 

723 Watts_per_Zone_Floor_Area=watts_per_zone_floor_area, 

724 Return_Air_Fraction=return_air_fraction, 

725 Fraction_Radiant=fraction_radiant, 

726 Fraction_Visible=fraction_visible 

727 ) 

728 else: 

729 idf.newidfobject( 

730 "LIGHTS", 

731 Name=name, 

732 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name, 

733 Schedule_Name=schedule_name, 

734 Design_Level_Calculation_Method=mode, 

735 Watts_per_Zone_Floor_Area=watts_per_zone_floor_area, 

736 Return_Air_Fraction=return_air_fraction, 

737 Fraction_Radiant=fraction_radiant, 

738 Fraction_Visible=fraction_visible 

739 ) 

740 

741 @staticmethod 

742 def set_infiltration(idf: IDF, 

743 name: str, zone_name: str, 

744 space: ThermalZone, ep_version: str): 

745 """Set infiltration rate. 

746 

747 This function sets the infiltration rate per space based on the 

748 BIM2SIM preprocessing values (IFC-based if available or 

749 template-based). 

750 

751 Args: 

752 idf: idf file object 

753 name: name of the new people idf object 

754 zone_name: name of zone or zone_list 

755 space: ThermalZone instance 

756 ep_version: Used version of EnergyPlus 

757 """ 

758 if ep_version in ["9-2-0", "9-4-0"]: 

759 idf.newidfobject( 

760 "ZONEINFILTRATION:DESIGNFLOWRATE", 

761 Name=name, 

762 Zone_or_ZoneList_Name=zone_name, 

763 Schedule_Name="Continuous", 

764 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

765 Air_Changes_per_Hour=space.base_infiltration 

766 ) 

767 else: 

768 idf.newidfobject( 

769 "ZONEINFILTRATION:DESIGNFLOWRATE", 

770 Name=name, 

771 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name, 

772 Schedule_Name="Continuous", 

773 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

774 Air_Changes_per_Hour=space.base_infiltration 

775 ) 

776 

777 @staticmethod 

778 def set_natural_ventilation(idf: IDF, name: str, zone_name: str, 

779 space: ThermalZone, ep_version): 

780 """Set natural ventilation. 

781 

782 This function sets the natural ventilation per space based on the 

783 BIM2SIM preprocessing values (IFC-based if available or 

784 template-based). Natural ventilation is defined for winter, summer 

785 and overheating cases, setting the air change per hours and minimum 

786 and maximum outdoor temperature if applicable. 

787 

788 Args: 

789 idf: idf file object 

790 name: name of the new people idf object 

791 zone_name: name of zone or zone_list 

792 space: ThermalZone instance 

793 ep_version: Used version of EnergyPlus 

794 

795 """ 

796 if ep_version in ["9-2-0", "9-4-0"]: 

797 idf.newidfobject( 

798 "ZONEVENTILATION:DESIGNFLOWRATE", 

799 Name=name + '_winter', 

800 Zone_or_ZoneList_Name=zone_name, 

801 Schedule_Name="Continuous", 

802 Ventilation_Type="Natural", 

803 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

804 Air_Changes_per_Hour=space.winter_reduction_infiltration[0], 

805 Minimum_Outdoor_Temperature= 

806 space.winter_reduction_infiltration[1] - 273.15, 

807 Maximum_Outdoor_Temperature= 

808 space.winter_reduction_infiltration[2] - 273.15, 

809 ) 

810 

811 idf.newidfobject( 

812 "ZONEVENTILATION:DESIGNFLOWRATE", 

813 Name=name + '_summer', 

814 Zone_or_ZoneList_Name=zone_name, 

815 Schedule_Name="Continuous", 

816 Ventilation_Type="Natural", 

817 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

818 Air_Changes_per_Hour=space.max_summer_infiltration[0], 

819 Minimum_Outdoor_Temperature 

820 =space.max_summer_infiltration[1] - 273.15, 

821 Maximum_Outdoor_Temperature 

822 =space.max_summer_infiltration[2] - 273.15, 

823 ) 

824 

825 idf.newidfobject( 

826 "ZONEVENTILATION:DESIGNFLOWRATE", 

827 Name=name + '_overheating', 

828 Zone_or_ZoneList_Name=zone_name, 

829 Schedule_Name="Continuous", 

830 Ventilation_Type="Natural", 

831 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

832 # calculation of overheating infiltration is a simplification 

833 # compared to the corresponding TEASER implementation which 

834 # dynamically computes thresholds for overheating infiltration 

835 # based on the zone temperature and additional factors. 

836 Air_Changes_per_Hour=space.max_overheating_infiltration[0], 

837 Minimum_Outdoor_Temperature 

838 =space.max_summer_infiltration[2] - 273.15, 

839 ) 

840 else: 

841 idf.newidfobject( 

842 "ZONEVENTILATION:DESIGNFLOWRATE", 

843 Name=name + '_winter', 

844 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name, 

845 Schedule_Name="Continuous", 

846 Ventilation_Type="Natural", 

847 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

848 Air_Changes_per_Hour=space.winter_reduction_infiltration[0], 

849 Minimum_Outdoor_Temperature= 

850 space.winter_reduction_infiltration[1] - 273.15, 

851 Maximum_Outdoor_Temperature= 

852 space.winter_reduction_infiltration[2] - 273.15, 

853 ) 

854 

855 idf.newidfobject( 

856 "ZONEVENTILATION:DESIGNFLOWRATE", 

857 Name=name + '_summer', 

858 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name, 

859 Schedule_Name="Continuous", 

860 Ventilation_Type="Natural", 

861 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

862 Air_Changes_per_Hour=space.max_summer_infiltration[0], 

863 Minimum_Outdoor_Temperature 

864 =space.max_summer_infiltration[1] - 273.15, 

865 Maximum_Outdoor_Temperature 

866 =space.max_summer_infiltration[2] - 273.15, 

867 ) 

868 

869 idf.newidfobject( 

870 "ZONEVENTILATION:DESIGNFLOWRATE", 

871 Name=name + '_overheating', 

872 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name, 

873 Schedule_Name="Continuous", 

874 Ventilation_Type="Natural", 

875 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

876 # calculation of overheating infiltration is a simplification 

877 # compared to the corresponding TEASER implementation which 

878 # dynamically computes thresholds for overheating infiltration 

879 # based on the zone temperature and additional factors. 

880 Air_Changes_per_Hour=space.max_overheating_infiltration[0], 

881 Minimum_Outdoor_Temperature 

882 =space.max_summer_infiltration[2] - 273.15, 

883 ) 

884 

885 def set_day_hvac_template(self, idf: IDF, space: ThermalZone, name: str): 

886 """Set 24 hour hvac template. 

887 

888 This function sets idf schedules with 24hour schedules for heating and 

889 cooling. 

890 

891 Args: 

892 idf: idf file object 

893 space: ThermalZone 

894 name: IDF Thermostat Name 

895 """ 

896 htg_schedule_name = "Schedule " + "Heating " + space.usage.replace( 

897 ',', '') 

898 self.set_day_week_year_schedule(idf, space.heating_profile[:24], 

899 'heating_profile', 

900 htg_schedule_name) 

901 

902 clg_schedule_name = "Schedule " + "Cooling " + space.usage.replace( 

903 ',', '') 

904 self.set_day_week_year_schedule(idf, space.cooling_profile[:24], 

905 'cooling_profile', 

906 clg_schedule_name) 

907 stat = idf.newidfobject( 

908 "HVACTEMPLATE:THERMOSTAT", 

909 Name=name, 

910 Heating_Setpoint_Schedule_Name=htg_schedule_name, 

911 Cooling_Setpoint_Schedule_Name=clg_schedule_name 

912 ) 

913 return stat 

914 

915 def set_hvac_template(self, idf: IDF, name: str, 

916 heating_sp: Union[int, float], 

917 cooling_sp: Union[int, float], 

918 mode='setback'): 

919 """Set heating and cooling templates (manually). 

920 

921 This function manually sets heating and cooling templates. 

922 

923 Args: 

924 idf: idf file object 

925 heating_sp: float or int for heating set point 

926 cooling_sp: float or int for cooling set point 

927 name: IDF Thermostat Name 

928 """ 

929 if cooling_sp < 20: 

930 cooling_sp = 26 

931 elif cooling_sp < 24: 

932 cooling_sp = 23 

933 

934 setback_htg = 18 # "T_threshold_heating" 

935 setback_clg = 26 # "T_threshold_cooling" 

936 

937 # ensure setback temperature actually performs a setback on temperature 

938 if setback_htg > heating_sp: 

939 setback_htg = heating_sp 

940 if setback_clg < cooling_sp: 

941 setback_clg = cooling_sp 

942 

943 if mode == "setback": 

944 htg_alldays = self._define_schedule_part('Alldays', 

945 [('5:00', setback_htg), 

946 ('21:00', heating_sp), 

947 ('24:00', setback_htg)]) 

948 clg_alldays = self._define_schedule_part('Alldays', 

949 [('5:00', setback_clg), 

950 ('21:00', cooling_sp), 

951 ('24:00', setback_clg)]) 

952 htg_name = "H_SetBack_" + str(heating_sp) 

953 clg_name = "C_SetBack_" + str(cooling_sp) 

954 if idf.getobject("SCHEDULE:COMPACT", htg_name) is None: 

955 self.write_schedule(idf, htg_name, [htg_alldays, ]) 

956 else: 

957 idf.getobject("SCHEDULE:COMPACT", htg_name) 

958 if idf.getobject("SCHEDULE:COMPACT", clg_name) is None: 

959 self.write_schedule(idf, clg_name, [clg_alldays, ]) 

960 else: 

961 idf.getobject("SCHEDULE:COMPACT", clg_name) 

962 stat = idf.newidfobject( 

963 "HVACTEMPLATE:THERMOSTAT", 

964 Name="STAT_" + name, 

965 Heating_Setpoint_Schedule_Name=htg_name, 

966 Cooling_Setpoint_Schedule_Name=clg_name, 

967 ) 

968 

969 if mode == "constant": 

970 stat = idf.newidfobject( 

971 "HVACTEMPLATE:THERMOSTAT", 

972 Name="STAT_" + name, 

973 Constant_Heating_Setpoint=heating_sp, 

974 Constant_Cooling_Setpoint=cooling_sp, 

975 ) 

976 return stat 

977 

978 @staticmethod 

979 def write_schedule(idf: IDF, sched_name: str, sched_part_list: list): 

980 """Write schedules to idf. 

981 

982 This function writes a schedule to the idf. Only used for manual 

983 setup of schedules (combined with set_hvac_template). 

984 

985 Args: 

986 idf: idf file object 

987 sched_name: str with name of the schedule 

988 sched_part_list: list of schedule parts (cf. function 

989 _define_schedule_part) 

990 """ 

991 sched_list = {} 

992 field_count = 1 

993 for parts in sched_part_list: 

994 field_count += 1 

995 sched_list.update({'Field_' + str(field_count): 'For: ' + parts[0]}) 

996 part = parts[1] 

997 for set in part: 

998 field_count += 1 

999 sched_list.update( 

1000 {'Field_' + str(field_count): 'Until: ' + str(set[0])}) 

1001 field_count += 1 

1002 sched_list.update({'Field_' + str(field_count): str(set[1])}) 

1003 if idf.getobject("SCHEDULETYPELIMITS", "Temperature") is None: 

1004 idf.newidfobject("SCHEDULETYPELIMITS", Name="Temperature") 

1005 

1006 sched = idf.newidfobject( 

1007 "SCHEDULE:COMPACT", 

1008 Name=sched_name, 

1009 Schedule_Type_Limits_Name="Temperature", 

1010 Field_1="Through: 12/31", 

1011 **sched_list 

1012 ) 

1013 return sched 

1014 

1015 @staticmethod 

1016 def _define_schedule_part( 

1017 days: str, til_time_temp: list[tuple[str, Union[int, float]]]): 

1018 """Defines a part of a schedule. 

1019 

1020 Args: 

1021 days: string: Weekdays, Weekends, Alldays, AllOtherDays, Saturdays, 

1022 Sundays, ... 

1023 til_time_temp: List of tuples 

1024 (until-time format 'hh:mm' (24h) as str), 

1025 temperature until this time in Celsius), 

1026 e.g. (05:00, 18) 

1027 """ 

1028 return [days, til_time_temp] 

1029 

1030 @staticmethod 

1031 def add_shadings(elements: dict, idf: IDF): 

1032 """Add shading boundaries to idf. 

1033 

1034 Args: 

1035 elements: dict[guid: element] 

1036 idf: idf file object 

1037 """ 

1038 logger.info("Add Shadings ...") 

1039 spatials = [] 

1040 ext_spatial_elem = filter_elements(elements, ExternalSpatialElement) 

1041 for elem in ext_spatial_elem: 

1042 for sb in elem.space_boundaries: 

1043 spatials.append(sb) 

1044 if not spatials: 

1045 return 

1046 pure_spatials = [] 

1047 description_list = [s.ifc.Description for s in spatials] 

1048 descriptions = list(dict.fromkeys(description_list)) 

1049 shades_included = ("Shading:Building" or "Shading:Site") in descriptions 

1050 

1051 # check if ifc has dedicated shading space boundaries included and 

1052 # append them to pure_spatials for further processing 

1053 if shades_included: 

1054 for s in spatials: 

1055 if s.ifc.Description in ["Shading:Building", "Shading:Site"]: 

1056 pure_spatials.append(s) 

1057 # if no shading boundaries are included in ifc, derive these from the 

1058 # set of given space boundaries and append them to pure_spatials for 

1059 # further processing 

1060 else: 

1061 for s in spatials: 

1062 # only consider almost horizontal 2b shapes (roof-like SBs) 

1063 if s.level_description == '2b': 

1064 angle = math.degrees( 

1065 gp_Dir(s.bound_normal).Angle(gp_Dir(gp_XYZ(0, 0, 1)))) 

1066 if not ((-45 < angle < 45) or (135 < angle < 225)): 

1067 continue 

1068 if s.related_bound and s.related_bound.ifc.RelatingSpace.is_a( 

1069 'IfcSpace'): 

1070 continue 

1071 pure_spatials.append(s) 

1072 

1073 # create idf shadings from set of pure_spatials 

1074 for s in pure_spatials: 

1075 obj = idf.newidfobject('SHADING:BUILDING:DETAILED', 

1076 Name=s.guid, 

1077 ) 

1078 obj_pnts = PyOCCTools.get_points_of_face(s.bound_shape) 

1079 obj_coords = [] 

1080 for pnt in obj_pnts: 

1081 co = tuple(round(p, 3) for p in pnt.Coord()) 

1082 obj_coords.append(co) 

1083 obj.setcoords(obj_coords) 

1084 

1085 def add_shading_control(self, shading_type, elements, 

1086 idf, outdoor_temp=22, solar=40): 

1087 """Add a default shading control to IDF. 

1088 Two criteria must be met such that the window shades are set: the 

1089 outdoor temperature must exceed a certain temperature and the solar 

1090 radiation [W/m²] must be greater than a certain heat flow. 

1091 Args: 

1092 shading_type: shading type, 'Interior' or 'Exterior' 

1093 elements: elements 

1094 idf: idf 

1095 outdoor_temp: outdoor temperature [°C] 

1096 solar: solar radiation on window surface [W/m²] 

1097 """ 

1098 zones = filter_elements(elements, ThermalZone) 

1099 

1100 for zone in zones: 

1101 zone_name = zone.guid 

1102 zone_openings = [sb for sb in zone.space_boundaries if 

1103 isinstance(sb.bound_element, Window)] 

1104 if not zone_openings: 

1105 continue 

1106 fenestration_dict = {} 

1107 for i, opening in enumerate(zone_openings): 

1108 fenestration_dict.update({'Fenestration_Surface_' + str( 

1109 i+1) + '_Name': opening.guid}) 

1110 shade_control_name = "ShadeControl_" + zone_name 

1111 opening_obj = idf.getobject( 

1112 'FENESTRATIONSURFACE:DETAILED', zone_openings[ 

1113 0].guid) 

1114 if opening_obj: 

1115 construction_name = opening_obj.Construction_Name + "_" + \ 

1116 shading_type 

1117 else: 

1118 continue 

1119 if not idf.getobject( 

1120 "WINDOWSHADINGCONTROL", shade_control_name): 

1121 idf.newidfobject("WINDOWSHADINGCONTROL", 

1122 Name=shade_control_name, 

1123 Zone_Name=zone_name, 

1124 Shading_Type=shading_type+"Shade", 

1125 Construction_with_Shading_Name=construction_name, 

1126 Shading_Control_Type= 

1127 'OnIfHighOutdoorAirTempAndHighSolarOnWindow', 

1128 Setpoint=outdoor_temp, 

1129 Setpoint_2=solar, 

1130 Multiple_Surface_Control_Type='Group', 

1131 **fenestration_dict 

1132 ) 

1133 

1134 @staticmethod 

1135 def set_simulation_control(sim_settings: EnergyPlusSimSettings, idf): 

1136 """Set simulation control parameters. 

1137 

1138 This function sets general simulation control parameters. These can 

1139 be easily overwritten in the exported idf. 

1140 Args: 

1141 sim_settings: EnergyPlusSimSettings 

1142 idf: idf file object 

1143 """ 

1144 logger.info("Set Simulation Control ...") 

1145 for sim_control in idf.idfobjects["SIMULATIONCONTROL"]: 

1146 if sim_settings.system_sizing: 

1147 sim_control.Do_System_Sizing_Calculation = 'Yes' 

1148 else: 

1149 sim_control.Do_System_Sizing_Calculation = 'No' 

1150 if sim_settings.run_for_sizing_periods: 

1151 sim_control.Run_Simulation_for_Sizing_Periods = 'Yes' 

1152 else: 

1153 sim_control.Run_Simulation_for_Sizing_Periods = 'No' 

1154 if sim_settings.run_for_weather_period: 

1155 sim_control.Run_Simulation_for_Weather_File_Run_Periods = 'Yes' 

1156 else: 

1157 sim_control.Run_Simulation_for_Weather_File_Run_Periods = 'No' 

1158 if sim_settings.set_run_period: 

1159 sim_control.Run_Simulation_for_Weather_File_Run_Periods = 'Yes' 

1160 

1161 if sim_settings.set_run_period: 

1162 for run_period in idf.idfobjects["RUNPERIOD"]: 

1163 run_period.Begin_Month = sim_settings.run_period_start_month 

1164 run_period.Begin_Day_of_Month = ( 

1165 sim_settings.run_period_start_day) 

1166 run_period.End_Month = sim_settings.run_period_end_month 

1167 run_period.End_Day_of_Month = sim_settings.run_period_end_day 

1168 

1169 for building in idf.idfobjects['BUILDING']: 

1170 building.Solar_Distribution = sim_settings.solar_distribution 

1171 

1172 @staticmethod 

1173 def set_ground_temperature(idf: IDF, t_ground: ureg.Quantity): 

1174 """Set the ground temperature in the idf. 

1175 

1176 Args: 

1177 idf: idf file object 

1178 t_ground: ground temperature as ureg.Quantity 

1179 """ 

1180 logger.info("Set ground temperature...") 

1181 

1182 string = '_Ground_Temperature' 

1183 month_list = ['January', 'February', 'March', 'April', 'May', 'June', 

1184 'July', 'August', 'September', 'October', 

1185 'November', 'December'] 

1186 temp_dict = {} 

1187 for month in month_list: 

1188 temp_dict.update({month + string: t_ground.to(ureg.degC).m}) 

1189 idf.newidfobject("SITE:GROUNDTEMPERATURE:BUILDINGSURFACE", **temp_dict) 

1190 

1191 @staticmethod 

1192 def set_output_variables(idf: IDF, sim_settings: EnergyPlusSimSettings): 

1193 """Set user defined output variables in the idf file. 

1194 

1195 Args: 

1196 idf: idf file object 

1197 sim_settings: BIM2SIM simulation settings 

1198 """ 

1199 logger.info("Set output variables ...") 

1200 

1201 # general output settings. May be moved to general settings 

1202 out_control = idf.idfobjects['OUTPUTCONTROL:TABLE:STYLE'] 

1203 out_control[0].Column_Separator = sim_settings.output_format 

1204 out_control[0].Unit_Conversion = sim_settings.unit_conversion 

1205 

1206 # remove all existing output variables with reporting frequency 

1207 # "Timestep" 

1208 out_var = [v for v in idf.idfobjects['OUTPUT:VARIABLE'] 

1209 if v.Reporting_Frequency.upper() == "TIMESTEP"] 

1210 for var in out_var: 

1211 idf.removeidfobject(var) 

1212 if 'output_outdoor_conditions' in sim_settings.output_keys: 

1213 idf.newidfobject( 

1214 "OUTPUT:VARIABLE", 

1215 Variable_Name="Site Outdoor Air Drybulb Temperature", 

1216 Reporting_Frequency="Hourly", 

1217 ) 

1218 idf.newidfobject( 

1219 "OUTPUT:VARIABLE", 

1220 Variable_Name="Site Outdoor Air Humidity Ratio", 

1221 Reporting_Frequency="Hourly", 

1222 ) 

1223 idf.newidfobject( 

1224 "OUTPUT:VARIABLE", 

1225 Variable_Name="Site Outdoor Air Relative Humidity", 

1226 Reporting_Frequency="Hourly", 

1227 ) 

1228 idf.newidfobject( 

1229 "OUTPUT:VARIABLE", 

1230 Variable_Name="Site Outdoor Air Barometric Pressure", 

1231 Reporting_Frequency="Hourly", 

1232 ) 

1233 idf.newidfobject( 

1234 "OUTPUT:VARIABLE", 

1235 Variable_Name="Site Diffuse Solar Radiation Rate per Area", 

1236 Reporting_Frequency="Hourly", 

1237 ) 

1238 idf.newidfobject( 

1239 "OUTPUT:VARIABLE", 

1240 Variable_Name="Site Direct Solar Radiation Rate per Area", 

1241 Reporting_Frequency="Hourly", 

1242 ) 

1243 idf.newidfobject( 

1244 "OUTPUT:VARIABLE", 

1245 Variable_Name="Site Ground Temperature", 

1246 Reporting_Frequency="Hourly", 

1247 ) 

1248 idf.newidfobject( 

1249 "OUTPUT:VARIABLE", 

1250 Variable_Name="Site Wind Speed", 

1251 Reporting_Frequency="Hourly", 

1252 ) 

1253 idf.newidfobject( 

1254 "OUTPUT:VARIABLE", 

1255 Variable_Name="Site Wind Direction", 

1256 Reporting_Frequency="Hourly", 

1257 ) 

1258 if 'output_zone_temperature' in sim_settings.output_keys: 

1259 idf.newidfobject( 

1260 "OUTPUT:VARIABLE", 

1261 Variable_Name="Zone Mean Air Temperature", 

1262 Reporting_Frequency="Hourly", 

1263 ) 

1264 idf.newidfobject( 

1265 "OUTPUT:VARIABLE", 

1266 Variable_Name="Zone Operative Temperature", 

1267 Reporting_Frequency="Hourly", 

1268 ) 

1269 idf.newidfobject( 

1270 "OUTPUT:VARIABLE", 

1271 Variable_Name="Zone Air Relative Humidity", 

1272 Reporting_Frequency="Hourly", 

1273 ) 

1274 if 'output_internal_gains' in sim_settings.output_keys: 

1275 idf.newidfobject( 

1276 "OUTPUT:VARIABLE", 

1277 Variable_Name="Zone People Occupant Count", 

1278 Reporting_Frequency="Hourly", 

1279 ) 

1280 idf.newidfobject( 

1281 "OUTPUT:VARIABLE", 

1282 Variable_Name="Zone People Total Heating Rate", 

1283 Reporting_Frequency="Hourly", 

1284 ) 

1285 idf.newidfobject( 

1286 "OUTPUT:VARIABLE", 

1287 Variable_Name="Zone Electric Equipment Total Heating Rate", 

1288 Reporting_Frequency="Hourly", 

1289 ) 

1290 idf.newidfobject( 

1291 "OUTPUT:VARIABLE", 

1292 Variable_Name="Zone Lights Total Heating Rate", 

1293 Reporting_Frequency="Hourly", 

1294 ) 

1295 if 'output_zone' in sim_settings.output_keys: 

1296 idf.newidfobject( 

1297 "OUTPUT:VARIABLE", 

1298 Variable_Name="Zone Thermostat Heating Setpoint Temperature", 

1299 Reporting_Frequency="Hourly", 

1300 ) 

1301 idf.newidfobject( 

1302 "OUTPUT:VARIABLE", 

1303 Variable_Name="Zone Thermostat Cooling Setpoint Temperature", 

1304 Reporting_Frequency="Hourly", 

1305 ) 

1306 idf.newidfobject( 

1307 "OUTPUT:VARIABLE", 

1308 Variable_Name="Zone Ideal Loads Zone Total Cooling Rate", 

1309 Reporting_Frequency="Hourly", 

1310 ) 

1311 idf.newidfobject( 

1312 "OUTPUT:VARIABLE", 

1313 Variable_Name="Zone Ideal Loads Zone Total Heating Rate", 

1314 Reporting_Frequency="Hourly", 

1315 ) 

1316 idf.newidfobject( 

1317 "OUTPUT:VARIABLE", 

1318 Variable_Name="Zone Ideal Loads Zone Total Heating Energy", 

1319 Reporting_Frequency="Hourly", 

1320 ) 

1321 idf.newidfobject( 

1322 "OUTPUT:VARIABLE", 

1323 Variable_Name="Zone Ideal Loads Zone Total Cooling Energy", 

1324 Reporting_Frequency="Hourly", 

1325 ) 

1326 idf.newidfobject( 

1327 "OUTPUT:VARIABLE", 

1328 Variable_Name="Zone Windows Total Heat Gain Rate", 

1329 Reporting_Frequency="Hourly", 

1330 ) 

1331 idf.newidfobject( 

1332 "OUTPUT:VARIABLE", 

1333 Variable_Name="Zone Windows Total Heat Gain Energy", 

1334 Reporting_Frequency="Hourly", 

1335 ) 

1336 idf.newidfobject( 

1337 "OUTPUT:VARIABLE", 

1338 Variable_Name="Zone Windows Total Transmitted Solar Radiation " 

1339 "Energy", 

1340 Reporting_Frequency="Hourly", 

1341 ) 

1342 idf.newidfobject( 

1343 "OUTPUT:VARIABLE", 

1344 Variable_Name="Zone Air System Sensible Heating Energy", 

1345 Reporting_Frequency="Hourly", 

1346 ) 

1347 idf.newidfobject( 

1348 "OUTPUT:VARIABLE", 

1349 Variable_Name="Zone Air System Sensible Cooling Energy", 

1350 Reporting_Frequency="Hourly", 

1351 ) 

1352 if 'output_infiltration' in sim_settings.output_keys: 

1353 idf.newidfobject( 

1354 "OUTPUT:VARIABLE", 

1355 Variable_Name="Zone Infiltration Sensible Heat Gain Energy", 

1356 Reporting_Frequency="Hourly", 

1357 ) 

1358 idf.newidfobject( 

1359 "OUTPUT:VARIABLE", 

1360 Variable_Name="Zone Infiltration Sensible Heat Loss Energy", 

1361 Reporting_Frequency="Hourly", 

1362 ) 

1363 idf.newidfobject( 

1364 "OUTPUT:VARIABLE", 

1365 Variable_Name="Zone Infiltration Air Change Rate", 

1366 Reporting_Frequency="Hourly", 

1367 ) 

1368 idf.newidfobject( 

1369 "OUTPUT:VARIABLE", 

1370 Variable_Name="Zone Ventilation Air Change Rate", 

1371 Reporting_Frequency="Hourly", 

1372 ) 

1373 idf.newidfobject( 

1374 "OUTPUT:VARIABLE", 

1375 Variable_Name="Zone Ventilation Standard Density Volume Flow Rate", 

1376 Reporting_Frequency="Hourly", 

1377 ) 

1378 idf.newidfobject( 

1379 "OUTPUT:VARIABLE", 

1380 Variable_Name="Zone Ventilation Total Heat Gain Energy", 

1381 Reporting_Frequency="Hourly", 

1382 ) 

1383 idf.newidfobject( 

1384 "OUTPUT:VARIABLE", 

1385 Variable_Name="Zone Ventilation Total Heat Loss Energy", 

1386 Reporting_Frequency="Hourly", 

1387 ) 

1388 

1389 if 'output_meters' in sim_settings.output_keys: 

1390 idf.newidfobject( 

1391 "OUTPUT:METER", 

1392 Key_Name="Heating:EnergyTransfer", 

1393 Reporting_Frequency="Hourly", 

1394 ) 

1395 idf.newidfobject( 

1396 "OUTPUT:METER", 

1397 Key_Name="Cooling:EnergyTransfer", 

1398 Reporting_Frequency="Hourly", 

1399 ) 

1400 if 'output_dxf' in sim_settings.output_keys: 

1401 idf.newidfobject("OUTPUT:SURFACES:DRAWING", 

1402 Report_Type="DXF") 

1403 if sim_settings.cfd_export: 

1404 idf.newidfobject( 

1405 "OUTPUT:VARIABLE", 

1406 Variable_Name="Surface Inside Face Temperature", 

1407 Reporting_Frequency="Hourly", 

1408 ) 

1409 idf.newidfobject("OUTPUT:DIAGNOSTICS", 

1410 Key_1="DisplayAdvancedReportVariables", 

1411 Key_2="DisplayExtraWarnings") 

1412 return idf 

1413 

1414 @staticmethod 

1415 def export_geom_to_idf(sim_settings: EnergyPlusSimSettings, 

1416 elements: dict, idf: IDF): 

1417 """Write space boundary geometry to idf. 

1418 

1419 This function converts the space boundary bound_shape from 

1420 OpenCascade to idf geometry. 

1421 

1422 Args: 

1423 elements: dict[guid: element] 

1424 idf: idf file object 

1425 """ 

1426 logger.info("Export IDF geometry") 

1427 bounds = filter_elements(elements, SpaceBoundary) 

1428 for bound in bounds: 

1429 idfp = IdfObject(sim_settings, bound, idf) 

1430 if idfp.skip_bound: 

1431 idf.popidfobject(idfp.key, -1) 

1432 logger.warning( 

1433 "Boundary with the GUID %s (%s) is skipped (due to " 

1434 "missing boundary conditions)!", 

1435 idfp.name, idfp.surface_type) 

1436 continue 

1437 bounds_2b = filter_elements(elements, SpaceBoundary2B) 

1438 for b_bound in bounds_2b: 

1439 idfp = IdfObject(sim_settings, b_bound, idf) 

1440 if idfp.skip_bound: 

1441 logger.warning( 

1442 "Boundary with the GUID %s (%s) is skipped (due to " 

1443 "missing boundary conditions)!", 

1444 idfp.name, idfp.surface_type) 

1445 continue 

1446 

1447 @staticmethod 

1448 def idf_validity_check(idf): 

1449 """Perform idf validity check and simple fixes. 

1450 

1451 This function performs a basic validity check of the resulting idf. 

1452 It removes openings from adiabatic surfaces and very small surfaces. 

1453 

1454 Args: 

1455 idf: idf file object 

1456 """ 

1457 logger.info('Start IDF Validity Checker') 

1458 

1459 # remove erroneous fenestration surfaces which do may crash 

1460 # EnergyPlus simulation 

1461 fenestrations = idf.idfobjects['FENESTRATIONSURFACE:DETAILED'] 

1462 

1463 # Create a list of fenestrations to remove 

1464 to_remove = [] 

1465 

1466 for fenestration in fenestrations: 

1467 should_remove = False 

1468 

1469 # Check for missing building surface reference 

1470 if not fenestration.Building_Surface_Name: 

1471 should_remove = True 

1472 else: 

1473 # Check if the referenced surface is adiabatic 

1474 building_surface = idf.getobject( 

1475 'BUILDINGSURFACE:DETAILED', 

1476 fenestration.Building_Surface_Name 

1477 ) 

1478 if building_surface and building_surface.Outside_Boundary_Condition == 'Adiabatic': 

1479 should_remove = True 

1480 

1481 if should_remove: 

1482 to_remove.append(fenestration) 

1483 

1484 # Remove the collected fenestrations 

1485 for fenestration in to_remove: 

1486 logger.info('Removed Fenestration: %s' % fenestration.Name) 

1487 idf.removeidfobject(fenestration) 

1488 

1489 # Check if shading control elements contain unavailable fenestration 

1490 fenestration_updated = idf.idfobjects['FENESTRATIONSURFACE:DETAILED'] 

1491 shading_control = idf.idfobjects['WINDOWSHADINGCONTROL'] 

1492 fenestration_guids = [fe.Name for fe in fenestration_updated] 

1493 for shc in shading_control: 

1494 # create a list with current fenestration guids (only available 

1495 # fenestration) 

1496 fenestration_guids_new = [] 

1497 skipped_fenestration = False # flag for unavailable fenestration 

1498 for attr_name in dir(shc): 

1499 if ('Fenestration_Surface' in attr_name): 

1500 if (getattr(shc, attr_name) in 

1501 fenestration_guids): 

1502 fenestration_guids_new.append(getattr(shc, attr_name)) 

1503 elif (getattr(shc, attr_name) not in 

1504 fenestration_guids) and getattr(shc, attr_name): 

1505 skipped_fenestration = True 

1506 # if the shading control element containes unavailable 

1507 # fenestration objects, the shading control must be updated to 

1508 # prevent errors in simulation 

1509 if fenestration_guids_new and skipped_fenestration: 

1510 fenestration_dict = {} 

1511 for i, guid in enumerate(fenestration_guids_new): 

1512 fenestration_dict.update({'Fenestration_Surface_' + str( 

1513 i + 1) + '_Name': guid}) 

1514 # remove previous shading control from idf and create a new one 

1515 # removing individual attributes of the shading element 

1516 # caused errors, so new shading control is created 

1517 idf.removeidfobject(shc) 

1518 idf.newidfobject("WINDOWSHADINGCONTROL", Name=shc.Name, 

1519 Zone_Name=shc.Zone_Name, 

1520 Shading_Type=shc.Shading_Type, 

1521 Construction_with_Shading_Name= 

1522 shc.Construction_with_Shading_Name, 

1523 Shading_Control_Type=shc.Shading_Control_Type, 

1524 Setpoint=shc.Setpoint, 

1525 Setpoint_2=shc.Setpoint_2, 

1526 Multiple_Surface_Control_Type= 

1527 shc.Multiple_Surface_Control_Type, 

1528 **fenestration_dict) 

1529 logger.info('Updated Shading Control due to unavailable ' 

1530 'fenestration: %s' % shc.Name) 

1531 

1532 # check for small building surfaces and remove them 

1533 sfs = idf.getsurfaces() 

1534 small_area_obj = [s for s in sfs 

1535 if PyOCCTools.get_shape_area( 

1536 PyOCCTools.make_faces_from_pnts(s.coords)) < 1e-2] 

1537 

1538 for obj in small_area_obj: 

1539 logger.info('Removed small area: %s' % obj.Name) 

1540 idf.removeidfobject(obj) 

1541 

1542 # check for small shading surfaces and remove them 

1543 shadings = idf.getshadingsurfaces() 

1544 small_area_obj = [s for s in shadings if PyOCCTools.get_shape_area( 

1545 PyOCCTools.make_faces_from_pnts(s.coords)) < 1e-2] 

1546 

1547 for obj in small_area_obj: 

1548 logger.info('Removed small area: %s' % obj.Name) 

1549 idf.removeidfobject(obj) 

1550 

1551 # Check for building surfaces holding default window materials 

1552 bsd = idf.idfobjects['BUILDINGSURFACE:DETAILED'] 

1553 for sf in bsd: 

1554 if sf.Construction_Name == 'BS Exterior Window': 

1555 logger.info( 

1556 'Surface due to invalid material: %s' % sf.Name) 

1557 idf.removeidfobject(sf) 

1558 logger.info('IDF Validity Checker done') 

1559 

1560 

1561class IdfObject: 

1562 """Create idf elements for surfaces. 

1563 

1564 This class holds all data required for the idf setup of 

1565 BUILDINGSURFACE:DETAILED and FENESTRATIONSURFACE:DETAILED. 

1566 This includes further methods for processing the preprocessed information 

1567 from the BIM2SIM process for the use in idf (e.g., surface type mapping). 

1568 """ 

1569 

1570 def __init__(self, sim_settings, inst_obj, idf): 

1571 self.name = inst_obj.guid 

1572 self.building_surface_name = None 

1573 self.key = None 

1574 self.out_bound_cond = '' 

1575 self.out_bound_cond_obj = '' 

1576 self.sun_exposed = '' 

1577 self.wind_exposed = '' 

1578 self.surface_type = None 

1579 self.physical = inst_obj.physical 

1580 self.construction_name = None 

1581 self.related_bound = inst_obj.related_bound 

1582 self.this_bound = inst_obj 

1583 self.skip_bound = False 

1584 self.bound_shape = inst_obj.bound_shape 

1585 self.add_window_shade = False 

1586 if not hasattr(inst_obj.bound_thermal_zone, 'guid'): 

1587 self.skip_bound = True 

1588 return 

1589 self.zone_name = inst_obj.bound_thermal_zone.guid 

1590 if inst_obj.parent_bound: 

1591 self.key = "FENESTRATIONSURFACE:DETAILED" 

1592 if sim_settings.add_window_shading == 'Interior': 

1593 self.add_window_shade = 'Interior' 

1594 elif sim_settings.add_window_shading == 'Exterior': 

1595 self.add_window_shade = 'Exterior' 

1596 else: 

1597 self.key = "BUILDINGSURFACE:DETAILED" 

1598 if inst_obj.parent_bound: 

1599 self.building_surface_name = inst_obj.parent_bound.guid 

1600 self.map_surface_types(inst_obj) 

1601 self.map_boundary_conditions(inst_obj) 

1602 self.set_preprocessed_construction_name() 

1603 # only set a construction name if this construction is available 

1604 if not self.construction_name \ 

1605 or not (idf.getobject("CONSTRUCTION", self.construction_name) 

1606 or idf.getobject("CONSTRUCTION:AIRBOUNDARY", 

1607 self.construction_name)): 

1608 self.set_construction_name() 

1609 obj = self.set_idfobject_attributes(idf) 

1610 if obj is not None: 

1611 self.set_idfobject_coordinates(obj, idf, inst_obj) 

1612 else: 

1613 pass 

1614 

1615 def set_construction_name(self): 

1616 """Set default construction names. 

1617 

1618 This function sets default constructions for all idf surface types. 

1619 Should only be used if no construction is available for the current 

1620 object. 

1621 """ 

1622 if self.surface_type == "Wall": 

1623 self.construction_name = "Project Wall" 

1624 elif self.surface_type == "Roof": 

1625 self.construction_name = "Project Flat Roof" 

1626 elif self.surface_type == "Ceiling": 

1627 self.construction_name = "Project Ceiling" 

1628 elif self.surface_type == "Floor": 

1629 self.construction_name = "Project Floor" 

1630 elif self.surface_type == "Door": 

1631 self.construction_name = "Project Door" 

1632 elif self.surface_type == "Window": 

1633 self.construction_name = "Project External Window" 

1634 

1635 def set_preprocessed_construction_name(self): 

1636 """Set preprocessed constructions. 

1637 

1638 This function sets constructions of idf surfaces to preprocessed 

1639 constructions. Virtual space boundaries are set to be an air wall 

1640 (not defined in preprocessing). 

1641 """ 

1642 # set air wall for virtual bounds 

1643 if not self.physical: 

1644 if self.out_bound_cond == "Surface": 

1645 self.construction_name = "Air Wall" 

1646 else: 

1647 rel_elem = self.this_bound.bound_element 

1648 if not rel_elem: 

1649 return 

1650 if any([isinstance(rel_elem, window) for window in 

1651 all_subclasses(Window, include_self=True)]): 

1652 self.construction_name = 'Window_WM_' + \ 

1653 rel_elem.layerset.layers[ 

1654 0].material.name \ 

1655 + '_' + str( 

1656 rel_elem.layerset.layers[0].thickness.to(ureg.metre).m) 

1657 else: 

1658 self.construction_name = (rel_elem.key.replace( 

1659 "Disaggregated", "") + '_' + str(len( 

1660 rel_elem.layerset.layers)) + '_' + '_'.join( 

1661 [str(l.thickness.to(ureg.metre).m) for l in 

1662 rel_elem.layerset.layers])) 

1663 

1664 def set_idfobject_coordinates(self, obj, idf: IDF, 

1665 inst_obj: Union[SpaceBoundary, 

1666 SpaceBoundary2B]): 

1667 """Export surface coordinates. 

1668 

1669 This function exports the surface coordinates from the BIM2SIM Space 

1670 Boundary instance to idf. 

1671 Circular shapes and shapes with more than 120 vertices 

1672 (BuildingSurfaces) or more than 4 vertices (fenestration) are 

1673 simplified. 

1674 

1675 Args: 

1676 obj: idf-surface object (buildingSurface:Detailed or fenestration) 

1677 idf: idf file object 

1678 inst_obj: SpaceBoundary instance 

1679 """ 

1680 # write bound_shape to obj 

1681 obj_pnts = PyOCCTools.get_points_of_face(self.bound_shape) 

1682 obj_coords = [] 

1683 for pnt in obj_pnts: 

1684 co = tuple(round(p, 3) for p in pnt.Coord()) 

1685 obj_coords.append(co) 

1686 try: 

1687 obj.setcoords(obj_coords) 

1688 except Exception as ex: 

1689 logger.warning(f"Unexpected {ex=}. Setting coordinates for " 

1690 f"{inst_obj.guid} failed. This element is not " 

1691 f"exported." 

1692 f"{type(ex)=}") 

1693 self.skip_bound = True 

1694 return 

1695 circular_shape = self.get_circular_shape(obj_pnts) 

1696 try: 

1697 if (3 <= len(obj_coords) <= 120 

1698 and self.key == "BUILDINGSURFACE:DETAILED") \ 

1699 or (3 <= len(obj_coords) <= 4 

1700 and self.key == "FENESTRATIONSURFACE:DETAILED"): 

1701 obj.setcoords(obj_coords) 

1702 elif circular_shape is True and self.surface_type != 'Door': 

1703 self.process_circular_shapes(idf, obj_coords, obj, inst_obj) 

1704 else: 

1705 self.process_other_shapes(inst_obj, obj) 

1706 except Exception as ex: 

1707 logger.warning(f"Unexpected {ex=}. Setting coordinates for " 

1708 f"{inst_obj.guid} failed. This element is not " 

1709 f"exported." 

1710 f"{type(ex)=}") 

1711 

1712 def set_idfobject_attributes(self, idf: IDF): 

1713 """Writes precomputed surface attributes to idf. 

1714 

1715 Args: 

1716 idf: the idf file 

1717 """ 

1718 if self.surface_type is not None: 

1719 if self.key == "BUILDINGSURFACE:DETAILED": 

1720 if self.surface_type.lower() in {"DOOR".lower(), 

1721 "Window".lower()}: 

1722 self.surface_type = "Wall" 

1723 obj = idf.newidfobject( 

1724 self.key, 

1725 Name=self.name, 

1726 Surface_Type=self.surface_type, 

1727 Construction_Name=self.construction_name, 

1728 Outside_Boundary_Condition=self.out_bound_cond, 

1729 Outside_Boundary_Condition_Object=self.out_bound_cond_obj, 

1730 Zone_Name=self.zone_name, 

1731 Sun_Exposure=self.sun_exposed, 

1732 Wind_Exposure=self.wind_exposed, 

1733 ) 

1734 else: 

1735 obj = idf.newidfobject( 

1736 self.key, 

1737 Name=self.name, 

1738 Surface_Type=self.surface_type, 

1739 Construction_Name=self.construction_name, 

1740 Building_Surface_Name=self.building_surface_name, 

1741 Outside_Boundary_Condition_Object=self.out_bound_cond_obj, 

1742 ) 

1743 return obj 

1744 

1745 def map_surface_types(self, inst_obj: Union[SpaceBoundary, 

1746 SpaceBoundary2B]): 

1747 """Map surface types. 

1748 

1749 This function maps the attributes of a SpaceBoundary instance to idf 

1750 surface type. 

1751 

1752 Args: 

1753 inst_obj: SpaceBoundary instance 

1754 """ 

1755 # TODO use bim2sim elements mapping instead of ifc.is_a() 

1756 # TODO update to new disaggregations 

1757 elem = inst_obj.bound_element 

1758 surface_type = None 

1759 if elem is not None: 

1760 if any([isinstance(elem, wall) for wall in all_subclasses(Wall, 

1761 include_self=True)]): 

1762 surface_type = 'Wall' 

1763 elif any([isinstance(elem, door) for door in all_subclasses(Door, 

1764 include_self=True)]): 

1765 surface_type = "Door" 

1766 elif any([isinstance(elem, window) for window in all_subclasses( 

1767 Window, include_self=True)]): 

1768 surface_type = "Window" 

1769 elif any([isinstance(elem, roof) for roof in all_subclasses(Roof, 

1770 include_self=True)]): 

1771 surface_type = "Roof" 

1772 elif any([isinstance(elem, slab) for slab in all_subclasses(Slab, 

1773 include_self=True)]): 

1774 if any([isinstance(elem, floor) for floor in all_subclasses( 

1775 GroundFloor, include_self=True)]): 

1776 surface_type = "Floor" 

1777 elif any([isinstance(elem, floor) for floor in all_subclasses( 

1778 InnerFloor, include_self=True)]): 

1779 if inst_obj.top_bottom == BoundaryOrientation.bottom: 

1780 surface_type = "Floor" 

1781 elif inst_obj.top_bottom == BoundaryOrientation.top: 

1782 surface_type = "Ceiling" 

1783 elif inst_obj.top_bottom == BoundaryOrientation.vertical: 

1784 surface_type = "Wall" 

1785 logger.warning(f"InnerFloor with vertical orientation " 

1786 f"found, exported as wall, " 

1787 f"GUID: {inst_obj.guid}.") 

1788 else: 

1789 logger.warning(f"InnerFloor was not correctly matched " 

1790 f"to surface type for GUID: " 

1791 f"{inst_obj.guid}.") 

1792 surface_type = "Floor" 

1793 # elif elem.ifc is not None: 

1794 # if elem.ifc.is_a("IfcBeam"): 

1795 # if not PyOCCTools.compare_direction_of_normals( 

1796 # inst_obj.bound_normal, gp_XYZ(0, 0, 1)): 

1797 # surface_type = 'Wall' 

1798 # else: 

1799 # surface_type = 'Ceiling' 

1800 # elif elem.ifc.is_a('IfcColumn'): 

1801 # surface_type = 'Wall' 

1802 elif inst_obj.top_bottom == BoundaryOrientation.bottom: 

1803 surface_type = "Floor" 

1804 elif inst_obj.top_bottom == BoundaryOrientation.top: 

1805 surface_type = "Ceiling" 

1806 if inst_obj.related_bound is None or inst_obj.is_external: 

1807 surface_type = "Roof" 

1808 elif inst_obj.top_bottom == BoundaryOrientation.vertical: 

1809 surface_type = "Wall" 

1810 else: 

1811 if not PyOCCTools.compare_direction_of_normals( 

1812 inst_obj.bound_normal, gp_XYZ(0, 0, 1)): 

1813 surface_type = 'Wall' 

1814 elif inst_obj.top_bottom == BoundaryOrientation.bottom: 

1815 surface_type = "Floor" 

1816 elif inst_obj.top_bottom == BoundaryOrientation.top: 

1817 surface_type = "Ceiling" 

1818 if inst_obj.related_bound is None or inst_obj.is_external: 

1819 surface_type = "Roof" 

1820 else: 

1821 logger.warning(f"No surface type matched for {inst_obj}!") 

1822 elif not inst_obj.physical: 

1823 if not PyOCCTools.compare_direction_of_normals( 

1824 inst_obj.bound_normal, gp_XYZ(0, 0, 1)): 

1825 surface_type = 'Wall' 

1826 else: 

1827 if inst_obj.top_bottom == BoundaryOrientation.bottom: 

1828 surface_type = "Floor" 

1829 elif inst_obj.top_bottom == BoundaryOrientation.top: 

1830 surface_type = "Ceiling" 

1831 else: 

1832 logger.warning(f"No surface type matched for {inst_obj}!") 

1833 

1834 self.surface_type = surface_type 

1835 

1836 def map_boundary_conditions(self, inst_obj: Union[SpaceBoundary, 

1837 SpaceBoundary2B]): 

1838 """Map boundary conditions. 

1839 

1840 This function maps the boundary conditions of a SpaceBoundary instance 

1841 to the idf space boundary conditions. 

1842 

1843 Args: 

1844 inst_obj: SpaceBoundary instance 

1845 """ 

1846 if inst_obj.level_description == '2b' \ 

1847 or inst_obj.related_adb_bound is not None: 

1848 self.out_bound_cond = 'Adiabatic' 

1849 self.sun_exposed = 'NoSun' 

1850 self.wind_exposed = 'NoWind' 

1851 elif (hasattr(inst_obj.ifc, 'CorrespondingBoundary') 

1852 and ((inst_obj.ifc.CorrespondingBoundary is not None) and ( 

1853 inst_obj.ifc.CorrespondingBoundary.InternalOrExternalBoundary.upper() 

1854 == 'EXTERNAL_EARTH')) 

1855 and (self.key == "BUILDINGSURFACE:DETAILED") 

1856 and not (len(inst_obj.opening_bounds) > 0)): 

1857 self.out_bound_cond = "Ground" 

1858 self.sun_exposed = 'NoSun' 

1859 self.wind_exposed = 'NoWind' 

1860 elif inst_obj.is_external and inst_obj.physical \ 

1861 and not self.surface_type == 'Floor': 

1862 self.out_bound_cond = 'Outdoors' 

1863 self.sun_exposed = 'SunExposed' 

1864 self.wind_exposed = 'WindExposed' 

1865 self.out_bound_cond_obj = '' 

1866 elif self.surface_type == "Floor" and \ 

1867 (inst_obj.related_bound is None 

1868 or inst_obj.related_bound.ifc.RelatingSpace.is_a( 

1869 'IfcExternalSpatialElement')): 

1870 self.out_bound_cond = "Ground" 

1871 self.sun_exposed = 'NoSun' 

1872 self.wind_exposed = 'NoWind' 

1873 elif inst_obj.related_bound is not None \ 

1874 and not inst_obj.related_bound.ifc.RelatingSpace.is_a( 

1875 'IfcExternalSpatialElement'): 

1876 self.out_bound_cond = 'Surface' 

1877 self.out_bound_cond_obj = inst_obj.related_bound.guid 

1878 self.sun_exposed = 'NoSun' 

1879 self.wind_exposed = 'NoWind' 

1880 elif self.key == "FENESTRATIONSURFACE:DETAILED": 

1881 self.out_bound_cond = 'Outdoors' 

1882 self.sun_exposed = 'SunExposed' 

1883 self.wind_exposed = 'WindExposed' 

1884 self.out_bound_cond_obj = '' 

1885 elif self.related_bound is None: 

1886 self.out_bound_cond = 'Outdoors' 

1887 self.sun_exposed = 'SunExposed' 

1888 self.wind_exposed = 'WindExposed' 

1889 self.out_bound_cond_obj = '' 

1890 else: 

1891 self.skip_bound = True 

1892 

1893 @staticmethod 

1894 def get_circular_shape(obj_pnts: list[tuple]) -> bool: 

1895 """Check if a shape is circular. 

1896 

1897 This function checks if a SpaceBoundary has a circular shape. 

1898 

1899 Args: 

1900 obj_pnts: SpaceBoundary vertices (list of coordinate tuples) 

1901 Returns: 

1902 True if shape is circular 

1903 """ 

1904 circular_shape = False 

1905 # compute if shape is circular: 

1906 if len(obj_pnts) > 4: 

1907 pnt = obj_pnts[0] 

1908 pnt2 = obj_pnts[1] 

1909 distance_prev = pnt.Distance(pnt2) 

1910 pnt = pnt2 

1911 for pnt2 in obj_pnts[2:]: 

1912 distance = pnt.Distance(pnt2) 

1913 if (distance_prev - distance) ** 2 < 0.01: 

1914 circular_shape = True 

1915 pnt = pnt2 

1916 distance_prev = distance 

1917 else: 

1918 continue 

1919 return circular_shape 

1920 

1921 @staticmethod 

1922 def process_circular_shapes(idf: IDF, obj_coords: list[tuple], obj, 

1923 inst_obj: Union[SpaceBoundary, SpaceBoundary2B] 

1924 ): 

1925 """Simplify circular space boundaries. 

1926 

1927 This function processes circular boundary shapes. It converts circular 

1928 shapes to triangular shapes. 

1929 

1930 Args: 

1931 idf: idf file object 

1932 obj_coords: coordinates of an idf object 

1933 obj: idf object 

1934 inst_obj: SpaceBoundary instance 

1935 """ 

1936 drop_count = int(len(obj_coords) / 8) 

1937 drop_list = obj_coords[0::drop_count] 

1938 pnt = drop_list[0] 

1939 counter = 0 

1940 # del inst_obj.__dict__['bound_center'] 

1941 for pnt2 in drop_list[1:]: 

1942 counter += 1 

1943 new_obj = idf.copyidfobject(obj) 

1944 new_obj.Name = str(obj.Name) + '_' + str(counter) 

1945 fc = PyOCCTools.make_faces_from_pnts( 

1946 [pnt, pnt2, inst_obj.bound_center.Coord()]) 

1947 fcsc = PyOCCTools.scale_face(fc, 0.99) 

1948 new_pnts = PyOCCTools.get_points_of_face(fcsc) 

1949 new_coords = [] 

1950 for pnt in new_pnts: 

1951 new_coords.append(pnt.Coord()) 

1952 new_obj.setcoords(new_coords) 

1953 pnt = pnt2 

1954 new_obj = idf.copyidfobject(obj) 

1955 new_obj.Name = str(obj.Name) + '_' + str(counter + 1) 

1956 fc = PyOCCTools.make_faces_from_pnts( 

1957 [drop_list[-1], drop_list[0], inst_obj.bound_center.Coord()]) 

1958 fcsc = PyOCCTools.scale_face(fc, 0.99) 

1959 new_pnts = PyOCCTools.get_points_of_face(fcsc) 

1960 new_coords = [] 

1961 for pnt in new_pnts: 

1962 new_coords.append(pnt.Coord()) 

1963 new_obj.setcoords(new_coords) 

1964 idf.removeidfobject(obj) 

1965 

1966 @staticmethod 

1967 def process_other_shapes(inst_obj: Union[SpaceBoundary, SpaceBoundary2B], 

1968 obj): 

1969 """Simplify non-circular shapes. 

1970 

1971 This function processes non-circular shapes with too many vertices 

1972 by approximation of the shape utilizing the UV-Bounds from OCC 

1973 (more than 120 vertices for BUILDINGSURFACE:DETAILED 

1974 and more than 4 vertices for FENESTRATIONSURFACE:DETAILED) 

1975 

1976 Args: 

1977 inst_obj: SpaceBoundary Instance 

1978 obj: idf object 

1979 """ 

1980 # print("TOO MANY EDGES") 

1981 obj_pnts = [] 

1982 exp = TopExp_Explorer(inst_obj.bound_shape, TopAbs_FACE) 

1983 face = topods_Face(exp.Current()) 

1984 umin, umax, vmin, vmax = breptools_UVBounds(face) 

1985 surf = BRep_Tool.Surface(face) 

1986 plane = Handle_Geom_Plane_DownCast(surf) 

1987 plane = gp_Pln(plane.Location(), plane.Axis().Direction()) 

1988 new_face = BRepBuilderAPI_MakeFace(plane, 

1989 umin, 

1990 umax, 

1991 vmin, 

1992 vmax).Face().Reversed() 

1993 face_exp = TopExp_Explorer(new_face, TopAbs_WIRE) 

1994 w_exp = BRepTools_WireExplorer(topods_Wire(face_exp.Current())) 

1995 while w_exp.More(): 

1996 wire_vert = w_exp.CurrentVertex() 

1997 obj_pnts.append(BRep_Tool.Pnt(wire_vert)) 

1998 w_exp.Next() 

1999 obj_coords = [] 

2000 for pnt in obj_pnts: 

2001 obj_coords.append(pnt.Coord()) 

2002 obj.setcoords(obj_coords)