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

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 

195 @staticmethod 

196 def init_zonelist( 

197 idf: IDF, 

198 name: str = None, 

199 zones_in_list: list[str] = None): 

200 """Initialize zone lists. 

201 

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

206 

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) 

235 

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

237 """Assign one zonegroup per storey. 

238 

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) 

259 

260 # add zonelist for All_Zones 

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

262 if zlist.Name != "All_Zones"] 

263 

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 

289 

290 correct_preprocessing = True 

291 

292 return correct_preprocessing 

293 

294 def get_preprocessed_materials_and_constructions( 

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

296 """Get preprocessed materials and constructions. 

297 

298 This function sets preprocessed construction and material for 

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

300 boundary construction is set. 

301 

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) 

332 

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 ) 

348 

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. 

355 

356 This function uses preprocessed data to define idf construction 

357 elements. 

358 

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 ) 

384 

385 @staticmethod 

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

387 """Set a preprocessed material element. 

388 

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 ) 

421 

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. 

427 

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. 

432 

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) 

461 

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 ) 

504 

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

506 space: ThermalZone): 

507 """Set heating and cooling parameters. 

508 

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

510 available from BIM2SIM Preprocessing (either IFC-based or 

511 Template-based). 

512 

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) 

523 

524 cooling_availability = "Off" 

525 heating_availability = "Off" 

526 

527 if space.with_cooling: 

528 cooling_availability = "On" 

529 if space.with_heating: 

530 heating_availability = "On" 

531 

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 ) 

539 

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

541 zone_name: str, space: ThermalZone): 

542 """Set occupancy schedules. 

543 

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. 

547 

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 ) 

598 

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

604 

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

606 

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) 

641 

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. 

646 

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. 

650 

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 ) 

683 

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

685 zone_name: str, space: ThermalZone): 

686 """Set internal loads from lighting. 

687 

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. 

691 

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 ) 

735 

736 @staticmethod 

737 def set_infiltration(idf: IDF, 

738 name: str, zone_name: str, 

739 space: ThermalZone, ep_version: str): 

740 """Set infiltration rate. 

741 

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

743 BIM2SIM preprocessing values (IFC-based if available or 

744 template-based). 

745 

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 ) 

771 

772 @staticmethod 

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

774 space: ThermalZone, ep_version): 

775 """Set natural ventilation. 

776 

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. 

782 

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 

789 

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 ) 

805 

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 ) 

819 

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 ) 

849 

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 ) 

863 

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 ) 

879 

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

881 """Set 24 hour hvac template. 

882 

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

884 cooling. 

885 

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) 

896 

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 

909 

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

915 

916 This function manually sets heating and cooling templates. 

917 

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 

928 

929 setback_htg = 18 # "T_threshold_heating" 

930 setback_clg = 26 # "T_threshold_cooling" 

931 

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 

937 

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 ) 

963 

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 

972 

973 @staticmethod 

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

975 """Write schedules to idf. 

976 

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

978 setup of schedules (combined with set_hvac_template). 

979 

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

1000 

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 

1009 

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. 

1014 

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] 

1024 

1025 @staticmethod 

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

1027 """Add shading boundaries to idf. 

1028 

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 

1045 

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) 

1067 

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) 

1079 

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) 

1094 

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 ) 

1128 

1129 @staticmethod 

1130 def set_simulation_control(sim_settings: EnergyPlusSimSettings, idf): 

1131 """Set simulation control parameters. 

1132 

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' 

1155 

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 

1163 

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

1165 building.Solar_Distribution = sim_settings.solar_distribution 

1166 

1167 @staticmethod 

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

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

1170 

1171 Args: 

1172 idf: idf file object 

1173 t_ground: ground temperature as ureg.Quantity 

1174 """ 

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

1176 

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) 

1185 

1186 @staticmethod 

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

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

1189 

1190 Args: 

1191 idf: idf file object 

1192 sim_settings: BIM2SIM simulation settings 

1193 """ 

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

1195 

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 

1200 

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 ) 

1383 

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 

1408 

1409 @staticmethod 

1410 def export_geom_to_idf(sim_settings: EnergyPlusSimSettings, 

1411 elements: dict, idf: IDF): 

1412 """Write space boundary geometry to idf. 

1413 

1414 This function converts the space boundary bound_shape from 

1415 OpenCascade to idf geometry. 

1416 

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 

1441 

1442 @staticmethod 

1443 def idf_validity_check(idf): 

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

1445 

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

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

1448 

1449 Args: 

1450 idf: idf file object 

1451 """ 

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

1453 

1454 # remove erroneous fenestration surfaces which do may crash 

1455 # EnergyPlus simulation 

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

1457 

1458 # Create a list of fenestrations to remove 

1459 to_remove = [] 

1460 

1461 for fenestration in fenestrations: 

1462 should_remove = False 

1463 

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 

1475 

1476 if should_remove: 

1477 to_remove.append(fenestration) 

1478 

1479 # Remove the collected fenestrations 

1480 for fenestration in to_remove: 

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

1482 idf.removeidfobject(fenestration) 

1483 

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) 

1526 

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] 

1532 

1533 for obj in small_area_obj: 

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

1535 idf.removeidfobject(obj) 

1536 

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] 

1541 

1542 for obj in small_area_obj: 

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

1544 idf.removeidfobject(obj) 

1545 

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') 

1554 

1555 

1556class IdfObject: 

1557 """Create idf elements for surfaces. 

1558 

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

1564 

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 

1609 

1610 def set_construction_name(self): 

1611 """Set default construction names. 

1612 

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" 

1629 

1630 def set_preprocessed_construction_name(self): 

1631 """Set preprocessed constructions. 

1632 

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])) 

1658 

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

1660 inst_obj: Union[SpaceBoundary, 

1661 SpaceBoundary2B]): 

1662 """Export surface coordinates. 

1663 

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. 

1669 

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)=}") 

1706 

1707 def set_idfobject_attributes(self, idf: IDF): 

1708 """Writes precomputed surface attributes to idf. 

1709 

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 

1739 

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

1741 SpaceBoundary2B]): 

1742 """Map surface types. 

1743 

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

1745 surface type. 

1746 

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}!") 

1828 

1829 self.surface_type = surface_type 

1830 

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

1832 SpaceBoundary2B]): 

1833 """Map boundary conditions. 

1834 

1835 This function maps the boundary conditions of a SpaceBoundary instance 

1836 to the idf space boundary conditions. 

1837 

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 

1887 

1888 @staticmethod 

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

1890 """Check if a shape is circular. 

1891 

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

1893 

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 

1915 

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. 

1921 

1922 This function processes circular boundary shapes. It converts circular 

1923 shapes to triangular shapes. 

1924 

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) 

1960 

1961 @staticmethod 

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

1963 obj): 

1964 """Simplify non-circular shapes. 

1965 

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) 

1970 

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)