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

909 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-01 10:24 +0000

1from __future__ import annotations 

2 

3import logging 

4import math 

5import os 

6from pathlib import Path, PosixPath 

7from typing import Union, TYPE_CHECKING 

8 

9import pandas as pd 

10from OCC.Core.BRep import BRep_Tool 

11from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace 

12from OCC.Core.BRepTools import breptools_UVBounds, BRepTools_WireExplorer 

13from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_WIRE 

14from OCC.Core.TopExp import TopExp_Explorer 

15from OCC.Core.TopoDS import topods_Face, topods_Wire 

16from OCC.Core._Geom import Handle_Geom_Plane_DownCast 

17 

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

19from geomeppy import IDF 

20 

21from bim2sim.elements.base_elements import IFCBased 

22from bim2sim.elements.bps_elements import (ExternalSpatialElement, 

23 SpaceBoundary2B, ThermalZone, Storey, 

24 Layer, Window, SpaceBoundary, Wall, 

25 Door, Roof, Slab, InnerFloor, 

26 GroundFloor) 

27from bim2sim.elements.mapping.units import ureg 

28from bim2sim.project import FolderStructure 

29from bim2sim.tasks.base import ITask 

30from bim2sim.utilities.common_functions import filter_elements, \ 

31 get_spaces_with_bounds, all_subclasses 

32from bim2sim.utilities.pyocc_tools import PyOCCTools 

33from bim2sim.utilities.types import BoundaryOrientation 

34 

35if TYPE_CHECKING: 

36 from bim2sim.plugins.PluginEnergyPlus.bim2sim_energyplus import \ 

37 EnergyPlusSimSettings 

38 

39logger = logging.getLogger(__name__) 

40 

41 

42class CreateIdf(ITask): 

43 """Create an EnergyPlus Input file. 

44 

45 Task to create an EnergyPlus Input file based on the for EnergyPlus 

46 preprocessed space boundary geometries. See detailed explanation in the run 

47 function below. 

48 """ 

49 

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

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

52 

53 def __init__(self, playground): 

54 super().__init__(playground) 

55 self.idf = None 

56 

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

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

59 

60 This task includes all functions for exporting EnergyPlus Input files 

61 (idf) based on the previously preprocessed SpaceBoundary geometry from 

62 the ep_geom_preprocessing task. Geometric preprocessing (includes 

63 EnergyPlus-specific space boundary enrichment) must be executed 

64 before this task. 

65 In this task, first, the IDF itself is initialized. Then, the zones, 

66 materials and constructions, shadings and control parameters are set 

67 in the idf. Within the export of the idf, the final mapping of the 

68 bim2sim elements and the idf components is executed. Shading control 

69 is added if required, and the ground temperature of the building 

70 surrounding ground is set, as well as the output variables of the 

71 simulation. Finally, the generated idf is validated, and minor 

72 corrections are performed, e.g., tiny surfaces are deleted that 

73 would cause errors during the EnergyPlus Simulation run. 

74 

75 Args: 

76 elements (dict): dictionary in the format dict[guid: element], 

77 holds preprocessed elements including space boundaries. 

78 weather_file (Path): path to weather file in .epw data format 

79 Returns: 

80 idf (IDF): EnergyPlus input file 

81 sim_results_path (Path): path to the simulation results. 

82 """ 

83 logger.info("IDF generation started ...") 

84 idf, sim_results_path = self.init_idf(self.playground.sim_settings, 

85 self.paths, weather_file, 

86 self.prj_name) 

87 self.init_zone(self.playground.sim_settings, elements, idf) 

88 self.init_zonegroups(elements, idf) 

89 self.get_preprocessed_materials_and_constructions( 

90 self.playground.sim_settings, elements, idf) 

91 if self.playground.sim_settings.add_shadings: 

92 self.add_shadings(elements, idf) 

93 self.set_simulation_control(self.playground.sim_settings, idf) 

94 idf.set_default_constructions() 

95 self.export_geom_to_idf(self.playground.sim_settings, elements, idf) 

96 if self.playground.sim_settings.add_window_shading: 

97 self.add_shading_control( 

98 self.playground.sim_settings.add_window_shading, elements, 

99 idf) 

100 self.set_ground_temperature(idf, t_ground=get_spaces_with_bounds( 

101 elements)[0].t_ground) # assuming all zones have same ground 

102 self.set_output_variables(idf, self.playground.sim_settings) 

103 self.idf_validity_check(idf) 

104 logger.info("Save idf ...") 

105 idf.save(idf.idfname) 

106 logger.info("Idf file successfully saved.") 

107 if (self.playground.sim_settings.weather_file_for_sizing or 

108 self.playground.sim_settings.enforce_system_sizing): 

109 # apply HVAC system sizing based on weather file 

110 if self.playground.sim_settings.weather_file_for_sizing: 

111 weather_file_sizing = ( 

112 self.playground.sim_settings.weather_file_for_sizing) 

113 else: 

114 weather_file_sizing = str(weather_file) 

115 self.apply_system_sizing( 

116 idf, weather_file_sizing, 

117 sim_results_path) 

118 logger.info("Idf has been updated with limits from weather file " 

119 "sizing.") 

120 

121 return idf, sim_results_path 

122 

123 def apply_system_sizing(self, idf, sizing_weather_file, sim_results_path): 

124 """ 

125 Apply system sizing based on weather file, sizes for maximum without 

126 buffer. 

127 

128 Args: 

129 idf: Eppy IDF 

130 sizing_weather_file: Weather file for system sizing 

131 sim_results_path: path to energyplus simulation results. 

132 

133 Returns: 

134 

135 """ 

136 IDF.setiddname( 

137 self.playground.sim_settings.ep_install_path / 'Energy+.idd') 

138 export_path = sim_results_path / self.prj_name 

139 

140 # initialize the idf with a minimal idf setup 

141 idf2 = IDF(export_path / str(self.prj_name + '.idf')) 

142 idf2.save(export_path / str(self.prj_name + '_before_sizing.idf')) 

143 idf2.save(export_path / str(self.prj_name + '_sizing.idf')) 

144 idf3 = IDF(export_path / str(self.prj_name + '_sizing.idf')) 

145 idf3.removeallidfobjects('OUTPUT:VARIABLE') 

146 idf3.newidfobject( 

147 "OUTPUT:VARIABLE", 

148 Variable_Name="Zone Ideal Loads Supply Air Total Cooling Rate", 

149 Reporting_Frequency="Hourly", 

150 ) 

151 idf3.newidfobject( 

152 "OUTPUT:VARIABLE", 

153 Variable_Name="Zone Ideal Loads Supply Air Total Heating Rate", 

154 Reporting_Frequency="Hourly", 

155 ) 

156 idf3.epw = sizing_weather_file 

157 for sim_control in idf3.idfobjects["SIMULATIONCONTROL"]: 

158 sim_control.Run_Simulation_for_Sizing_Periods = 'Yes' 

159 sim_control.Run_Simulation_for_Weather_File_Run_Periods = 'No' 

160 idf3.run(output_directory=export_path, readvars=True, annual=False) 

161 res_sizing = pd.read_csv(export_path / 'epluszsz.csv') 

162 res_sizing = res_sizing.set_index('Time') 

163 peak = res_sizing.loc['Peak'] 

164 peak_heating = peak.filter(like='Des Heat Load') 

165 peak_cooling = peak.filter(like='Des Sens Cool Load') 

166 for obj in idf.idfobjects['HVACTEMPLATE:ZONE:IDEALLOADSAIRSYSTEM']: 

167 curr_heating = peak_heating.filter(like=obj.Zone_Name.upper()).max() 

168 curr_cooling = peak_cooling.filter(like=obj.Zone_Name.upper()).max() 

169 obj.Heating_Limit = 'LimitCapacity' 

170 obj.Cooling_Limit = 'LimitCapacity' 

171 obj.Maximum_Sensible_Heating_Capacity = curr_heating 

172 obj.Maximum_Total_Cooling_Capacity = curr_cooling 

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

174 sim_control.Do_System_Sizing_Calculation = 'Yes' 

175 idf.save(idf.idfname) 

176 

177 @staticmethod 

178 def init_idf(sim_settings: EnergyPlusSimSettings, paths: FolderStructure, 

179 weather_file: PosixPath, ifc_name: str) -> tuple[IDF, Path]: 

180 """ Initialize the EnergyPlus input file. 

181 

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

183 and set default weather 

184 data. 

185 

186 Args: 

187 sim_settings: EnergyPlusSimSettings 

188 paths: BIM2SIM FolderStructure 

189 weather_file: PosixPath to *.epw weather file 

190 ifc_name: str of name of ifc 

191 Returns: 

192 idf file of type IDF, sim_results_path 

193 """ 

194 logger.info("Initialize the idf ...") 

195 # set the installation path for the EnergyPlus installation 

196 ep_install_path = sim_settings.ep_install_path 

197 # set the plugin path of the PluginEnergyPlus within the BIM2SIM Tool 

198 plugin_ep_path = str(Path(__file__).parent.parent.parent) 

199 # set Energy+.idd as base for new idf 

200 IDF.setiddname(ep_install_path / 'Energy+.idd') 

201 # initialize the idf with a minimal idf setup 

202 idf = IDF(plugin_ep_path + '/data/Minimal.idf') 

203 # remove location and design days 

204 idf.removeallidfobjects('SIZINGPERIOD:DESIGNDAY') 

205 idf.removeallidfobjects('SITE:LOCATION') 

206 if sim_settings.system_weather_sizing != 'DesignDay': 

207 # enable system sizing for extreme or typical days. 

208 if sim_settings.system_weather_sizing == 'Extreme': 

209 period_selection = 'Extreme' 

210 elif sim_settings.system_weather_sizing == 'Typical': 

211 period_selection = 'Typical' 

212 idf.newidfobject("SIZINGPERIOD:WEATHERFILECONDITIONTYPE", 

213 Name='Summer Design Day from Weather File', 

214 Period_Selection=f'Summer{period_selection}', 

215 Day_of_Week_for_Start_Day='SummerDesignDay' 

216 ) 

217 idf.newidfobject("SIZINGPERIOD:WEATHERFILECONDITIONTYPE", 

218 Name='Winter Design Day from Weather File', 

219 Period_Selection=f'Winter{period_selection}', 

220 Day_of_Week_for_Start_Day='WinterDesignDay' 

221 ) 

222 else: 

223 # use default Design day (July 21, December 21) for system sizing 

224 idf.newidfobject("SIZINGPERIOD:WEATHERFILEDAYS", 

225 Name='Summer Design Day from Weather File', 

226 Begin_Month=7, 

227 Begin_Day_of_Month=21, 

228 End_Month=7, 

229 End_Day_of_Month=21, 

230 Day_of_Week_for_Start_Day='SummerDesignDay' 

231 ) 

232 idf.newidfobject("SIZINGPERIOD:WEATHERFILEDAYS", 

233 Name='Winter Design Day from Weather File', 

234 Begin_Month=12, 

235 Begin_Day_of_Month=21, 

236 End_Month=12, 

237 End_Day_of_Month=21, 

238 Day_of_Week_for_Start_Day='WinterDesignDay' 

239 ) 

240 

241 sim_results_path = paths.export/'EnergyPlus'/'SimResults' 

242 export_path = sim_results_path / ifc_name 

243 if not os.path.exists(export_path): 

244 os.makedirs(export_path) 

245 idf.idfname = export_path / str(ifc_name + '.idf') 

246 # load and set basic compact schedules and ScheduleTypeLimits 

247 schedules_idf = IDF(plugin_ep_path + '/data/Schedules.idf') 

248 schedules = schedules_idf.idfobjects["Schedule:Compact".upper()] 

249 sch_typelim = schedules_idf.idfobjects["ScheduleTypeLimits".upper()] 

250 for s in schedules: 

251 idf.copyidfobject(s) 

252 for t in sch_typelim: 

253 idf.copyidfobject(t) 

254 # set weather file 

255 idf.epw = str(weather_file) 

256 return idf, sim_results_path 

257 

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

259 idf: IDF): 

260 """Initialize zone settings. 

261 

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

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

264 equipment, lighting). 

265 

266 Args: 

267 sim_settings: BIM2SIM simulation settings 

268 elements: dict[guid: element] 

269 idf: idf file object 

270 """ 

271 logger.info("Init thermal zones ...") 

272 spaces = get_spaces_with_bounds(elements) 

273 for space in spaces: 

274 if space.space_shape_volume: 

275 volume = space.space_shape_volume.to(ureg.meter ** 3).m 

276 # for some shapes, shape volume calculation might not work 

277 else: 

278 volume = space.volume.to(ureg.meter ** 3).m 

279 zone = idf.newidfobject( 

280 'ZONE', 

281 Name=space.ifc.GlobalId, 

282 Volume=volume 

283 ) 

284 self.set_heating_and_cooling(idf, zone_name=zone.Name, space=space) 

285 self.set_infiltration( 

286 idf, name=zone.Name, zone_name=zone.Name, space=space, 

287 ep_version=sim_settings.ep_version) 

288 if (not space.with_cooling and 

289 self.playground.sim_settings.add_natural_ventilation): 

290 self.set_natural_ventilation( 

291 idf, name=zone.Name, zone_name=zone.Name, space=space, 

292 ep_version=sim_settings.ep_version, 

293 residential=sim_settings.residential, 

294 ventilation_method= 

295 sim_settings.natural_ventilation_approach) 

296 self.set_people(sim_settings, idf, name=zone.Name, 

297 zone_name=zone.Name, space=space) 

298 self.set_equipment(sim_settings, idf, name=zone.Name, 

299 zone_name=zone.Name, space=space) 

300 self.set_lights(sim_settings, idf, name=zone.Name, zone_name=zone.Name, 

301 space=space) 

302 if sim_settings.building_rotation_overwrite != 0: 

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

304 sim_settings.building_rotation_overwrite) 

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

306 'Relative' 

307 

308 @staticmethod 

309 def init_zonelist( 

310 idf: IDF, 

311 name: str = None, 

312 zones_in_list: list[str] = None): 

313 """Initialize zone lists. 

314 

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

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

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

318 in an idf). 

319 

320 Args: 

321 idf: idf file object 

322 name: str with name of zone list 

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

324 the list 

325 """ 

326 if zones_in_list is None: 

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

328 # is larger than 20. 

329 idf_zones = idf.idfobjects["ZONE"] 

330 if len(idf_zones) > 99: 

331 logger.warning("Trying to assign more than 99 zones to a " 

332 "single zone list. May require changes in " 

333 "Energy+.idd to successfully execute " 

334 "simulation.") 

335 else: 

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

337 # zones_in_list to the zonelist. 

338 all_idf_zones = idf.idfobjects["ZONE"] 

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

340 in zones_in_list] 

341 if len(idf_zones) > 99: 

342 logger.warning("Trying to assign more than 99 zones to a " 

343 "single zone list. May require changes in " 

344 "Energy+.idd to successfully execute " 

345 "simulation.") 

346 if len(idf_zones) == 0: 

347 return 

348 if name is None: 

349 name = "All_Zones" 

350 zs = {} 

351 for i, z in enumerate(idf_zones): 

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

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

354 

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

356 """Assign one zonegroup per storey. 

357 

358 Args: 

359 elements: dict[guid: element] 

360 idf: idf file object 

361 """ 

362 spaces = get_spaces_with_bounds(elements) 

363 space_usage_dict = {} 

364 for space in spaces: 

365 if space.usage.replace(',', '') not in space_usage_dict.keys(): 

366 space_usage_dict.update({space.usage.replace(',', 

367 ''): [space.guid]}) 

368 else: 

369 space_usage_dict[space.usage.replace(',', '')].append( 

370 space.guid) 

371 

372 for key, value in space_usage_dict.items(): 

373 if not idf.getobject('ZONELIST', key): 

374 self.init_zonelist(idf, name=key, zones_in_list=value) 

375 # add zonelist for All_Zones 

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

377 if zlist.Name != "All_Zones"] 

378 

379 # add zonegroup for each zonegroup in zone_lists. 

380 for zlist in zone_lists: 

381 idf.newidfobject("ZONEGROUP", 

382 Name=zlist.Name, 

383 Zone_List_Name=zlist.Name, 

384 Zone_List_Multiplier=1 

385 ) 

386 @staticmethod 

387 def check_preprocessed_materials_and_constructions(rel_elem, layers): 

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

389 correct_preprocessing = False 

390 # check if thickness and material parameters are available from 

391 # preprocessing 

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

393 for layer in rel_elem.layerset.layers: 

394 if None in (layer.material.thermal_conduc, 

395 layer.material.spec_heat_capacity, 

396 layer.material.density): 

397 return correct_preprocessing 

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

399 layer.material.spec_heat_capacity.m, 

400 layer.material.density.m): 

401 return correct_preprocessing 

402 else: 

403 pass 

404 

405 correct_preprocessing = True 

406 

407 return correct_preprocessing 

408 

409 def get_preprocessed_materials_and_constructions( 

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

411 """Get preprocessed materials and constructions. 

412 

413 This function sets preprocessed construction and material for 

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

415 boundary construction is set. 

416 

417 Args: 

418 sim_settings: BIM2SIM simulation settings 

419 elements: dict[guid: element] 

420 idf: idf file object 

421 """ 

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

423 bounds = filter_elements(elements, 'SpaceBoundary') 

424 for bound in bounds: 

425 rel_elem = bound.bound_element 

426 if not rel_elem: 

427 continue 

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

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

430 # set construction for all but fenestration 

431 if self.check_preprocessed_materials_and_constructions( 

432 rel_elem, rel_elem.layerset.layers): 

433 self.set_preprocessed_construction_elem( 

434 rel_elem, rel_elem.layerset.layers, idf) 

435 for layer in rel_elem.layerset.layers: 

436 self.set_preprocessed_material_elem(layer, idf) 

437 else: 

438 logger.warning("No preprocessed construction and " 

439 "material found for space boundary %s on " 

440 "related building element %s. Using " 

441 "default values instead.", 

442 bound.guid, rel_elem.guid) 

443 else: 

444 # set construction elements for windows 

445 self.set_preprocessed_window_material_elem( 

446 rel_elem, idf, sim_settings.add_window_shading) 

447 

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

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

450 idf.newidfobject("CONSTRUCTION:AIRBOUNDARY", 

451 Name='Air Wall', 

452 Solar_and_Daylighting_Method='GroupedZones', 

453 Radiant_Exchange_Method='GroupedZones', 

454 Air_Exchange_Method='SimpleMixing', 

455 Simple_Mixing_Air_Changes_per_Hour=0.5, 

456 ) 

457 else: 

458 idf.newidfobject("CONSTRUCTION:AIRBOUNDARY", 

459 Name='Air Wall', 

460 Air_Exchange_Method='SimpleMixing', 

461 Simple_Mixing_Air_Changes_per_Hour=0.5, 

462 ) 

463 

464 @staticmethod 

465 def set_preprocessed_construction_elem( 

466 rel_elem: IFCBased, 

467 layers: list[Layer], 

468 idf: IDF): 

469 """Write preprocessed constructions to idf. 

470 

471 This function uses preprocessed data to define idf construction 

472 elements. 

473 

474 Args: 

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

476 layers: list of Layer 

477 idf: idf file object 

478 """ 

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

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

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

482 # todo: find a unique key for construction name 

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

484 outer_layer = layers[-1] 

485 other_layer_list = layers[:-1] 

486 other_layer_list.reverse() 

487 other_layers = {} 

488 for i, l in enumerate(other_layer_list): 

489 other_layers.update( 

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

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

492 idf.newidfobject("CONSTRUCTION", 

493 Name=construction_name, 

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

495 str(outer_layer.thickness.to( 

496 ureg.metre).m), 

497 **other_layers 

498 ) 

499 

500 @staticmethod 

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

502 """Set a preprocessed material element. 

503 

504 Args: 

505 layer: Layer Instance 

506 idf: idf file object 

507 """ 

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

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

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

511 return 

512 specific_heat = \ 

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

514 ureg.kilogram).m 

515 if specific_heat < 100: 

516 specific_heat = 100 

517 conductivity = layer.material.thermal_conduc.to( 

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

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

520 if conductivity == 0: 

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

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

523 f"file.") 

524 if density == 0: 

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

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

527 f"file.") 

528 idf.newidfobject("MATERIAL", 

529 Name=material_name, 

530 Roughness="MediumRough", 

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

532 Conductivity=conductivity, 

533 Density=density, 

534 Specific_Heat=specific_heat 

535 ) 

536 

537 @staticmethod 

538 def set_preprocessed_window_material_elem(rel_elem: Window, 

539 idf: IDF, 

540 add_window_shading: False): 

541 """Set preprocessed window material. 

542 

543 This function constructs windows with a 

544 WindowMaterial:SimpleGlazingSystem consisting of the outermost layer 

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

546 be extended to hold multilayer window constructions. 

547 

548 Args: 

549 rel_elem: Window instance 

550 idf: idf file object 

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

552 'Interior', 'Exterior') 

553 """ 

554 material_name = \ 

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

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

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

558 return 

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

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

561 ureg.meter ** 2).m 

562 + 0.13) 

563 else: 

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

565 ureg.metre).m / 

566 rel_elem.layerset.layers[ 

567 0].material.thermal_conduc.to( 

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

569 0.13) 

570 if rel_elem.g_value >= 1: 

571 old_g_value = rel_elem.g_value 

572 rel_elem.g_value = 0.999 

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

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

575 old_g_value, rel_elem.g_value) 

576 

577 idf.newidfobject("WINDOWMATERIAL:SIMPLEGLAZINGSYSTEM", 

578 Name=material_name, 

579 UFactor=ufactor, 

580 Solar_Heat_Gain_Coefficient=rel_elem.g_value, 

581 # Visible_Transmittance=0.8 # optional 

582 ) 

583 if add_window_shading: 

584 default_shading_name = "DefaultWindowShade" 

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

586 idf.newidfobject("WINDOWMATERIAL:SHADE", 

587 Name=default_shading_name, 

588 Solar_Transmittance=0.3, 

589 Solar_Reflectance=0.5, 

590 Visible_Transmittance=0.3, 

591 Visible_Reflectance=0.5, 

592 Infrared_Hemispherical_Emissivity=0.9, 

593 Infrared_Transmittance=0.05, 

594 Thickness=0.003, 

595 Conductivity=0.1) 

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

597 + add_window_shading 

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

599 if add_window_shading == 'Interior': 

600 idf.newidfobject("CONSTRUCTION", 

601 Name=construction_name, 

602 Outside_Layer=material_name, 

603 Layer_2=default_shading_name 

604 ) 

605 else: 

606 idf.newidfobject("CONSTRUCTION", 

607 Name=construction_name, 

608 Outside_Layer=default_shading_name, 

609 Layer_2=material_name 

610 ) 

611 # todo: enable use of multilayer windows 

612 # set construction without shading anyways 

613 construction_name = 'Window_' + material_name 

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

615 idf.newidfobject("CONSTRUCTION", 

616 Name=construction_name, 

617 Outside_Layer=material_name 

618 ) 

619 

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

621 space: ThermalZone): 

622 """Set heating and cooling parameters. 

623 

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

625 available from BIM2SIM Preprocessing (either IFC-based or 

626 Template-based). 

627 

628 Args: 

629 idf: idf file object 

630 zone_name: str 

631 space: ThermalZone instance 

632 """ 

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

634 if self.playground.sim_settings.control_operative_temperature: 

635 # set control for operative temperature instead of air temperature 

636 operative_stats_name = space.usage.replace(',', '') + ' THERMOSTAT' 

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

638 ',', '') 

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

640 'heating_profile', 

641 htg_schedule_name) 

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

643 ',', '') 

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

645 'cooling_profile', 

646 clg_schedule_name) 

647 if idf.getobject('THERMOSTATSETPOINT:DUALSETPOINT', 

648 htg_schedule_name + ' ' + clg_schedule_name) is None: 

649 idf.newidfobject( 

650 'THERMOSTATSETPOINT:DUALSETPOINT', 

651 Name=htg_schedule_name + ' ' + clg_schedule_name, 

652 Heating_Setpoint_Temperature_Schedule_Name 

653 =htg_schedule_name, 

654 Cooling_Setpoint_Temperature_Schedule_Name=clg_schedule_name 

655 ) 

656 if idf.getobject("ZONECONTROL:THERMOSTAT:OPERATIVETEMPERATURE", 

657 zone_name + ' ' + operative_stats_name) is None: 

658 idf.newidfobject( 

659 "ZONECONTROL:THERMOSTAT:OPERATIVETEMPERATURE", 

660 Thermostat_Name=zone_name + ' ' + operative_stats_name, 

661 Radiative_Fraction_Input_Mode='constant', 

662 Fixed_Radiative_Fraction=0.5 

663 ) 

664 if idf.getobject("ZONECONTROL:THERMOSTAT", 

665 operative_stats_name) is None: 

666 stat = idf.newidfobject( 

667 "ZONECONTROL:THERMOSTAT", 

668 Name=operative_stats_name, 

669 Zone_or_ZoneList_Name=space.usage.replace(',', ''), 

670 Control_Type_Schedule_Name='Zone Control Type Sched', 

671 Control_1_Object_Type='ThermostatSetpoint:DualSetpoint', 

672 Control_1_Name=htg_schedule_name + ' ' + clg_schedule_name, 

673 # Temperature_Difference_Between_Cutout_And_Setpoint=0.2, 

674 # temperature difference disables operative control 

675 ) 

676 else: 

677 stat = idf.getobject('ZONECONTROL:THERMOSTAT', 

678 operative_stats_name) 

679 if idf.getobject("SCHEDULE:COMPACT", 

680 'Zone Control Type Sched') is None: 

681 idf.newidfobject("SCHEDULE:COMPACT", 

682 Name='Zone Control Type Sched', 

683 Schedule_Type_Limits_Name='Control Type', 

684 Field_1='Through: 12/31', 

685 Field_2='For: AllDays', 

686 Field_3='Until: 24:00', 

687 Field_4='4', 

688 ) 

689 if idf.getobject("SCHEDULETYPELIMITS", 'Control Type') is None: 

690 idf.newidfobject('SCHEDULETYPELIMITS', 

691 Name='Control Type', 

692 Lower_Limit_Value=0, 

693 Upper_Limit_Value=4, 

694 Numeric_Type='DISCRETE') 

695 template_thermostat_name = '' 

696 elif idf.getobject("HVACTEMPLATE:THERMOSTAT", stat_name) is None: 

697 # if air temperature is controlled, create thermostat if it is 

698 # not available 

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

700 template_thermostat_name = stat.Name 

701 else: 

702 # assign available thermostats for air control 

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

704 template_thermostat_name = stat.Name 

705 

706 # initialize heating and cooling availability, and capacity 

707 cooling_availability = "Off" 

708 heating_availability = "Off" 

709 heating_limit = 'NoLimit' 

710 cooling_limit = 'NoLimit' 

711 maximum_cooling_capacity = 'autosize' 

712 maximum_cooling_rate = 'autosize' 

713 outdoor_air_method = 'None' 

714 # convert from l/s to m3/s 

715 outdoor_air_person = ( 

716 self.playground.sim_settings.outdoor_air_per_person) / 1000 

717 outdoor_air_area = ( 

718 self.playground.sim_settings.outdoor_air_per_area) / 1000 

719 ventilation_demand_control = 'None' 

720 outdoor_air_economizer = 'NoEconomizer' 

721 heat_recovery_type = 'None' 

722 heat_recovery_sensible = ( 

723 self.playground.sim_settings.heat_recovery_sensible) 

724 heat_recovery_latent = self.playground.sim_settings.heat_recovery_latent 

725 

726 # initialize night setback if required 

727 if self.playground.sim_settings.hvac_off_at_night and idf.getobject( 

728 "SCHEDULE:COMPACT", "On_except_10pm_to_6am") is None: 

729 idf.newidfobject( 

730 "SCHEDULE:COMPACT", 

731 Name='On_except_10pm_to_6am', 

732 Schedule_Type_Limits_Name='on/off', 

733 Field_1='Through: 12/31', 

734 Field_2='For: AllDays', 

735 Field_3='Until: 06:00', 

736 Field_4='0', 

737 Field_5='Until: 22:00', 

738 Field_6='1', 

739 Field_7='Until: 24:00', 

740 Field_8='0' 

741 ) 

742 

743 # overwrite heating / cooling availability if required 

744 if space.with_cooling: 

745 cooling_availability = "On" 

746 if self.playground.sim_settings.hvac_off_at_night: 

747 cooling_availability = 'On_except_10pm_to_6am' 

748 cooling_limit = 'LimitFlowRateAndCapacity' 

749 

750 if space.with_heating: 

751 heating_availability = "On" 

752 if self.playground.sim_settings.hvac_off_at_night: 

753 heating_availability = 'On_except_10pm_to_6am' 

754 

755 if space.with_cooling or \ 

756 not self.playground.sim_settings.add_natural_ventilation: 

757 outdoor_air_method = 'Sum' 

758 ventilation_demand_control = ( 

759 str(self.playground.sim_settings.ventilation_demand_control)) 

760 outdoor_air_economizer = ( 

761 self.playground.sim_settings.outdoor_air_economizer) 

762 heat_recovery_type = self.playground.sim_settings.heat_recovery_type 

763 

764 # initialize ideal loads air system according to the settings 

765 idf.newidfobject( 

766 "HVACTEMPLATE:ZONE:IDEALLOADSAIRSYSTEM", 

767 Zone_Name=zone_name, 

768 Template_Thermostat_Name=template_thermostat_name, 

769 Heating_Availability_Schedule_Name=heating_availability, 

770 Cooling_Availability_Schedule_Name=cooling_availability, 

771 Heating_Limit=heating_limit, 

772 Cooling_Limit=cooling_limit, 

773 Maximum_Cooling_Air_Flow_Rate=maximum_cooling_rate, 

774 Maximum_Total_Cooling_Capacity=maximum_cooling_capacity, 

775 Outdoor_Air_Method=outdoor_air_method, 

776 Outdoor_Air_Flow_Rate_per_Person=outdoor_air_person, 

777 Outdoor_Air_Flow_Rate_per_Zone_Floor_Area=outdoor_air_area, 

778 Demand_Controlled_Ventilation_Type=ventilation_demand_control, 

779 Outdoor_Air_Economizer_Type=outdoor_air_economizer, 

780 Heat_Recovery_Type=heat_recovery_type, 

781 Sensible_Heat_Recovery_Effectiveness=heat_recovery_sensible, 

782 Latent_Heat_Recovery_Effectiveness=heat_recovery_latent, 

783 ) 

784 

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

786 zone_name: str, space: ThermalZone): 

787 """Set occupancy schedules. 

788 

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

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

791 available or on templates. 

792 

793 Args: 

794 sim_settings: BIM2SIM simulation settings 

795 idf: idf file object 

796 name: name of the new people idf object 

797 zone_name: name of zone or zone_list 

798 space: ThermalZone instance 

799 """ 

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

801 profile_name = 'persons_profile' 

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

803 profile_name, schedule_name) 

804 # set default activity schedule 

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

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

807 activity_schedule_name = "Schedule Activity " + str( 

808 space.fixed_heat_flow_rate_persons) 

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

810 idf.newidfobject("SCHEDULE:COMPACT", 

811 Name=activity_schedule_name, 

812 Schedule_Type_Limits_Name="Any Number", 

813 Field_1="Through: 12/31", 

814 Field_2="For: Alldays", 

815 Field_3="Until: 24:00", 

816 Field_4=space.fixed_heat_flow_rate_persons.to( 

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

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

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

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

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

822 idf.newidfobject( 

823 "PEOPLE", 

824 Name=name, 

825 Zone_or_ZoneList_Name=zone_name, 

826 Number_of_People_Calculation_Method="People/Area", 

827 People_per_Zone_Floor_Area=space.persons, 

828 Activity_Level_Schedule_Name=activity_schedule_name, 

829 Number_of_People_Schedule_Name=schedule_name, 

830 Fraction_Radiant=space.ratio_conv_rad_persons 

831 ) 

832 else: 

833 idf.newidfobject( 

834 "PEOPLE", 

835 Name=name, 

836 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name, 

837 Number_of_People_Calculation_Method="People/Area", 

838 People_per_Floor_Area=space.persons, 

839 Activity_Level_Schedule_Name=activity_schedule_name, 

840 Number_of_People_Schedule_Name=schedule_name, 

841 Fraction_Radiant=space.ratio_conv_rad_persons 

842 ) 

843 

844 @staticmethod 

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

846 profile_name: str, 

847 schedule_name: str): 

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

849 

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

851 

852 Args: 

853 idf: idf file object 

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

855 temperatures, loads) 

856 profile_name: string 

857 schedule_name: str 

858 """ 

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

860 limits_name = 'Fraction' 

861 hours = {} 

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

863 limits_name = 'Temperature' 

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

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

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

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

868 # convert Kelvin to Celsius for EnergyPlus Export 

869 if schedule[i] > 270: 

870 schedule[i] = schedule[i] - 273.15 

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

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

873 Schedule_Type_Limits_Name=limits_name, **hours) 

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

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

876 DayType_List_1="AllDays", 

877 ScheduleDay_Name_1=schedule_name) 

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

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

880 Schedule_Type_Limits_Name=limits_name, 

881 ScheduleWeek_Name_1=schedule_name, 

882 Start_Month_1=1, 

883 Start_Day_1=1, 

884 End_Month_1=12, 

885 End_Day_1=31) 

886 

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

888 name: str, zone_name: str, 

889 space: ThermalZone): 

890 """Set internal loads from equipment. 

891 

892 This function sets schedules and internal loads from equipment based 

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

894 templates. 

895 

896 Args: 

897 sim_settings: BIM2SIM simulation settings 

898 idf: idf file object 

899 name: name of the new people idf object 

900 zone_name: name of zone or zone_list 

901 space: ThermalZone instance 

902 """ 

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

904 '') 

905 profile_name = 'machines_profile' 

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

907 profile_name, schedule_name) 

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

909 idf.newidfobject( 

910 "ELECTRICEQUIPMENT", 

911 Name=name, 

912 Zone_or_ZoneList_Name=zone_name, 

913 Schedule_Name=schedule_name, 

914 Design_Level_Calculation_Method="Watts/Area", 

915 Watts_per_Zone_Floor_Area=space.machines.to( 

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

917 ) 

918 else: 

919 idf.newidfobject( 

920 "ELECTRICEQUIPMENT", 

921 Name=name, 

922 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name, 

923 Schedule_Name=schedule_name, 

924 Design_Level_Calculation_Method="Watts/Area", 

925 Watts_per_Zone_Floor_Area=space.machines.to( 

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

927 ) 

928 

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

930 zone_name: str, space: ThermalZone): 

931 """Set internal loads from lighting. 

932 

933 This function sets schedules and lighting based on the 

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

935 templates. 

936 

937 Args: 

938 sim_settings: BIM2SIM simulation settings 

939 idf: idf file object 

940 name: name of the new people idf object 

941 zone_name: name of zone or zone_list 

942 space: ThermalZone instance 

943 """ 

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

945 profile_name = 'lighting_profile' 

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

947 profile_name, schedule_name) 

948 mode = "Watts/Area" 

949 watts_per_zone_floor_area = space.lighting_power.to( 

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

951 return_air_fraction = 0.0 

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

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

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

955 # Data. Maybe set by user-input later 

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

957 idf.newidfobject( 

958 "LIGHTS", 

959 Name=name, 

960 Zone_or_ZoneList_Name=zone_name, 

961 Schedule_Name=schedule_name, 

962 Design_Level_Calculation_Method=mode, 

963 Watts_per_Zone_Floor_Area=watts_per_zone_floor_area, 

964 Return_Air_Fraction=return_air_fraction, 

965 Fraction_Radiant=fraction_radiant, 

966 Fraction_Visible=fraction_visible 

967 ) 

968 else: 

969 idf.newidfobject( 

970 "LIGHTS", 

971 Name=name, 

972 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name, 

973 Schedule_Name=schedule_name, 

974 Design_Level_Calculation_Method=mode, 

975 Watts_per_Zone_Floor_Area=watts_per_zone_floor_area, 

976 Return_Air_Fraction=return_air_fraction, 

977 Fraction_Radiant=fraction_radiant, 

978 Fraction_Visible=fraction_visible 

979 ) 

980 

981 @staticmethod 

982 def set_infiltration(idf: IDF, 

983 name: str, zone_name: str, 

984 space: ThermalZone, ep_version: str): 

985 """Set infiltration rate. 

986 

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

988 BIM2SIM preprocessing values (IFC-based if available or 

989 template-based). 

990 

991 Args: 

992 idf: idf file object 

993 name: name of the new people idf object 

994 zone_name: name of zone or zone_list 

995 space: ThermalZone instance 

996 ep_version: Used version of EnergyPlus 

997 """ 

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

999 idf.newidfobject( 

1000 "ZONEINFILTRATION:DESIGNFLOWRATE", 

1001 Name=name, 

1002 Zone_or_ZoneList_Name=zone_name, 

1003 Schedule_Name="Continuous", 

1004 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

1005 Air_Changes_per_Hour=space.base_infiltration 

1006 ) 

1007 else: 

1008 idf.newidfobject( 

1009 "ZONEINFILTRATION:DESIGNFLOWRATE", 

1010 Name=name, 

1011 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name, 

1012 Schedule_Name="Continuous", 

1013 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

1014 Air_Changes_per_Hour=space.base_infiltration 

1015 ) 

1016 

1017 @staticmethod 

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

1019 space: ThermalZone, ep_version, 

1020 min_in_temp=22, residential=False, 

1021 ventilation_method="Simple"): 

1022 """Set natural ventilation. 

1023 

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

1025 BIM2SIM preprocessing values (IFC-based if available or 

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

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

1028 and maximum outdoor temperature if applicable. 

1029 

1030 Args: 

1031 idf: idf file object 

1032 name: name of the new people idf object 

1033 zone_name: name of zone or zone_list 

1034 space: ThermalZone instance 

1035 ep_version: Used version of EnergyPlus 

1036 

1037 """ 

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

1039 if ventilation_method == 'DIN4108': 

1040 space_volume = space.space_shape_volume.m 

1041 space_area = space.net_area.m 

1042 if residential: 

1043 occupied_air_exchange = 0.5 

1044 unoccupied_air_exchange = 0.5 

1045 occupied_increased_exchange = 3 

1046 unoccupied_increased_exchange = 2 

1047 else: 

1048 occupied_air_exchange = 4 * space_area/space_volume 

1049 unoccupied_air_exchange = 0.24 

1050 occupied_increased_exchange = 3 

1051 unoccupied_increased_exchange = 2 

1052 

1053 if residential: 

1054 occupied_schedule_name = 'residential_occupied_hours' 

1055 unoccupied_schedule_name = 'residential_unoccupied_hours' 

1056 if idf.getobject("SCHEDULE:COMPACT", 

1057 occupied_schedule_name) is None: 

1058 idf.newidfobject("SCHEDULE:COMPACT", 

1059 Name=occupied_schedule_name, 

1060 Schedule_Type_Limits_Name='Continuous', 

1061 Field_1='Through: 12/31', 

1062 Field_2='For: AllDays', 

1063 Field_3='Until: 6:00', 

1064 Field_4='0', 

1065 Field_5='Until: 23:00', 

1066 Field_6='1', 

1067 Field_7='Until: 24:00', 

1068 Field_8='0', 

1069 ) 

1070 if idf.getobject("SCHEDULE:COMPACT", 

1071 unoccupied_schedule_name) is None: 

1072 idf.newidfobject("SCHEDULE:COMPACT", 

1073 Name=unoccupied_schedule_name, 

1074 Schedule_Type_Limits_Name='Continuous', 

1075 Field_1='Through: 12/31', 

1076 Field_2='For: AllDays', 

1077 Field_3='Until: 6:00', 

1078 Field_4='1', 

1079 Field_5='Until: 23:00', 

1080 Field_6='0', 

1081 Field_7='Until: 24:00', 

1082 Field_8='1', 

1083 ) 

1084 else: 

1085 occupied_schedule_name = 'non_residential_occupied_hours' 

1086 unoccupied_schedule_name = 'non_residential_unoccupied_hours' 

1087 if idf.getobject("SCHEDULE:COMPACT", 

1088 occupied_schedule_name) is None: 

1089 idf.newidfobject("SCHEDULE:COMPACT", 

1090 Name=occupied_schedule_name, 

1091 Schedule_Type_Limits_Name='Continuous', 

1092 Field_1='Through: 12/31', 

1093 Field_2='For: AllDays', 

1094 Field_3='Until: 7:00', 

1095 Field_4='0', 

1096 Field_5='Until: 18:00', 

1097 Field_6='1', 

1098 Field_7='Until: 24:00', 

1099 Field_8='0', 

1100 ) 

1101 if idf.getobject("SCHEDULE:COMPACT", 

1102 unoccupied_schedule_name) is None: 

1103 idf.newidfobject("SCHEDULE:COMPACT", 

1104 Name=unoccupied_schedule_name, 

1105 Schedule_Type_Limits_Name='Continuous', 

1106 Field_1='Through: 12/31', 

1107 Field_2='For: AllDays', 

1108 Field_3='Until: 7:00', 

1109 Field_4='1', 

1110 Field_5='Until: 18:00', 

1111 Field_6='0', 

1112 Field_7='Until: 24:00', 

1113 Field_8='1', 

1114 ) 

1115 idf.newidfobject( 

1116 "ZONEVENTILATION:DESIGNFLOWRATE", 

1117 Name=name + '_occupied_ventilation', 

1118 Zone_or_ZoneList_Name=zone_name, 

1119 Schedule_Name=occupied_schedule_name, 

1120 Ventilation_Type="Natural", 

1121 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

1122 Air_Changes_per_Hour=occupied_air_exchange, 

1123 ) 

1124 idf.newidfobject( 

1125 "ZONEVENTILATION:DESIGNFLOWRATE", 

1126 Name=name + '_occupied_ventilation_increased', 

1127 Zone_or_ZoneList_Name=zone_name, 

1128 Schedule_Name=occupied_schedule_name, 

1129 Ventilation_Type="Natural", 

1130 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

1131 Air_Changes_per_Hour=max( 

1132 occupied_increased_exchange-occupied_air_exchange, 0), 

1133 Minimum_Indoor_Temperature=23, 

1134 Maximum_Indoor_Temperature=26, 

1135 Minimum_Outdoor_Temperature=12, 

1136 Delta_Temperature=0, 

1137 ) 

1138 idf.newidfobject( 

1139 "ZONEVENTILATION:DESIGNFLOWRATE", 

1140 Name=name + '_occupied_ventilation_increased_overheating', 

1141 Zone_or_ZoneList_Name=zone_name, 

1142 Schedule_Name=occupied_schedule_name, 

1143 Ventilation_Type="Natural", 

1144 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

1145 Air_Changes_per_Hour=max( 

1146 occupied_increased_exchange-occupied_air_exchange, 0), 

1147 Minimum_Indoor_Temperature=26, 

1148 Delta_Temperature=0, 

1149 ) 

1150 idf.newidfobject( 

1151 "ZONEVENTILATION:DESIGNFLOWRATE", 

1152 Name=name + '_unoccupied_ventilation', 

1153 Zone_or_ZoneList_Name=zone_name, 

1154 Schedule_Name=unoccupied_schedule_name, 

1155 Ventilation_Type="Natural", 

1156 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

1157 Air_Changes_per_Hour=unoccupied_air_exchange, 

1158 ) 

1159 idf.newidfobject( 

1160 "ZONEVENTILATION:DESIGNFLOWRATE", 

1161 Name=name + '_unoccupied_ventilation_increased', 

1162 Zone_or_ZoneList_Name=zone_name, 

1163 Schedule_Name=unoccupied_schedule_name, 

1164 Ventilation_Type="Natural", 

1165 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

1166 Air_Changes_per_Hour=max( 

1167 unoccupied_increased_exchange-unoccupied_air_exchange, 0), 

1168 Minimum_Indoor_Temperature=23, 

1169 Maximum_Indoor_Temperature=26, 

1170 Minimum_Outdoor_Temperature=15, 

1171 Delta_Temperature=0, 

1172 ) 

1173 idf.newidfobject( 

1174 "ZONEVENTILATION:DESIGNFLOWRATE", 

1175 Name=name + '_unoccupied_ventilation_increased_overheating', 

1176 Zone_or_ZoneList_Name=zone_name, 

1177 Schedule_Name=unoccupied_schedule_name, 

1178 Ventilation_Type="Natural", 

1179 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

1180 Air_Changes_per_Hour=max( 

1181 unoccupied_increased_exchange-unoccupied_air_exchange, 0), 

1182 Minimum_Indoor_Temperature=26, 

1183 # Minimum_Outdoor_Temperature=10, 

1184 Delta_Temperature=0, 

1185 ) 

1186 else: 

1187 # use bim2sim standard zone ventilation based on TEASER 

1188 # templates 

1189 idf.newidfobject( 

1190 "ZONEVENTILATION:DESIGNFLOWRATE", 

1191 Name=name + '_winter', 

1192 Zone_or_ZoneList_Name=zone_name, 

1193 Schedule_Name="Continuous", 

1194 Ventilation_Type="Natural", 

1195 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

1196 Air_Changes_per_Hour=space.winter_reduction_infiltration[0], 

1197 Minimum_Outdoor_Temperature= 

1198 space.winter_reduction_infiltration[1] - 273.15, 

1199 Maximum_Outdoor_Temperature= 

1200 space.winter_reduction_infiltration[2] - 273.15, 

1201 ) 

1202 

1203 idf.newidfobject( 

1204 "ZONEVENTILATION:DESIGNFLOWRATE", 

1205 Name=name + '_summer', 

1206 Zone_or_ZoneList_Name=zone_name, 

1207 Schedule_Name="Continuous", 

1208 Ventilation_Type="Natural", 

1209 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

1210 Air_Changes_per_Hour=space.max_summer_infiltration[0], 

1211 Minimum_Outdoor_Temperature 

1212 =space.max_summer_infiltration[1] - 273.15, 

1213 Maximum_Outdoor_Temperature 

1214 =space.max_summer_infiltration[2] - 273.15, 

1215 ) 

1216 

1217 idf.newidfobject( 

1218 "ZONEVENTILATION:DESIGNFLOWRATE", 

1219 Name=name + '_overheating', 

1220 Zone_or_ZoneList_Name=zone_name, 

1221 Schedule_Name="Continuous", 

1222 Ventilation_Type="Natural", 

1223 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

1224 # calculation of overheating infiltration is a simplification 

1225 # compared to the corresponding TEASER implementation which 

1226 # dynamically computes thresholds for overheating infiltration 

1227 # based on the zone temperature and additional factors. 

1228 Air_Changes_per_Hour=space.max_overheating_infiltration[0], 

1229 Minimum_Outdoor_Temperature 

1230 =space.max_summer_infiltration[2] - 273.15, 

1231 ) 

1232 else: 

1233 # use bim2sim standard zone ventilation based on TEASER templates 

1234 idf.newidfobject( 

1235 "ZONEVENTILATION:DESIGNFLOWRATE", 

1236 Name=name + '_winter', 

1237 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name, 

1238 Schedule_Name="Continuous", 

1239 Ventilation_Type="Natural", 

1240 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

1241 Air_Changes_per_Hour=space.winter_reduction_infiltration[0], 

1242 Minimum_Outdoor_Temperature= 

1243 space.winter_reduction_infiltration[1] - 273.15, 

1244 Maximum_Outdoor_Temperature= 

1245 space.winter_reduction_infiltration[2] - 273.15, 

1246 ) 

1247 

1248 idf.newidfobject( 

1249 "ZONEVENTILATION:DESIGNFLOWRATE", 

1250 Name=name + '_summer', 

1251 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name, 

1252 Schedule_Name="Continuous", 

1253 Ventilation_Type="Natural", 

1254 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

1255 Air_Changes_per_Hour=space.max_summer_infiltration[0], 

1256 Minimum_Outdoor_Temperature 

1257 =space.max_summer_infiltration[1] - 273.15, 

1258 Maximum_Outdoor_Temperature 

1259 =space.max_summer_infiltration[2] - 273.15, 

1260 ) 

1261 

1262 idf.newidfobject( 

1263 "ZONEVENTILATION:DESIGNFLOWRATE", 

1264 Name=name + '_overheating', 

1265 Zone_or_ZoneList_or_Space_or_SpaceList_Name=zone_name, 

1266 Schedule_Name="Continuous", 

1267 Ventilation_Type="Natural", 

1268 Design_Flow_Rate_Calculation_Method="AirChanges/Hour", 

1269 # calculation of overheating infiltration is a simplification 

1270 # compared to the corresponding TEASER implementation which 

1271 # dynamically computes thresholds for overheating infiltration 

1272 # based on the zone temperature and additional factors. 

1273 Air_Changes_per_Hour=space.max_overheating_infiltration[0], 

1274 Minimum_Outdoor_Temperature 

1275 =space.max_summer_infiltration[2] - 273.15, 

1276 ) 

1277 

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

1279 """Set 24 hour hvac template. 

1280 

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

1282 cooling. 

1283 

1284 Args: 

1285 idf: idf file object 

1286 space: ThermalZone 

1287 name: IDF Thermostat Name 

1288 """ 

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

1290 ',', '') 

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

1292 'heating_profile', 

1293 htg_schedule_name) 

1294 

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

1296 ',', '') 

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

1298 'cooling_profile', 

1299 clg_schedule_name) 

1300 stat = idf.newidfobject( 

1301 "HVACTEMPLATE:THERMOSTAT", 

1302 Name=name, 

1303 Heating_Setpoint_Schedule_Name=htg_schedule_name, 

1304 Cooling_Setpoint_Schedule_Name=clg_schedule_name 

1305 ) 

1306 return stat 

1307 

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

1309 heating_sp: Union[int, float], 

1310 cooling_sp: Union[int, float], 

1311 mode='setback'): 

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

1313 

1314 This function manually sets heating and cooling templates. 

1315 

1316 Args: 

1317 idf: idf file object 

1318 heating_sp: float or int for heating set point 

1319 cooling_sp: float or int for cooling set point 

1320 name: IDF Thermostat Name 

1321 """ 

1322 if cooling_sp < 20: 

1323 cooling_sp = 26 

1324 elif cooling_sp < 24: 

1325 cooling_sp = 23 

1326 

1327 setback_htg = 18 # "T_threshold_heating" 

1328 setback_clg = 26 # "T_threshold_cooling" 

1329 

1330 # ensure setback temperature actually performs a setback on temperature 

1331 if setback_htg > heating_sp: 

1332 setback_htg = heating_sp 

1333 if setback_clg < cooling_sp: 

1334 setback_clg = cooling_sp 

1335 

1336 if mode == "setback": 

1337 htg_alldays = self._define_schedule_part('Alldays', 

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

1339 ('21:00', heating_sp), 

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

1341 clg_alldays = self._define_schedule_part('Alldays', 

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

1343 ('21:00', cooling_sp), 

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

1345 htg_name = "H_SetBack_" + str(heating_sp) 

1346 clg_name = "C_SetBack_" + str(cooling_sp) 

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

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

1349 else: 

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

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

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

1353 else: 

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

1355 stat = idf.newidfobject( 

1356 "HVACTEMPLATE:THERMOSTAT", 

1357 Name="STAT_" + name, 

1358 Heating_Setpoint_Schedule_Name=htg_name, 

1359 Cooling_Setpoint_Schedule_Name=clg_name, 

1360 ) 

1361 

1362 if mode == "constant": 

1363 stat = idf.newidfobject( 

1364 "HVACTEMPLATE:THERMOSTAT", 

1365 Name="STAT_" + name, 

1366 Constant_Heating_Setpoint=heating_sp, 

1367 Constant_Cooling_Setpoint=cooling_sp, 

1368 ) 

1369 return stat 

1370 

1371 @staticmethod 

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

1373 """Write schedules to idf. 

1374 

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

1376 setup of schedules (combined with set_hvac_template). 

1377 

1378 Args: 

1379 idf: idf file object 

1380 sched_name: str with name of the schedule 

1381 sched_part_list: list of schedule parts (cf. function 

1382 _define_schedule_part) 

1383 """ 

1384 sched_list = {} 

1385 field_count = 1 

1386 for parts in sched_part_list: 

1387 field_count += 1 

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

1389 part = parts[1] 

1390 for set in part: 

1391 field_count += 1 

1392 sched_list.update( 

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

1394 field_count += 1 

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

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

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

1398 

1399 sched = idf.newidfobject( 

1400 "SCHEDULE:COMPACT", 

1401 Name=sched_name, 

1402 Schedule_Type_Limits_Name="Temperature", 

1403 Field_1="Through: 12/31", 

1404 **sched_list 

1405 ) 

1406 return sched 

1407 

1408 @staticmethod 

1409 def _define_schedule_part( 

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

1411 """Defines a part of a schedule. 

1412 

1413 Args: 

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

1415 Sundays, ... 

1416 til_time_temp: List of tuples 

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

1418 temperature until this time in Celsius), 

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

1420 """ 

1421 return [days, til_time_temp] 

1422 

1423 @staticmethod 

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

1425 """Add shading boundaries to idf. 

1426 

1427 Args: 

1428 elements: dict[guid: element] 

1429 idf: idf file object 

1430 """ 

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

1432 spatials = [] 

1433 ext_spatial_elem = filter_elements(elements, ExternalSpatialElement) 

1434 for elem in ext_spatial_elem: 

1435 for sb in elem.space_boundaries: 

1436 spatials.append(sb) 

1437 if not spatials: 

1438 return 

1439 pure_spatials = [] 

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

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

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

1443 

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

1445 # append them to pure_spatials for further processing 

1446 if shades_included: 

1447 for s in spatials: 

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

1449 pure_spatials.append(s) 

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

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

1452 # further processing 

1453 else: 

1454 for s in spatials: 

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

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

1457 angle = math.degrees( 

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

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

1460 continue 

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

1462 'IfcSpace'): 

1463 continue 

1464 pure_spatials.append(s) 

1465 

1466 # create idf shadings from set of pure_spatials 

1467 for s in pure_spatials: 

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

1469 Name=s.guid, 

1470 ) 

1471 obj_pnts = PyOCCTools.get_points_of_face(s.bound_shape) 

1472 obj_coords = [] 

1473 for pnt in obj_pnts: 

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

1475 obj_coords.append(co) 

1476 obj.setcoords(obj_coords) 

1477 

1478 def add_shading_control(self, shading_type, elements, 

1479 idf, solar=150): 

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

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

1482 indoor air temperature must exceed a certain temperature and the solar 

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

1484 Args: 

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

1486 elements: elements 

1487 idf: idf 

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

1489 """ 

1490 zones = filter_elements(elements, ThermalZone) 

1491 

1492 for zone in zones: 

1493 zone_name = zone.guid 

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

1495 isinstance(sb.bound_element, Window)] 

1496 if not zone_openings: 

1497 continue 

1498 fenestration_dict = {} 

1499 for i, opening in enumerate(zone_openings): 

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

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

1502 shade_control_name = "ShadeControl_" + zone_name 

1503 opening_obj = idf.getobject( 

1504 'FENESTRATIONSURFACE:DETAILED', zone_openings[ 

1505 0].guid) 

1506 if opening_obj: 

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

1508 shading_type 

1509 else: 

1510 continue 

1511 if not idf.getobject( 

1512 "WINDOWSHADINGCONTROL", shade_control_name): 

1513 # temperature setpoint for indoor air temperature [°C], set to 

1514 # 2K higher than the maximum heating profile temperature within 

1515 # the current thermal zone. 

1516 idf.newidfobject("WINDOWSHADINGCONTROL", 

1517 Name=shade_control_name, 

1518 Zone_Name=zone_name, 

1519 Shading_Type=shading_type+"Shade", 

1520 Construction_with_Shading_Name=construction_name, 

1521 Shading_Control_Type= 

1522 'OnIfHighZoneAirTempAndHighSolarOnWindow', 

1523 # only close blinds if heating setpoint 

1524 # temperature is already exceeded (save energy) 

1525 Setpoint=max(zone.heating_profile)+2 - 273.15, 

1526 Setpoint_2=solar, 

1527 Multiple_Surface_Control_Type='Group', 

1528 **fenestration_dict 

1529 ) 

1530 

1531 @staticmethod 

1532 def set_simulation_control(sim_settings: EnergyPlusSimSettings, idf): 

1533 """Set simulation control parameters. 

1534 

1535 This function sets general simulation control parameters. These can 

1536 be easily overwritten in the exported idf. 

1537 Args: 

1538 sim_settings: EnergyPlusSimSettings 

1539 idf: idf file object 

1540 """ 

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

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

1543 if sim_settings.system_sizing or sim_settings.weather_file_for_sizing: 

1544 sim_control.Do_System_Sizing_Calculation = 'Yes' 

1545 else: 

1546 sim_control.Do_System_Sizing_Calculation = 'No' 

1547 if sim_settings.run_for_sizing_periods or not sim_settings.run_full_simulation: 

1548 sim_control.Run_Simulation_for_Sizing_Periods = 'Yes' 

1549 else: 

1550 sim_control.Run_Simulation_for_Sizing_Periods = 'No' 

1551 if sim_settings.run_for_weather_period: 

1552 sim_control.Run_Simulation_for_Weather_File_Run_Periods = 'Yes' 

1553 else: 

1554 sim_control.Run_Simulation_for_Weather_File_Run_Periods = 'No' 

1555 if sim_settings.set_run_period or sim_settings.run_full_simulation: 

1556 sim_control.Run_Simulation_for_Weather_File_Run_Periods = 'Yes' 

1557 else: 

1558 sim_control.Run_Simulation_for_Weather_File_Run_Periods = 'No' 

1559 

1560 if sim_settings.set_run_period: 

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

1562 run_period.Begin_Month = sim_settings.run_period_start_month 

1563 run_period.Begin_Day_of_Month = ( 

1564 sim_settings.run_period_start_day) 

1565 run_period.End_Month = sim_settings.run_period_end_month 

1566 run_period.End_Day_of_Month = sim_settings.run_period_end_day 

1567 

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

1569 building.Solar_Distribution = sim_settings.solar_distribution 

1570 

1571 @staticmethod 

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

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

1574 

1575 Args: 

1576 idf: idf file object 

1577 t_ground: ground temperature as ureg.Quantity 

1578 """ 

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

1580 

1581 string = '_Ground_Temperature' 

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

1583 'July', 'August', 'September', 'October', 

1584 'November', 'December'] 

1585 temp_dict = {} 

1586 for month in month_list: 

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

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

1589 

1590 @staticmethod 

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

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

1593 

1594 Args: 

1595 idf: idf file object 

1596 sim_settings: BIM2SIM simulation settings 

1597 """ 

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

1599 

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

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

1602 out_control[0].Column_Separator = sim_settings.output_format 

1603 out_control[0].Unit_Conversion = sim_settings.unit_conversion 

1604 

1605 # remove all existing output variables with reporting frequency 

1606 # "Timestep" 

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

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

1609 for var in out_var: 

1610 idf.removeidfobject(var) 

1611 if 'output_outdoor_conditions' in sim_settings.output_keys: 

1612 idf.newidfobject( 

1613 "OUTPUT:VARIABLE", 

1614 Variable_Name="Site Outdoor Air Drybulb Temperature", 

1615 Reporting_Frequency="Hourly", 

1616 ) 

1617 idf.newidfobject( 

1618 "OUTPUT:VARIABLE", 

1619 Variable_Name="Site Outdoor Air Humidity Ratio", 

1620 Reporting_Frequency="Hourly", 

1621 ) 

1622 idf.newidfobject( 

1623 "OUTPUT:VARIABLE", 

1624 Variable_Name="Site Outdoor Air Relative Humidity", 

1625 Reporting_Frequency="Hourly", 

1626 ) 

1627 idf.newidfobject( 

1628 "OUTPUT:VARIABLE", 

1629 Variable_Name="Site Outdoor Air Barometric Pressure", 

1630 Reporting_Frequency="Hourly", 

1631 ) 

1632 idf.newidfobject( 

1633 "OUTPUT:VARIABLE", 

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

1635 Reporting_Frequency="Hourly", 

1636 ) 

1637 idf.newidfobject( 

1638 "OUTPUT:VARIABLE", 

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

1640 Reporting_Frequency="Hourly", 

1641 ) 

1642 idf.newidfobject( 

1643 "OUTPUT:VARIABLE", 

1644 Variable_Name="Site Ground Temperature", 

1645 Reporting_Frequency="Hourly", 

1646 ) 

1647 idf.newidfobject( 

1648 "OUTPUT:VARIABLE", 

1649 Variable_Name="Site Wind Speed", 

1650 Reporting_Frequency="Hourly", 

1651 ) 

1652 idf.newidfobject( 

1653 "OUTPUT:VARIABLE", 

1654 Variable_Name="Site Wind Direction", 

1655 Reporting_Frequency="Hourly", 

1656 ) 

1657 if 'output_zone_temperature' in sim_settings.output_keys: 

1658 idf.newidfobject( 

1659 "OUTPUT:VARIABLE", 

1660 Variable_Name="Zone Mean Air Temperature", 

1661 Reporting_Frequency="Hourly", 

1662 ) 

1663 idf.newidfobject( 

1664 "OUTPUT:VARIABLE", 

1665 Variable_Name="Zone Operative Temperature", 

1666 Reporting_Frequency="Hourly", 

1667 ) 

1668 idf.newidfobject( 

1669 "OUTPUT:VARIABLE", 

1670 Variable_Name="Zone Air Relative Humidity", 

1671 Reporting_Frequency="Hourly", 

1672 ) 

1673 if 'output_internal_gains' in sim_settings.output_keys: 

1674 idf.newidfobject( 

1675 "OUTPUT:VARIABLE", 

1676 Variable_Name="Zone People Occupant Count", 

1677 Reporting_Frequency="Hourly", 

1678 ) 

1679 idf.newidfobject( 

1680 "OUTPUT:VARIABLE", 

1681 Variable_Name="Zone People Total Heating Rate", 

1682 Reporting_Frequency="Hourly", 

1683 ) 

1684 idf.newidfobject( 

1685 "OUTPUT:VARIABLE", 

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

1687 Reporting_Frequency="Hourly", 

1688 ) 

1689 idf.newidfobject( 

1690 "OUTPUT:VARIABLE", 

1691 Variable_Name="Zone Lights Total Heating Rate", 

1692 Reporting_Frequency="Hourly", 

1693 ) 

1694 idf.newidfobject( 

1695 "OUTPUT:VARIABLE", 

1696 Variable_Name="Zone Total Internal Total Heating Rate", 

1697 Reporting_Frequency="Hourly", 

1698 ) 

1699 if 'output_zone' in sim_settings.output_keys: 

1700 idf.newidfobject( 

1701 "OUTPUT:VARIABLE", 

1702 Variable_Name="Zone Thermostat Heating Setpoint Temperature", 

1703 Reporting_Frequency="Hourly", 

1704 ) 

1705 idf.newidfobject( 

1706 "OUTPUT:VARIABLE", 

1707 Variable_Name="Zone Thermostat Cooling Setpoint Temperature", 

1708 Reporting_Frequency="Hourly", 

1709 ) 

1710 idf.newidfobject( 

1711 "OUTPUT:VARIABLE", 

1712 Variable_Name="Zone Ideal Loads Supply Air Total Heating " 

1713 "Energy", 

1714 Reporting_Frequency="Hourly", 

1715 ) 

1716 idf.newidfobject( 

1717 "OUTPUT:VARIABLE", 

1718 Variable_Name="Zone Ideal Loads Supply Air Total Cooling " 

1719 "Energy", 

1720 Reporting_Frequency="Hourly", 

1721 ) 

1722 idf.newidfobject( 

1723 "OUTPUT:VARIABLE", 

1724 Variable_Name="Zone Ideal Loads Outdoor Air Total Cooling Rate", 

1725 Reporting_Frequency="Hourly", 

1726 ) 

1727 idf.newidfobject( 

1728 "OUTPUT:VARIABLE", 

1729 Variable_Name="Zone Ideal Loads Outdoor Air Total Heating Rate", 

1730 Reporting_Frequency="Hourly", 

1731 ) 

1732 idf.newidfobject( 

1733 "OUTPUT:VARIABLE", 

1734 Variable_Name="Zone Ideal Loads Supply Air Total Cooling Rate", 

1735 Reporting_Frequency="Hourly", 

1736 ) 

1737 idf.newidfobject( 

1738 "OUTPUT:VARIABLE", 

1739 Variable_Name="Zone Ideal Loads Supply Air Total Heating Rate", 

1740 Reporting_Frequency="Hourly", 

1741 ) 

1742 idf.newidfobject( 

1743 "OUTPUT:VARIABLE", 

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

1745 Reporting_Frequency="Hourly", 

1746 ) 

1747 idf.newidfobject( 

1748 "OUTPUT:VARIABLE", 

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

1750 Reporting_Frequency="Hourly", 

1751 ) 

1752 idf.newidfobject( 

1753 "OUTPUT:VARIABLE", 

1754 Variable_Name="Zone Windows Total Transmitted Solar Radiation " 

1755 "Energy", 

1756 Reporting_Frequency="Hourly", 

1757 ) 

1758 if 'output_infiltration' in sim_settings.output_keys: 

1759 idf.newidfobject( 

1760 "OUTPUT:VARIABLE", 

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

1762 Reporting_Frequency="Hourly", 

1763 ) 

1764 idf.newidfobject( 

1765 "OUTPUT:VARIABLE", 

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

1767 Reporting_Frequency="Hourly", 

1768 ) 

1769 idf.newidfobject( 

1770 "OUTPUT:VARIABLE", 

1771 Variable_Name="Zone Infiltration Air Change Rate", 

1772 Reporting_Frequency="Hourly", 

1773 ) 

1774 idf.newidfobject( 

1775 "OUTPUT:VARIABLE", 

1776 Variable_Name="Zone Ventilation Air Change Rate", 

1777 Reporting_Frequency="Hourly", 

1778 ) 

1779 idf.newidfobject( 

1780 "OUTPUT:VARIABLE", 

1781 Variable_Name="Zone Ventilation Standard Density Volume Flow " 

1782 "Rate", 

1783 Reporting_Frequency="Hourly", 

1784 ) 

1785 idf.newidfobject( 

1786 "OUTPUT:VARIABLE", 

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

1788 Reporting_Frequency="Hourly", 

1789 ) 

1790 idf.newidfobject( 

1791 "OUTPUT:VARIABLE", 

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

1793 Reporting_Frequency="Hourly", 

1794 ) 

1795 idf.newidfobject( 

1796 "OUTPUT:VARIABLE", 

1797 Variable_Name="Zone Mechanical Ventilation Air Changes per " 

1798 "Hour", 

1799 Reporting_Frequency="Hourly", 

1800 ) 

1801 idf.newidfobject( 

1802 "OUTPUT:VARIABLE", 

1803 Variable_Name="Zone Mechanical Ventilation Standard Density " 

1804 "Volume Flow Rate", 

1805 Reporting_Frequency="Hourly", 

1806 ) 

1807 

1808 

1809 if 'output_meters' in sim_settings.output_keys: 

1810 idf.newidfobject( 

1811 "OUTPUT:METER", 

1812 Key_Name="Heating:EnergyTransfer", 

1813 Reporting_Frequency="Hourly", 

1814 ) 

1815 idf.newidfobject( 

1816 "OUTPUT:METER", 

1817 Key_Name="Cooling:EnergyTransfer", 

1818 Reporting_Frequency="Hourly", 

1819 ) 

1820 idf.newidfobject( 

1821 "OUTPUT:METER", 

1822 Key_Name="DistrictHeating:HVAC", 

1823 Reporting_Frequency="Hourly", 

1824 ) 

1825 idf.newidfobject( 

1826 "OUTPUT:METER", 

1827 Key_Name="DistrictCooling:HVAC", 

1828 Reporting_Frequency="Hourly", 

1829 ) 

1830 if 'output_dxf' in sim_settings.output_keys: 

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

1832 Report_Type="DXF") 

1833 if sim_settings.cfd_export: 

1834 idf.newidfobject( 

1835 "OUTPUT:VARIABLE", 

1836 Variable_Name="Surface Inside Face Temperature", 

1837 Reporting_Frequency="Hourly", 

1838 ) 

1839 idf.newidfobject( 

1840 "OUTPUT:VARIABLE", 

1841 Variable_Name= 

1842 "Surface Inside Face Conduction Heat Transfer Rate per Area", 

1843 Reporting_Frequency="Hourly", 

1844 ) 

1845 idf.newidfobject( 

1846 "OUTPUT:VARIABLE", 

1847 Variable_Name= 

1848 "Surface Inside Face Conduction Heat Transfer Rate", 

1849 Reporting_Frequency="Hourly", 

1850 ) 

1851 idf.newidfobject( 

1852 "OUTPUT:VARIABLE", 

1853 Variable_Name= 

1854 "Surface Window Net Heat Transfer Rate", 

1855 Reporting_Frequency="Hourly", 

1856 ) 

1857 idf.newidfobject("OUTPUT:DIAGNOSTICS", 

1858 Key_1="DisplayAdvancedReportVariables", 

1859 Key_2="DisplayExtraWarnings") 

1860 return idf 

1861 

1862 @staticmethod 

1863 def export_geom_to_idf(sim_settings: EnergyPlusSimSettings, 

1864 elements: dict, idf: IDF): 

1865 """Write space boundary geometry to idf. 

1866 

1867 This function converts the space boundary bound_shape from 

1868 OpenCascade to idf geometry. 

1869 

1870 Args: 

1871 elements: dict[guid: element] 

1872 idf: idf file object 

1873 """ 

1874 logger.info("Export IDF geometry") 

1875 bounds = filter_elements(elements, SpaceBoundary) 

1876 for bound in bounds: 

1877 idfp = IdfObject(sim_settings, bound, idf) 

1878 if idfp.skip_bound: 

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

1880 logger.warning( 

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

1882 "missing boundary conditions)!", 

1883 idfp.name, idfp.surface_type) 

1884 continue 

1885 bounds_2b = filter_elements(elements, SpaceBoundary2B) 

1886 for b_bound in bounds_2b: 

1887 idfp = IdfObject(sim_settings, b_bound, idf) 

1888 if idfp.skip_bound: 

1889 logger.warning( 

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

1891 "missing boundary conditions)!", 

1892 idfp.name, idfp.surface_type) 

1893 continue 

1894 

1895 @staticmethod 

1896 def idf_validity_check(idf): 

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

1898 

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

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

1901 

1902 Args: 

1903 idf: idf file object 

1904 """ 

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

1906 

1907 # remove erroneous fenestration surfaces which do may crash 

1908 # EnergyPlus simulation 

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

1910 

1911 # Create a list of fenestrations to remove 

1912 to_remove = [] 

1913 

1914 for fenestration in fenestrations: 

1915 should_remove = False 

1916 

1917 # Check for missing building surface reference 

1918 if not fenestration.Building_Surface_Name: 

1919 should_remove = True 

1920 else: 

1921 # Check if the referenced surface is adiabatic 

1922 building_surface = idf.getobject( 

1923 'BUILDINGSURFACE:DETAILED', 

1924 fenestration.Building_Surface_Name 

1925 ) 

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

1927 should_remove = True 

1928 

1929 if should_remove: 

1930 to_remove.append(fenestration) 

1931 

1932 # Remove the collected fenestrations 

1933 for fenestration in to_remove: 

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

1935 idf.removeidfobject(fenestration) 

1936 

1937 # Check if shading control elements contain unavailable fenestration 

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

1939 shading_control = idf.idfobjects['WINDOWSHADINGCONTROL'] 

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

1941 for shc in shading_control: 

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

1943 # fenestration) 

1944 fenestration_guids_new = [] 

1945 skipped_fenestration = False # flag for unavailable fenestration 

1946 for attr_name in dir(shc): 

1947 if ('Fenestration_Surface' in attr_name): 

1948 if (getattr(shc, attr_name) in 

1949 fenestration_guids): 

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

1951 elif (getattr(shc, attr_name) not in 

1952 fenestration_guids) and getattr(shc, attr_name): 

1953 skipped_fenestration = True 

1954 # if the shading control element containes unavailable 

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

1956 # prevent errors in simulation 

1957 if fenestration_guids_new and skipped_fenestration: 

1958 fenestration_dict = {} 

1959 for i, guid in enumerate(fenestration_guids_new): 

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

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

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

1963 # removing individual attributes of the shading element 

1964 # caused errors, so new shading control is created 

1965 idf.removeidfobject(shc) 

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

1967 Zone_Name=shc.Zone_Name, 

1968 Shading_Type=shc.Shading_Type, 

1969 Construction_with_Shading_Name= 

1970 shc.Construction_with_Shading_Name, 

1971 Shading_Control_Type=shc.Shading_Control_Type, 

1972 Setpoint=shc.Setpoint, 

1973 Setpoint_2=shc.Setpoint_2, 

1974 Multiple_Surface_Control_Type= 

1975 shc.Multiple_Surface_Control_Type, 

1976 **fenestration_dict) 

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

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

1979 

1980 # check for small building surfaces and remove them 

1981 sfs = idf.getsurfaces() 

1982 small_area_obj = [s for s in sfs 

1983 if PyOCCTools.get_shape_area( 

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

1985 

1986 for obj in small_area_obj: 

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

1988 idf.removeidfobject(obj) 

1989 

1990 # check for small shading surfaces and remove them 

1991 shadings = idf.getshadingsurfaces() 

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

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

1994 

1995 for obj in small_area_obj: 

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

1997 idf.removeidfobject(obj) 

1998 

1999 # Check for building surfaces holding default window materials 

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

2001 for sf in bsd: 

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

2003 logger.info( 

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

2005 idf.removeidfobject(sf) 

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

2007 

2008 

2009class IdfObject: 

2010 """Create idf elements for surfaces. 

2011 

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

2013 BUILDINGSURFACE:DETAILED and FENESTRATIONSURFACE:DETAILED. 

2014 This includes further methods for processing the preprocessed information 

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

2016 """ 

2017 

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

2019 self.name = inst_obj.guid 

2020 self.building_surface_name = None 

2021 self.key = None 

2022 self.out_bound_cond = '' 

2023 self.out_bound_cond_obj = '' 

2024 self.sun_exposed = '' 

2025 self.wind_exposed = '' 

2026 self.surface_type = None 

2027 self.physical = inst_obj.physical 

2028 self.construction_name = None 

2029 self.related_bound = inst_obj.related_bound 

2030 self.this_bound = inst_obj 

2031 self.skip_bound = False 

2032 self.bound_shape = inst_obj.bound_shape 

2033 self.add_window_shade = False 

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

2035 self.skip_bound = True 

2036 return 

2037 self.zone_name = inst_obj.bound_thermal_zone.guid 

2038 if inst_obj.parent_bound: 

2039 self.key = "FENESTRATIONSURFACE:DETAILED" 

2040 if sim_settings.add_window_shading == 'Interior': 

2041 self.add_window_shade = 'Interior' 

2042 elif sim_settings.add_window_shading == 'Exterior': 

2043 self.add_window_shade = 'Exterior' 

2044 else: 

2045 self.key = "BUILDINGSURFACE:DETAILED" 

2046 if inst_obj.parent_bound: 

2047 self.building_surface_name = inst_obj.parent_bound.guid 

2048 self.map_surface_types(inst_obj) 

2049 self.map_boundary_conditions(inst_obj) 

2050 self.set_preprocessed_construction_name() 

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

2052 if not self.construction_name \ 

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

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

2055 self.construction_name)): 

2056 self.set_construction_name() 

2057 obj = self.set_idfobject_attributes(idf) 

2058 if obj is not None: 

2059 self.set_idfobject_coordinates(obj, idf, inst_obj) 

2060 else: 

2061 pass 

2062 

2063 def set_construction_name(self): 

2064 """Set default construction names. 

2065 

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

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

2068 object. 

2069 """ 

2070 if self.surface_type == "Wall": 

2071 self.construction_name = "Project Wall" 

2072 elif self.surface_type == "Roof": 

2073 self.construction_name = "Project Flat Roof" 

2074 elif self.surface_type == "Ceiling": 

2075 self.construction_name = "Project Ceiling" 

2076 elif self.surface_type == "Floor": 

2077 self.construction_name = "Project Floor" 

2078 elif self.surface_type == "Door": 

2079 self.construction_name = "Project Door" 

2080 elif self.surface_type == "Window": 

2081 self.construction_name = "Project External Window" 

2082 

2083 def set_preprocessed_construction_name(self): 

2084 """Set preprocessed constructions. 

2085 

2086 This function sets constructions of idf surfaces to preprocessed 

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

2088 (not defined in preprocessing). 

2089 """ 

2090 # set air wall for virtual bounds 

2091 if not self.physical: 

2092 if self.out_bound_cond == "Surface": 

2093 self.construction_name = "Air Wall" 

2094 else: 

2095 rel_elem = self.this_bound.bound_element 

2096 if not rel_elem: 

2097 return 

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

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

2100 self.construction_name = 'Window_WM_' + \ 

2101 rel_elem.layerset.layers[ 

2102 0].material.name \ 

2103 + '_' + str( 

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

2105 else: 

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

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

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

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

2110 rel_elem.layerset.layers])) 

2111 

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

2113 inst_obj: Union[SpaceBoundary, 

2114 SpaceBoundary2B]): 

2115 """Export surface coordinates. 

2116 

2117 This function exports the surface coordinates from the BIM2SIM Space 

2118 Boundary instance to idf. 

2119 Circular shapes and shapes with more than 120 vertices 

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

2121 simplified. 

2122 

2123 Args: 

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

2125 idf: idf file object 

2126 inst_obj: SpaceBoundary instance 

2127 """ 

2128 # write bound_shape to obj 

2129 obj_pnts = PyOCCTools.get_points_of_face(self.bound_shape) 

2130 obj_coords = [] 

2131 for pnt in obj_pnts: 

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

2133 obj_coords.append(co) 

2134 try: 

2135 obj.setcoords(obj_coords) 

2136 except Exception as ex: 

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

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

2139 f"exported." 

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

2141 self.skip_bound = True 

2142 return 

2143 circular_shape = self.get_circular_shape(obj_pnts) 

2144 try: 

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

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

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

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

2149 obj.setcoords(obj_coords) 

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

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

2152 else: 

2153 self.process_other_shapes(inst_obj, obj) 

2154 except Exception as ex: 

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

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

2157 f"exported." 

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

2159 

2160 def set_idfobject_attributes(self, idf: IDF): 

2161 """Writes precomputed surface attributes to idf. 

2162 

2163 Args: 

2164 idf: the idf file 

2165 """ 

2166 if self.surface_type is not None: 

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

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

2169 "Window".lower()}: 

2170 self.surface_type = "Wall" 

2171 obj = idf.newidfobject( 

2172 self.key, 

2173 Name=self.name, 

2174 Surface_Type=self.surface_type, 

2175 Construction_Name=self.construction_name, 

2176 Outside_Boundary_Condition=self.out_bound_cond, 

2177 Outside_Boundary_Condition_Object=self.out_bound_cond_obj, 

2178 Zone_Name=self.zone_name, 

2179 Sun_Exposure=self.sun_exposed, 

2180 Wind_Exposure=self.wind_exposed, 

2181 ) 

2182 else: 

2183 obj = idf.newidfobject( 

2184 self.key, 

2185 Name=self.name, 

2186 Surface_Type=self.surface_type, 

2187 Construction_Name=self.construction_name, 

2188 Building_Surface_Name=self.building_surface_name, 

2189 Outside_Boundary_Condition_Object=self.out_bound_cond_obj, 

2190 ) 

2191 return obj 

2192 

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

2194 SpaceBoundary2B]): 

2195 """Map surface types. 

2196 

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

2198 surface type. 

2199 

2200 Args: 

2201 inst_obj: SpaceBoundary instance 

2202 """ 

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

2204 # TODO update to new disaggregations 

2205 elem = inst_obj.bound_element 

2206 surface_type = None 

2207 if elem is not None: 

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

2209 include_self=True)]): 

2210 surface_type = 'Wall' 

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

2212 include_self=True)]): 

2213 surface_type = "Door" 

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

2215 Window, include_self=True)]): 

2216 surface_type = "Window" 

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

2218 include_self=True)]): 

2219 surface_type = "Roof" 

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

2221 include_self=True)]): 

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

2223 GroundFloor, include_self=True)]): 

2224 surface_type = "Floor" 

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

2226 InnerFloor, include_self=True)]): 

2227 if inst_obj.top_bottom == BoundaryOrientation.bottom: 

2228 surface_type = "Floor" 

2229 elif inst_obj.top_bottom == BoundaryOrientation.top: 

2230 surface_type = "Ceiling" 

2231 elif inst_obj.top_bottom == BoundaryOrientation.vertical: 

2232 surface_type = "Wall" 

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

2234 f"found, exported as wall, " 

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

2236 else: 

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

2238 f"to surface type for GUID: " 

2239 f"{inst_obj.guid}.") 

2240 surface_type = "Floor" 

2241 # elif elem.ifc is not None: 

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

2243 # if not PyOCCTools.compare_direction_of_normals( 

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

2245 # surface_type = 'Wall' 

2246 # else: 

2247 # surface_type = 'Ceiling' 

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

2249 # surface_type = 'Wall' 

2250 elif inst_obj.top_bottom == BoundaryOrientation.bottom: 

2251 surface_type = "Floor" 

2252 elif inst_obj.top_bottom == BoundaryOrientation.top: 

2253 surface_type = "Ceiling" 

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

2255 surface_type = "Roof" 

2256 elif inst_obj.top_bottom == BoundaryOrientation.vertical: 

2257 surface_type = "Wall" 

2258 else: 

2259 if not PyOCCTools.compare_direction_of_normals( 

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

2261 surface_type = 'Wall' 

2262 elif inst_obj.top_bottom == BoundaryOrientation.bottom: 

2263 surface_type = "Floor" 

2264 elif inst_obj.top_bottom == BoundaryOrientation.top: 

2265 surface_type = "Ceiling" 

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

2267 surface_type = "Roof" 

2268 else: 

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

2270 elif not inst_obj.physical: 

2271 if not PyOCCTools.compare_direction_of_normals( 

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

2273 surface_type = 'Wall' 

2274 else: 

2275 if inst_obj.top_bottom == BoundaryOrientation.bottom: 

2276 surface_type = "Floor" 

2277 elif inst_obj.top_bottom == BoundaryOrientation.top: 

2278 surface_type = "Ceiling" 

2279 else: 

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

2281 

2282 self.surface_type = surface_type 

2283 

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

2285 SpaceBoundary2B]): 

2286 """Map boundary conditions. 

2287 

2288 This function maps the boundary conditions of a SpaceBoundary instance 

2289 to the idf space boundary conditions. 

2290 

2291 Args: 

2292 inst_obj: SpaceBoundary instance 

2293 """ 

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

2295 or inst_obj.related_adb_bound is not None: 

2296 self.out_bound_cond = 'Adiabatic' 

2297 self.sun_exposed = 'NoSun' 

2298 self.wind_exposed = 'NoWind' 

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

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

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

2302 == 'EXTERNAL_EARTH')) 

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

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

2305 self.out_bound_cond = "Ground" 

2306 self.sun_exposed = 'NoSun' 

2307 self.wind_exposed = 'NoWind' 

2308 elif inst_obj.is_external and inst_obj.physical \ 

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

2310 self.out_bound_cond = 'Outdoors' 

2311 self.sun_exposed = 'SunExposed' 

2312 self.wind_exposed = 'WindExposed' 

2313 self.out_bound_cond_obj = '' 

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

2315 (inst_obj.related_bound is None 

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

2317 'IfcExternalSpatialElement')): 

2318 self.out_bound_cond = "Ground" 

2319 self.sun_exposed = 'NoSun' 

2320 self.wind_exposed = 'NoWind' 

2321 elif inst_obj.related_bound is not None \ 

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

2323 'IfcExternalSpatialElement'): 

2324 self.out_bound_cond = 'Surface' 

2325 self.out_bound_cond_obj = inst_obj.related_bound.guid 

2326 self.sun_exposed = 'NoSun' 

2327 self.wind_exposed = 'NoWind' 

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

2329 self.out_bound_cond = 'Outdoors' 

2330 self.sun_exposed = 'SunExposed' 

2331 self.wind_exposed = 'WindExposed' 

2332 self.out_bound_cond_obj = '' 

2333 elif self.related_bound is None: 

2334 self.out_bound_cond = 'Outdoors' 

2335 self.sun_exposed = 'SunExposed' 

2336 self.wind_exposed = 'WindExposed' 

2337 self.out_bound_cond_obj = '' 

2338 else: 

2339 self.skip_bound = True 

2340 

2341 @staticmethod 

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

2343 """Check if a shape is circular. 

2344 

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

2346 

2347 Args: 

2348 obj_pnts: SpaceBoundary vertices (list of coordinate tuples) 

2349 Returns: 

2350 True if shape is circular 

2351 """ 

2352 circular_shape = False 

2353 # compute if shape is circular: 

2354 if len(obj_pnts) > 4: 

2355 pnt = obj_pnts[0] 

2356 pnt2 = obj_pnts[1] 

2357 distance_prev = pnt.Distance(pnt2) 

2358 pnt = pnt2 

2359 for pnt2 in obj_pnts[2:]: 

2360 distance = pnt.Distance(pnt2) 

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

2362 circular_shape = True 

2363 pnt = pnt2 

2364 distance_prev = distance 

2365 else: 

2366 continue 

2367 return circular_shape 

2368 

2369 @staticmethod 

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

2371 inst_obj: Union[SpaceBoundary, SpaceBoundary2B] 

2372 ): 

2373 """Simplify circular space boundaries. 

2374 

2375 This function processes circular boundary shapes. It converts circular 

2376 shapes to triangular shapes. 

2377 

2378 Args: 

2379 idf: idf file object 

2380 obj_coords: coordinates of an idf object 

2381 obj: idf object 

2382 inst_obj: SpaceBoundary instance 

2383 """ 

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

2385 drop_list = obj_coords[0::drop_count] 

2386 pnt = drop_list[0] 

2387 counter = 0 

2388 # del inst_obj.__dict__['bound_center'] 

2389 for pnt2 in drop_list[1:]: 

2390 counter += 1 

2391 new_obj = idf.copyidfobject(obj) 

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

2393 fc = PyOCCTools.make_faces_from_pnts( 

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

2395 fcsc = PyOCCTools.scale_face(fc, 0.99) 

2396 new_pnts = PyOCCTools.get_points_of_face(fcsc) 

2397 new_coords = [] 

2398 for pnt in new_pnts: 

2399 new_coords.append(pnt.Coord()) 

2400 new_obj.setcoords(new_coords) 

2401 pnt = pnt2 

2402 new_obj = idf.copyidfobject(obj) 

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

2404 fc = PyOCCTools.make_faces_from_pnts( 

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

2406 fcsc = PyOCCTools.scale_face(fc, 0.99) 

2407 new_pnts = PyOCCTools.get_points_of_face(fcsc) 

2408 new_coords = [] 

2409 for pnt in new_pnts: 

2410 new_coords.append(pnt.Coord()) 

2411 new_obj.setcoords(new_coords) 

2412 idf.removeidfobject(obj) 

2413 

2414 @staticmethod 

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

2416 obj): 

2417 """Simplify non-circular shapes. 

2418 

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

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

2421 (more than 120 vertices for BUILDINGSURFACE:DETAILED 

2422 and more than 4 vertices for FENESTRATIONSURFACE:DETAILED) 

2423 

2424 Args: 

2425 inst_obj: SpaceBoundary Instance 

2426 obj: idf object 

2427 """ 

2428 # print("TOO MANY EDGES") 

2429 obj_pnts = [] 

2430 exp = TopExp_Explorer(inst_obj.bound_shape, TopAbs_FACE) 

2431 face = topods_Face(exp.Current()) 

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

2433 surf = BRep_Tool.Surface(face) 

2434 plane = Handle_Geom_Plane_DownCast(surf) 

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

2436 new_face = BRepBuilderAPI_MakeFace(plane, 

2437 umin, 

2438 umax, 

2439 vmin, 

2440 vmax).Face().Reversed() 

2441 face_exp = TopExp_Explorer(new_face, TopAbs_WIRE) 

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

2443 while w_exp.More(): 

2444 wire_vert = w_exp.CurrentVertex() 

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

2446 w_exp.Next() 

2447 obj_coords = [] 

2448 for pnt in obj_pnts: 

2449 obj_coords.append(pnt.Coord()) 

2450 obj.setcoords(obj_coords)