Coverage for bim2sim / elements / bps_elements.py: 49%

815 statements  

« prev     ^ index     » next       coverage.py v7.13.0, created at 2025-12-18 09:34 +0000

1"""Module contains the different classes for all HVAC elements""" 

2import inspect 

3import logging 

4import math 

5import re 

6import sys 

7from datetime import date 

8from typing import Set, List, Union 

9 

10import ifcopenshell 

11import ifcopenshell.geom 

12from OCC.Core.BRepBndLib import brepbndlib 

13from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform 

14from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape 

15from OCC.Core.BRepGProp import brepgprop 

16from OCC.Core.BRepLib import BRepLib_FuseEdges 

17from OCC.Core.Bnd import Bnd_Box 

18from OCC.Core.Extrema import Extrema_ExtFlag_MIN 

19from OCC.Core.GProp import GProp_GProps 

20from OCC.Core.ShapeUpgrade import ShapeUpgrade_UnifySameDomain 

21from OCC.Core.gp import gp_Trsf, gp_Vec, gp_XYZ, gp_Pnt, \ 

22 gp_Mat, gp_Quaternion 

23from ifcopenshell import guid 

24 

25from bim2sim.elements.mapping import condition, attribute 

26from bim2sim.elements.base_elements import ProductBased, RelationBased 

27from bim2sim.elements.mapping.units import ureg 

28from bim2sim.tasks.common.inner_loop_remover import remove_inner_loops 

29from bim2sim.utilities.common_functions import vector_angle, angle_equivalent 

30from bim2sim.utilities.pyocc_tools import PyOCCTools 

31from bim2sim.utilities.types import IFCDomain, BoundaryOrientation 

32 

33logger = logging.getLogger(__name__) 

34 

35 

36class BPSProduct(ProductBased): 

37 domain = 'BPS' 

38 

39 def __init__(self, *args, **kwargs): 

40 super().__init__(*args, **kwargs) 

41 self.thermal_zones = [] 

42 self.space_boundaries = [] 

43 self.storeys = [] 

44 self.material = None 

45 self.disaggregations = [] 

46 self.building = None 

47 self.site = None 

48 

49 def __repr__(self): 

50 return "<%s (guid: %s)>" % ( 

51 self.__class__.__name__, self.guid) 

52 

53 def get_bound_area(self, name) -> ureg.Quantity: 

54 """ get gross bound area (including opening areas) of the element""" 

55 return sum(sb.bound_area for sb in self.sbs_without_corresponding) 

56 

57 def get_net_bound_area(self, name) -> ureg.Quantity: 

58 """get net area (including opening areas) of the element""" 

59 return self.gross_area - self.opening_area 

60 

61 @property 

62 def is_external(self) -> bool or None: 

63 """Checks if the corresponding element has contact with external 

64 environment (e.g. ground, roof, wall)""" 

65 if hasattr(self, 'parent'): 

66 return self.parent.is_external 

67 elif hasattr(self, 'ifc'): 

68 if hasattr(self.ifc, 'ProvidesBoundaries'): 

69 if len(self.ifc.ProvidesBoundaries) > 0: 

70 ext_int = list( 

71 set([boundary.InternalOrExternalBoundary for boundary 

72 in self.ifc.ProvidesBoundaries])) 

73 if len(ext_int) == 1: 

74 if ext_int[0].lower() == 'external': 

75 return True 

76 if ext_int[0].lower() == 'internal': 

77 return False 

78 else: 

79 return ext_int 

80 return None 

81 

82 def calc_cost_group(self) -> int: 

83 """Default cost group for building elements is 300""" 

84 return 300 

85 

86 def _calc_teaser_orientation(self, name) -> Union[int, None]: 

87 """Calculate the orientation of the bps product based on SB direction. 

88 

89 For buildings elements we can use the more reliable space boundaries 

90 normal vector to calculate the orientation if the space boundaries 

91 exists. Otherwise the base calc_orientation of IFCBased will be used. 

92 

93 Returns: 

94 Orientation angle between 0 and 360. 

95 (0 : north, 90: east, 180: south, 270: west) 

96 """ 

97 true_north = self.get_true_north() 

98 if len(self.space_boundaries): 

99 new_orientation = self.group_orientation( 

100 [vector_angle(space_boundary.bound_normal.Coord()) 

101 for space_boundary in self.space_boundaries]) 

102 if new_orientation is not None: 

103 return int(angle_equivalent(new_orientation + true_north)) 

104 # return int(angle_equivalent(super().calc_orientation() + true_north)) 

105 return None 

106 

107 @staticmethod 

108 def group_orientation(orientations: list): 

109 dict_orientations = {} 

110 for orientation in orientations: 

111 rounded_orientation = round(orientation) 

112 if rounded_orientation not in dict_orientations: 

113 dict_orientations[rounded_orientation] = 0 

114 dict_orientations[rounded_orientation] += 1 

115 if len(dict_orientations): 

116 return max(dict_orientations, key=dict_orientations.get) 

117 return None 

118 

119 def _get_sbs_without_corresponding(self, name) -> list: 

120 """get a list with only not duplicated space boundaries""" 

121 sbs_without_corresponding = list(self.space_boundaries) 

122 for sb in self.space_boundaries: 

123 if sb in sbs_without_corresponding: 

124 if sb.related_bound and sb.related_bound in \ 

125 sbs_without_corresponding: 

126 sbs_without_corresponding.remove(sb.related_bound) 

127 return sbs_without_corresponding 

128 

129 def _get_opening_area(self, name): 

130 """get sum of opening areas of the element""" 

131 return sum(sb.opening_area for sb in self.sbs_without_corresponding) 

132 

133 teaser_orientation = attribute.Attribute( 

134 description="Orientation of element in TEASER conventions. 0-360 for " 

135 "orientation of vertical elements and -1 for roofs and " 

136 "ceiling, -2 for groundfloors and floors.", 

137 functions=[_calc_teaser_orientation], 

138 ) 

139 

140 gross_area = attribute.Attribute( 

141 functions=[get_bound_area], 

142 unit=ureg.meter ** 2 

143 ) 

144 

145 net_area = attribute.Attribute( 

146 functions=[get_net_bound_area], 

147 unit=ureg.meter ** 2 

148 ) 

149 

150 sbs_without_corresponding = attribute.Attribute( 

151 description="A list with only not duplicated space boundaries", 

152 functions=[_get_sbs_without_corresponding] 

153 ) 

154 

155 opening_area = attribute.Attribute( 

156 description="Sum of opening areas of the element", 

157 functions=[_get_opening_area] 

158 ) 

159 

160 

161class ThermalZone(BPSProduct): 

162 ifc_types = { 

163 "IfcSpace": 

164 ['*', 'SPACE', 'PARKING', 'GFA', 'INTERNAL', 'EXTERNAL'] 

165 } 

166 

167 pattern_ifc_type = [ 

168 re.compile('Space', flags=re.IGNORECASE), 

169 re.compile('Zone', flags=re.IGNORECASE) 

170 ] 

171 

172 def __init__(self, *args, **kwargs): 

173 self.bound_elements = kwargs.pop('bound_elements', []) 

174 super().__init__(*args, **kwargs) 

175 

176 @property 

177 def outer_walls(self) -> list: 

178 """List of all outer wall elements bounded to the thermal zone""" 

179 return [ 

180 ele for ele in self.bound_elements if isinstance(ele, OuterWall)] 

181 

182 @property 

183 def windows(self) -> list: 

184 """List of all window elements bounded to the thermal zone""" 

185 return [ele for ele in self.bound_elements if isinstance(ele, Window)] 

186 

187 @property 

188 def is_external(self) -> bool: 

189 """determines if a thermal zone is external or internal based on the 

190 presence of outer walls""" 

191 return len(self.outer_walls) > 0 

192 

193 def _get_external_orientation(self, name) -> str or float: 

194 """determines the orientation of the thermal zone based on its elements 

195 it can be a corner (list of 2 angles) or an edge (1 angle)""" 

196 if self.is_external is True: 

197 orientations = [ele.teaser_orientation for ele in self.outer_walls] 

198 calc_temp = list(set(orientations)) 

199 sum_or = sum(calc_temp) 

200 if 0 in calc_temp: 

201 if sum_or > 180: 

202 sum_or += 360 

203 return sum_or / len(calc_temp) 

204 return 'Internal' 

205 

206 def _get_glass_percentage(self, name) -> float or ureg.Quantity: 

207 """determines the glass area/facade area ratio for all the windows in 

208 the space in one of the 4 following ranges 

209 0%-30%: 15 

210 30%-50%: 40 

211 50%-70%: 60 

212 70%-100%: 85""" 

213 glass_area = sum(wi.gross_area for wi in self.windows) 

214 facade_area = sum(wa.gross_area for wa in self.outer_walls) 

215 if facade_area > 0: 

216 return 100 * (glass_area / (facade_area + glass_area)).m 

217 else: 

218 return 'Internal' 

219 

220 def _get_space_neighbors(self, name) -> list: 

221 """determines the neighbors of the thermal zone""" 

222 neighbors = [] 

223 for sb in self.space_boundaries: 

224 if sb.related_bound is not None: 

225 tz = sb.related_bound.bound_thermal_zone 

226 # todo: check if computation of neighbors works as expected 

227 # what if boundary has no related bound but still has a 

228 # neighbor? 

229 # hint: neighbors != related bounds 

230 if (tz is not self) and (tz not in neighbors): 

231 neighbors.append(tz) 

232 return neighbors 

233 

234 def _get_space_shape(self, name): 

235 """returns topods shape of the IfcSpace""" 

236 settings = ifcopenshell.geom.settings() 

237 settings.set(settings.USE_PYTHON_OPENCASCADE, True) 

238 settings.set(settings.USE_WORLD_COORDS, True) 

239 settings.set(settings.PRECISION, 1e-6) 

240 settings.set( 

241 "dimensionality", 

242 ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) # 2 

243 # settings.set(settings.EXCLUDE_SOLIDS_AND_SURFACES, False) 

244 # settings.set(settings.INCLUDE_CURVES, True) 

245 return ifcopenshell.geom.create_shape(settings, self.ifc).geometry 

246 

247 def _get_space_center(self, name) -> float: 

248 """ 

249 This function returns the center of the bounding box of an ifc space 

250 shape 

251 :return: center of space bounding box (gp_Pnt) 

252 """ 

253 bbox = Bnd_Box() 

254 brepbndlib.Add(self.space_shape, bbox) 

255 bbox_center = ifcopenshell.geom.utils.get_bounding_box_center(bbox) 

256 return bbox_center 

257 

258 def _get_footprint_shape(self, name): 

259 """ 

260 This function returns the footprint of a space shape. This can be 

261 used e.g., to visualize floor plans. 

262 """ 

263 footprint = PyOCCTools.get_footprint_of_shape(self.space_shape) 

264 return footprint 

265 

266 def _get_space_shape_volume(self, name): 

267 """ 

268 This function returns the volume of a space shape 

269 """ 

270 return PyOCCTools.get_shape_volume(self.space_shape) 

271 

272 def _get_volume_geometric(self, name): 

273 """ 

274 This function returns the volume of a space geometrically 

275 """ 

276 return self.gross_area * self.height 

277 

278 def _get_usage(self, name): 

279 """ 

280 This function returns the usage of a space 

281 """ 

282 if self.zone_name is not None: 

283 usage = self.zone_name 

284 elif self.ifc.LongName is not None and \ 

285 "oldSpaceGuids_" not in self.ifc.LongName: 

286 # todo oldSpaceGuids_ is hardcode for erics tool 

287 usage = self.ifc.LongName 

288 else: 

289 usage = self.name 

290 return usage 

291 

292 def _get_name(self, name): 

293 """ 

294 This function returns the name of a space 

295 """ 

296 if self.zone_name: 

297 space_name = self.zone_name 

298 else: 

299 space_name = self.ifc.Name 

300 return space_name 

301 

302 def get_bound_floor_area(self, name): 

303 """Get bound floor area of zone. This is currently set by sum of all 

304 horizontal gross area and take half of it due to issues with 

305 TOP BOTTOM""" 

306 leveled_areas = {} 

307 for height, sbs in self.horizontal_sbs.items(): 

308 if height not in leveled_areas: 

309 leveled_areas[height] = 0 

310 leveled_areas[height] += sum([sb.bound_area for sb in sbs]) 

311 

312 return sum(leveled_areas.values()) / 2 

313 

314 def get_net_bound_floor_area(self, name): 

315 """Get net bound floor area of zone. This is currently set by sum of all 

316 horizontal net area and take half of it due to issues with TOP BOTTOM.""" 

317 leveled_areas = {} 

318 for height, sbs in self.horizontal_sbs.items(): 

319 if height not in leveled_areas: 

320 leveled_areas[height] = 0 

321 leveled_areas[height] += sum([sb.net_bound_area for sb in sbs]) 

322 

323 return sum(leveled_areas.values()) / 2 

324 

325 def _get_horizontal_sbs(self, name): 

326 """get all horizonal SBs in a zone and convert them into a dict with 

327 key z-height in room and the SB as value.""" 

328 # todo: use only bottom when TOP bottom is working correctly 

329 valid = [BoundaryOrientation.top, BoundaryOrientation.bottom] 

330 leveled_sbs = {} 

331 for sb in self.sbs_without_corresponding: 

332 if sb.top_bottom in valid: 

333 pos = round(sb.position[2], 1) 

334 if pos not in leveled_sbs: 

335 leveled_sbs[pos] = [] 

336 leveled_sbs[pos].append(sb) 

337 

338 return leveled_sbs 

339 

340 def _area_specific_post_processing(self, value): 

341 return value / self.net_area 

342 

343 def _get_heating_profile(self, name) -> list: 

344 """returns a heating profile using the heat temperature in the IFC""" 

345 # todo make this "dynamic" with a night set back 

346 if self.t_set_heat is not None: 

347 return [self.t_set_heat.to(ureg.kelvin).m] * 24 

348 

349 def _get_cooling_profile(self, name) -> list: 

350 """returns a cooling profile using the cool temperature in the IFC""" 

351 # todo make this "dynamic" with a night set back 

352 if self.t_set_cool is not None: 

353 return [self.t_set_cool.to(ureg.kelvin).m] * 24 

354 

355 def _get_persons(self, name): 

356 if self.area_per_occupant: 

357 return 1 / self.area_per_occupant 

358 

359 external_orientation = attribute.Attribute( 

360 description="Orientation of the thermal zone, either 'Internal' or a " 

361 "list of 2 angles or a single angle as value between 0 and " 

362 "360.", 

363 functions=[_get_external_orientation] 

364 ) 

365 

366 glass_percentage = attribute.Attribute( 

367 description="Determines the glass area/facade area ratio for all the " 

368 "windows in the space in one of the 4 following ranges:" 

369 " 0%-30%: 15, 30%-50%: 40, 50%-70%: 60, 70%-100%: 85.", 

370 functions=[_get_glass_percentage] 

371 ) 

372 

373 space_neighbors = attribute.Attribute( 

374 description="Determines the neighbors of the thermal zone.", 

375 functions=[_get_space_neighbors] 

376 ) 

377 

378 space_shape = attribute.Attribute( 

379 description="Returns topods shape of the IfcSpace.", 

380 functions=[_get_space_shape] 

381 ) 

382 

383 space_center = attribute.Attribute( 

384 description="Returns the center of the bounding box of an ifc space " 

385 "shape.", 

386 functions=[_get_space_center] 

387 ) 

388 

389 footprint_shape = attribute.Attribute( 

390 description="Returns the footprint of a space shape, which can be " 

391 "used e.g., to visualize floor plans.", 

392 functions=[_get_footprint_shape] 

393 ) 

394 

395 horizontal_sbs = attribute.Attribute( 

396 description="All horizontal space boundaries in a zone as dict. Key is" 

397 " the z-zeight in the room and value the SB.", 

398 functions=[_get_horizontal_sbs] 

399 ) 

400 

401 zone_name = attribute.Attribute( 

402 default_ps=("Pset_SpaceCommon", "Reference") 

403 ) 

404 

405 name = attribute.Attribute( 

406 functions=[_get_name] 

407 ) 

408 

409 usage = attribute.Attribute( 

410 default_ps=("Pset_SpaceOccupancyRequirements", "OccupancyType"), 

411 functions=[_get_usage] 

412 ) 

413 

414 t_set_heat = attribute.Attribute( 

415 default_ps=("Pset_SpaceThermalRequirements", "SpaceTemperatureMin"), 

416 unit=ureg.degC, 

417 ) 

418 

419 t_set_cool = attribute.Attribute( 

420 default_ps=("Pset_SpaceThermalRequirements", "SpaceTemperatureMax"), 

421 unit=ureg.degC, 

422 ) 

423 

424 t_ground = attribute.Attribute( 

425 unit=ureg.degC, 

426 default=13, 

427 ) 

428 

429 max_humidity = attribute.Attribute( 

430 default_ps=("Pset_SpaceThermalRequirements", "SpaceHumidityMax"), 

431 unit=ureg.dimensionless, 

432 ) 

433 

434 min_humidity = attribute.Attribute( 

435 default_ps=("Pset_SpaceThermalRequirements", "SpaceHumidityMin"), 

436 unit=ureg.dimensionless, 

437 ) 

438 

439 natural_ventilation = attribute.Attribute( 

440 default_ps=("Pset_SpaceThermalRequirements", "NaturalVentilation"), 

441 ) 

442 

443 natural_ventilation_rate = attribute.Attribute( 

444 default_ps=("Pset_SpaceThermalRequirements", "NaturalVentilationRate"), 

445 unit=1 / ureg.hour, 

446 ) 

447 

448 mechanical_ventilation_rate = attribute.Attribute( 

449 default_ps=("Pset_SpaceThermalRequirements", 

450 "MechanicalVentilationRate"), 

451 unit=1 / ureg.hour, 

452 ) 

453 

454 with_ahu = attribute.Attribute( 

455 default_ps=("Pset_SpaceThermalRequirements", "AirConditioning"), 

456 ) 

457 

458 central_ahu = attribute.Attribute( 

459 default_ps=("Pset_SpaceThermalRequirements", "AirConditioningCentral"), 

460 ) 

461 

462 gross_area = attribute.Attribute( 

463 default_ps=("Qto_SpaceBaseQuantities", "GrossFloorArea"), 

464 functions=[get_bound_floor_area], 

465 unit=ureg.meter ** 2 

466 ) 

467 

468 net_area = attribute.Attribute( 

469 default_ps=("Qto_SpaceBaseQuantities", "NetFloorArea"), 

470 functions=[get_net_bound_floor_area], 

471 unit=ureg.meter ** 2 

472 ) 

473 

474 net_wall_area = attribute.Attribute( 

475 default_ps=("Qto_SpaceBaseQuantities", "NetWallArea"), 

476 unit=ureg.meter ** 2 

477 ) 

478 

479 net_ceiling_area = attribute.Attribute( 

480 default_ps=("Qto_SpaceBaseQuantities", "NetCeilingArea"), 

481 unit=ureg.meter ** 2 

482 ) 

483 

484 net_volume = attribute.Attribute( 

485 default_ps=("Qto_SpaceBaseQuantities", "NetVolume"), 

486 functions=[_get_space_shape_volume, _get_volume_geometric], 

487 unit=ureg.meter ** 3, 

488 ) 

489 gross_volume = attribute.Attribute( 

490 default_ps=("Qto_SpaceBaseQuantities", "GrossVolume"), 

491 functions=[_get_volume_geometric], 

492 unit=ureg.meter ** 3, 

493 ) 

494 

495 height = attribute.Attribute( 

496 default_ps=("Qto_SpaceBaseQuantities", "Height"), 

497 unit=ureg.meter, 

498 ) 

499 

500 length = attribute.Attribute( 

501 default_ps=("Qto_SpaceBaseQuantities", "Length"), 

502 unit=ureg.meter, 

503 ) 

504 

505 width = attribute.Attribute( 

506 default_ps=("Qto_SpaceBaseQuantities", "Width"), 

507 unit=ureg.m 

508 ) 

509 

510 area_per_occupant = attribute.Attribute( 

511 default_ps=("Pset_SpaceOccupancyRequirements", "AreaPerOccupant"), 

512 unit=ureg.meter ** 2 

513 ) 

514 

515 space_shape_volume = attribute.Attribute( 

516 functions=[_get_space_shape_volume], 

517 unit=ureg.meter ** 3, 

518 ) 

519 

520 clothing_persons = attribute.Attribute( 

521 default_ps=("", "") 

522 ) 

523 

524 surround_clo_persons = attribute.Attribute( 

525 default_ps=("", "") 

526 ) 

527 

528 heating_profile = attribute.Attribute( 

529 functions=[_get_heating_profile], 

530 ) 

531 

532 cooling_profile = attribute.Attribute( 

533 functions=[_get_cooling_profile], 

534 ) 

535 

536 persons = attribute.Attribute( 

537 functions=[_get_persons], 

538 ) 

539 

540 # use conditions 

541 with_cooling = attribute.Attribute( 

542 ) 

543 

544 with_heating = attribute.Attribute( 

545 ) 

546 

547 T_threshold_heating = attribute.Attribute( 

548 ) 

549 

550 activity_degree_persons = attribute.Attribute( 

551 ) 

552 

553 fixed_heat_flow_rate_persons = attribute.Attribute( 

554 default_ps=("Pset_SpaceThermalLoad", "People"), 

555 unit=ureg.W, 

556 ) 

557 

558 internal_gains_moisture_no_people = attribute.Attribute( 

559 ) 

560 

561 T_threshold_cooling = attribute.Attribute( 

562 ) 

563 

564 ratio_conv_rad_persons = attribute.Attribute( 

565 default=0.5, 

566 ) 

567 

568 ratio_conv_rad_machines = attribute.Attribute( 

569 default=0.5, 

570 ) 

571 

572 ratio_conv_rad_lighting = attribute.Attribute( 

573 default=0.5, 

574 ) 

575 

576 machines = attribute.Attribute( 

577 description="Specific internal gains through machines, if taken from" 

578 " IFC property set a division by thermal zone area is" 

579 " needed.", 

580 default_ps=("Pset_SpaceThermalLoad", "EquipmentSensible"), 

581 ifc_postprocessing=_area_specific_post_processing, 

582 unit=ureg.W / (ureg.meter ** 2), 

583 ) 

584 

585 def _calc_lighting_power(self, name) -> float: 

586 if self.use_maintained_illuminance: 

587 return self.maintained_illuminance / self.lighting_efficiency_lumen 

588 else: 

589 return self.fixed_lighting_power 

590 

591 lighting_power = attribute.Attribute( 

592 description="Specific lighting power in W/m2. If taken from IFC" 

593 " property set a division by thermal zone area is needed.", 

594 default_ps=("Pset_SpaceThermalLoad", "Lighting"), 

595 ifc_postprocessing=_area_specific_post_processing, 

596 functions=[_calc_lighting_power], 

597 unit=ureg.W / (ureg.meter ** 2), 

598 ) 

599 

600 fixed_lighting_power = attribute.Attribute( 

601 description="Specific fixed electrical power for lighting in W/m2. " 

602 "This value is taken from SIA 2024.", 

603 unit=ureg.W / (ureg.meter ** 2) 

604 ) 

605 

606 maintained_illuminance = attribute.Attribute( 

607 description="Maintained illuminance value for lighting. This value is" 

608 " taken from SIA 2024.", 

609 unit=ureg.lumen / (ureg.meter ** 2) 

610 ) 

611 

612 use_maintained_illuminance = attribute.Attribute( 

613 description="Decision variable to determine if lighting_power will" 

614 " be given by fixed_lighting_power or by calculation " 

615 "using the variables maintained_illuminance and " 

616 "lighting_efficiency_lumen. This is not available in IFC " 

617 "and can be set through the sim_setting with equivalent " 

618 "name. " 

619 ) 

620 

621 lighting_efficiency_lumen = attribute.Attribute( 

622 description="Lighting efficiency in lm/W_el, in german: Lichtausbeute.", 

623 unit=ureg.lumen / ureg.W 

624 ) 

625 

626 use_constant_infiltration = attribute.Attribute( 

627 ) 

628 

629 base_infiltration = attribute.Attribute( 

630 ) 

631 

632 max_user_infiltration = attribute.Attribute( 

633 ) 

634 

635 max_overheating_infiltration = attribute.Attribute( 

636 ) 

637 

638 max_summer_infiltration = attribute.Attribute( 

639 ) 

640 

641 winter_reduction_infiltration = attribute.Attribute( 

642 ) 

643 

644 min_ahu = attribute.Attribute( 

645 ) 

646 

647 max_ahu = attribute.Attribute( 

648 default_ps=("Pset_AirSideSystemInformation", "TotalAirflow"), 

649 unit=ureg.meter ** 3 / ureg.s 

650 ) 

651 

652 with_ideal_thresholds = attribute.Attribute( 

653 ) 

654 

655 persons_profile = attribute.Attribute( 

656 ) 

657 

658 machines_profile = attribute.Attribute( 

659 ) 

660 

661 lighting_profile = attribute.Attribute( 

662 ) 

663 

664 def get__elements_by_type(self, type): 

665 raise NotImplementedError 

666 

667 def __repr__(self): 

668 return "<%s (usage: %s)>" \ 

669 % (self.__class__.__name__, self.usage) 

670 

671class ExternalSpatialElement(ThermalZone): 

672 ifc_types = { 

673 "IfcExternalSpatialElement": 

674 ['*'] 

675 } 

676 

677 

678class SpaceBoundary(RelationBased): 

679 ifc_types = {'IfcRelSpaceBoundary': ['*']} 

680 

681 def __init__(self, *args, elements: dict, **kwargs): 

682 """spaceboundary __init__ function""" 

683 super().__init__(*args, **kwargs) 

684 self.disaggregation = [] 

685 self.bound_element = None 

686 self.disagg_parent = None 

687 self.bound_thermal_zone = None 

688 self._elements = elements 

689 self.parent_bound = None 

690 self.opening_bounds = [] 

691 

692 def _calc_position(self, name): 

693 """ 

694 calculates the position of the spaceboundary, using the relative 

695 position of resultant disaggregation 

696 """ 

697 if hasattr(self.ifc.ConnectionGeometry.SurfaceOnRelatingElement, 

698 'BasisSurface'): 

699 position = self.ifc.ConnectionGeometry.SurfaceOnRelatingElement. \ 

700 BasisSurface.Position.Location.Coordinates 

701 else: 

702 position = self.ifc.ConnectionGeometry.SurfaceOnRelatingElement. \ 

703 Position.Location.Coordinates 

704 

705 return position 

706 

707 @classmethod 

708 def pre_validate(cls, ifc) -> bool: 

709 return True 

710 

711 def validate_creation(self) -> bool: 

712 if self.bound_area and self.bound_area < 1e-2 * ureg.meter ** 2: 

713 return True 

714 return False 

715 

716 def get_bound_area(self, name) -> ureg.Quantity: 

717 """compute area of a space boundary""" 

718 bound_prop = GProp_GProps() 

719 brepgprop.SurfaceProperties(self.bound_shape, bound_prop) 

720 area = bound_prop.Mass() 

721 return area * ureg.meter ** 2 

722 

723 bound_area = attribute.Attribute( 

724 description="The area bound by the space boundary.", 

725 unit=ureg.meter ** 2, 

726 functions=[get_bound_area] 

727 ) 

728 

729 def _get_top_bottom(self, name) -> BoundaryOrientation: 

730 """ 

731 Determines if a boundary is a top (ceiling/roof) or bottom (floor/slab) 

732 element based solely on its normal vector orientation. 

733 

734 Classification is based on the dot product between the boundary's 

735 normal vector and the vertical vector (0, 0, 1): 

736 - TOP: when normal points upward (dot product > cos(89°)) 

737 - BOTTOM: when normal points downward (dot product < cos(91°)) 

738 - VERTICAL: when normal is perpendicular to vertical (dot product ≈ 0) 

739 

740 Returns: 

741 BoundaryOrientation: Enumerated orientation classification 

742 """ 

743 vertical_vector = gp_XYZ(0.0, 0.0, 1.0) 

744 cos_angle_top = math.cos(math.radians(89)) 

745 cos_angle_bottom = math.cos(math.radians(91)) 

746 

747 normal_dot_vertical = vertical_vector.Dot(self.bound_normal) 

748 

749 # Classify based on dot product 

750 if normal_dot_vertical > cos_angle_top: 

751 return BoundaryOrientation.top 

752 elif normal_dot_vertical < cos_angle_bottom: 

753 return BoundaryOrientation.bottom 

754 

755 return BoundaryOrientation.vertical 

756 

757 def _get_bound_center(self, name): 

758 """ compute center of the bounding box of a space boundary""" 

759 p = GProp_GProps() 

760 brepgprop.SurfaceProperties(self.bound_shape, p) 

761 return p.CentreOfMass().XYZ() 

762 

763 def _get_related_bound(self, name): 

764 """ 

765 Get corresponding space boundary in another space, 

766 ensuring that corresponding space boundaries have a matching number of 

767 vertices. 

768 """ 

769 if hasattr(self.ifc, 'CorrespondingBoundary') and \ 

770 self.ifc.CorrespondingBoundary is not None: 

771 corr_bound = self._elements.get( 

772 self.ifc.CorrespondingBoundary.GlobalId) 

773 if corr_bound: 

774 nb_vert_this = PyOCCTools.get_number_of_vertices( 

775 self.bound_shape) 

776 nb_vert_other = PyOCCTools.get_number_of_vertices( 

777 corr_bound.bound_shape) 

778 # if not nb_vert_this == nb_vert_other: 

779 # print("NO VERT MATCH!:", nb_vert_this, nb_vert_other) 

780 if nb_vert_this == nb_vert_other: 

781 return corr_bound 

782 else: 

783 # deal with a mismatch of vertices, due to different 

784 # triangulation or for other reasons. Only applicable for 

785 # small differences in the bound area between the 

786 # corresponding surfaces 

787 if abs(self.bound_area.m - corr_bound.bound_area.m) < 0.01: 

788 # get points of the current space boundary 

789 p = PyOCCTools.get_points_of_face(self.bound_shape) 

790 # reverse the points and create a new face. Points 

791 # have to be reverted, otherwise it would result in an 

792 # incorrectly oriented surface normal 

793 p.reverse() 

794 new_corr_shape = PyOCCTools.make_faces_from_pnts(p) 

795 # move the new shape of the corresponding boundary to 

796 # the original position of the corresponding boundary 

797 new_moved_corr_shape = ( 

798 PyOCCTools.move_bounds_to_vertical_pos([ 

799 new_corr_shape], corr_bound.bound_shape))[0] 

800 # assign the new shape to the original shape and 

801 # return the new corresponding boundary 

802 corr_bound.bound_shape = new_moved_corr_shape 

803 return corr_bound 

804 if self.bound_element is None: 

805 # return None 

806 # check for virtual bounds 

807 if not self.physical: 

808 corr_bound = None 

809 # cover virtual space boundaries without related IfcVirtualElement 

810 if not self.ifc.RelatedBuildingElement: 

811 vbs = [b for b in self._elements.values() if 

812 isinstance(b, SpaceBoundary) and not 

813 b.ifc.RelatedBuildingElement] 

814 for b in vbs: 

815 if b is self: 

816 continue 

817 if b.ifc.RelatingSpace == self.ifc.RelatingSpace: 

818 continue 

819 if not (b.bound_area.m - self.bound_area.m) ** 2 < 1e-2: 

820 continue 

821 center_dist = gp_Pnt(self.bound_center).Distance( 

822 gp_Pnt(b.bound_center)) ** 2 

823 if center_dist > 0.5: 

824 continue 

825 corr_bound = b 

826 return corr_bound 

827 return None 

828 # cover virtual space boundaries related to an IfcVirtualElement 

829 if self.ifc.RelatedBuildingElement.is_a('IfcVirtualElement'): 

830 if len(self.ifc.RelatedBuildingElement.ProvidesBoundaries) == 2: 

831 for bound in self.ifc.RelatedBuildingElement.ProvidesBoundaries: 

832 if bound.GlobalId != self.ifc.GlobalId: 

833 corr_bound = self._elements[bound.GlobalId] 

834 return corr_bound 

835 elif len(self.bound_element.space_boundaries) == 1: 

836 return None 

837 elif len(self.bound_element.space_boundaries) >= 2: 

838 own_space_id = self.bound_thermal_zone.ifc.GlobalId 

839 min_dist = 1000 

840 corr_bound = None 

841 for bound in self.bound_element.space_boundaries: 

842 if bound.level_description != "2a": 

843 continue 

844 if bound is self: 

845 continue 

846 # if bound.bound_normal.Dot(self.bound_normal) != -1: 

847 # continue 

848 other_area = bound.bound_area 

849 if (other_area.m - self.bound_area.m) ** 2 > 1e-1: 

850 continue 

851 center_dist = gp_Pnt(self.bound_center).Distance( 

852 gp_Pnt(bound.bound_center)) ** 2 

853 if abs(center_dist) > 0.5: 

854 continue 

855 distance = BRepExtrema_DistShapeShape( 

856 bound.bound_shape, 

857 self.bound_shape, 

858 Extrema_ExtFlag_MIN 

859 ).Value() 

860 if distance > min_dist: 

861 continue 

862 min_dist = abs(center_dist) 

863 # self.check_for_vertex_duplicates(bound) 

864 nb_vert_this = PyOCCTools.get_number_of_vertices( 

865 self.bound_shape) 

866 nb_vert_other = PyOCCTools.get_number_of_vertices( 

867 bound.bound_shape) 

868 # if not nb_vert_this == nb_vert_other: 

869 # print("NO VERT MATCH!:", nb_vert_this, nb_vert_other) 

870 if nb_vert_this == nb_vert_other: 

871 corr_bound = bound 

872 return corr_bound 

873 else: 

874 return None 

875 

876 def _get_related_adb_bound(self, name): 

877 adb_bound = None 

878 if self.bound_element is None: 

879 return None 

880 # check for visual bounds 

881 if not self.physical: 

882 return None 

883 if self.related_bound: 

884 if self.bound_thermal_zone == self.related_bound.bound_thermal_zone: 

885 adb_bound = self.related_bound 

886 return adb_bound 

887 for bound in self.bound_element.space_boundaries: 

888 if bound == self: 

889 continue 

890 if not bound.bound_thermal_zone == self.bound_thermal_zone: 

891 continue 

892 if abs(bound.bound_area.m - self.bound_area.m) > 1e-3: 

893 continue 

894 if all([abs(i) < 1e-3 for i in 

895 ((self.bound_normal - bound.bound_normal).Coord())]): 

896 continue 

897 if gp_Pnt(bound.bound_center).Distance( 

898 gp_Pnt(self.bound_center)) < 0.4: 

899 adb_bound = bound 

900 return adb_bound 

901 

902 related_adb_bound = attribute.Attribute( 

903 description="Related adiabatic boundary.", 

904 functions=[_get_related_adb_bound] 

905 ) 

906 

907 def _get_is_physical(self, name) -> bool: 

908 """ 

909 This function returns True if the spaceboundary is physical 

910 """ 

911 return self.ifc.PhysicalOrVirtualBoundary.lower() == 'physical' 

912 

913 def _get_bound_shape(self, name): 

914 settings = ifcopenshell.geom.settings() 

915 settings.set(settings.USE_PYTHON_OPENCASCADE, True) 

916 settings.set(settings.USE_WORLD_COORDS, True) 

917 settings.set(settings.PRECISION, 1e-6) 

918 settings.set( 

919 "dimensionality", 

920 ifcopenshell.ifcopenshell_wrapper.CURVES_SURFACES_AND_SOLIDS) # 2 

921 # settings.set(settings.EXCLUDE_SOLIDS_AND_SURFACES, False) 

922 # settings.set(settings.INCLUDE_CURVES, True) 

923 

924 # check if the space boundary shapes need a unit conversion (i.e., 

925 # an additional transformation to the correct size and position) 

926 length_unit = self.ifc_units.get('IfcLengthMeasure'.lower()) 

927 conv_required = length_unit != ureg.meter 

928 

929 try: 

930 sore = self.ifc.ConnectionGeometry.SurfaceOnRelatingElement 

931 # if sore.get_info()["InnerBoundaries"] is None: 

932 if sore.InnerBoundaries is None: 

933 sore.InnerBoundaries = () 

934 shape = ifcopenshell.geom.create_shape(settings, sore) 

935 if sore.InnerBoundaries: 

936 # shape = remove_inner_loops(shape) # todo: return None if not horizontal shape 

937 # if not shape: 

938 if self.bound_element.ifc.is_a( 

939 'IfcWall'): # todo: remove this hotfix (generalize) 

940 ifc_new = ifcopenshell.file() 

941 temp_sore = ifc_new.create_entity('IfcCurveBoundedPlane', 

942 OuterBoundary=sore.OuterBoundary, 

943 BasisSurface=sore.BasisSurface) 

944 temp_sore.InnerBoundaries = () 

945 shape = ifcopenshell.geom.create_shape(settings, temp_sore) 

946 else: 

947 # hotfix: ifcopenshell 0.8.4 does not sufficiently produce 

948 # faces with inner holes (as opposed to ifcopenshell 

949 # 0.7.0). This workaround "manually" performs a boolean 

950 # operation to generate a TopoDS_Shape with inner holes 

951 # before removing the inner loops with the dedicated 

952 # inner_loop_remover function 

953 ### START OF HOTFIX ##### 

954 inners = [] 

955 for inn in sore.InnerBoundaries: 

956 ifc_new = ifcopenshell.file() 

957 temp_sore = ifc_new.create_entity( 

958 'IfcCurveBoundedPlane', 

959 OuterBoundary=inn, 

960 BasisSurface=sore.BasisSurface) 

961 temp_sore.InnerBoundaries = () 

962 compound = ifcopenshell.geom.create_shape(settings, 

963 temp_sore) 

964 faces = PyOCCTools.get_face_from_shape(compound) 

965 inners.append(faces) 

966 sore.InnerBoundaries = () 

967 outer_shape_data = ifcopenshell.geom.create_shape(settings, 

968 sore) 

969 shape = PyOCCTools.triangulate_bound_shape( 

970 outer_shape_data, inners) 

971 #### END OF HOTFIX #### 

972 shape = remove_inner_loops(shape) 

973 if not (sore.InnerBoundaries and not self.bound_element.ifc.is_a( 

974 'IfcWall')): 

975 faces = PyOCCTools.get_faces_from_shape(shape) 

976 if len(faces) > 1: 

977 unify = ShapeUpgrade_UnifySameDomain() 

978 unify.Initialize(shape) 

979 unify.Build() 

980 shape = unify.Shape() 

981 faces = PyOCCTools.get_faces_from_shape(shape) 

982 face = faces[0] 

983 face = PyOCCTools.remove_coincident_and_collinear_points_from_face( 

984 face) 

985 shape = face 

986 except: 

987 try: 

988 sore = self.ifc.ConnectionGeometry.SurfaceOnRelatingElement 

989 ifc_new = ifcopenshell.file() 

990 temp_sore = ifc_new.create_entity('IfcCurveBoundedPlane', 

991 OuterBoundary=sore.OuterBoundary, 

992 BasisSurface=sore.BasisSurface) 

993 temp_sore.InnerBoundaries = () 

994 shape = ifcopenshell.geom.create_shape(settings, temp_sore) 

995 except: 

996 poly = self.ifc.ConnectionGeometry.SurfaceOnRelatingElement.OuterBoundary.Points 

997 pnts = [] 

998 for p in poly: 

999 p.Coordinates = (p.Coordinates[0], p.Coordinates[1], 0.0) 

1000 pnts.append((p.Coordinates[:])) 

1001 shape = PyOCCTools.make_faces_from_pnts(pnts) 

1002 shape = BRepLib_FuseEdges(shape).Shape() 

1003 

1004 if conv_required: 

1005 # scale newly created shape of space boundary to correct size 

1006 conv_factor = (1 * length_unit).to( 

1007 ureg.metre).m 

1008 # shape scaling seems to be covered by ifcopenshell, obsolete 

1009 # shape = PyOCCTools.scale_shape(shape, conv_factor, gp_Pnt(0, 0, 

1010 # 0)) 

1011 

1012 if self.ifc.RelatingSpace.ObjectPlacement: 

1013 lp = PyOCCTools.local_placement( 

1014 self.ifc.RelatingSpace.ObjectPlacement).tolist() 

1015 # transform newly created shape of space boundary to correct 

1016 # position if a unit conversion is required. 

1017 if conv_required: 

1018 for i in range(len(lp)): 

1019 for j in range(len(lp[i])): 

1020 coord = lp[i][j] * length_unit 

1021 lp[i][j] = coord.to(ureg.meter).m 

1022 mat = gp_Mat(lp[0][0], lp[0][1], lp[0][2], lp[1][0], lp[1][1], 

1023 lp[1][2], lp[2][0], lp[2][1], lp[2][2]) 

1024 vec = gp_Vec(lp[0][3], lp[1][3], lp[2][3]) 

1025 trsf = gp_Trsf() 

1026 trsf.SetTransformation(gp_Quaternion(mat), vec) 

1027 shape = BRepBuilderAPI_Transform(shape, trsf).Shape() 

1028 

1029 # shape = shape.Reversed() 

1030 unify = ShapeUpgrade_UnifySameDomain() 

1031 unify.Initialize(shape) 

1032 unify.Build() 

1033 shape = unify.Shape() 

1034 

1035 if self.bound_element is not None: 

1036 bi = self.bound_element 

1037 if not hasattr(bi, "related_openings"): 

1038 return shape 

1039 if len(bi.related_openings) == 0: 

1040 return shape 

1041 shape = PyOCCTools.get_face_from_shape(shape) 

1042 return shape 

1043 

1044 def get_level_description(self, name) -> str: 

1045 """ 

1046 This function returns the level description of the spaceboundary 

1047 """ 

1048 return self.ifc.Description 

1049 

1050 def _get_is_external(self, name) -> Union[None, bool]: 

1051 """ 

1052 This function returns True if the spaceboundary is external 

1053 """ 

1054 if self.ifc.InternalOrExternalBoundary is not None: 

1055 ifc_ext_internal = self.ifc.InternalOrExternalBoundary.lower() 

1056 if ifc_ext_internal == 'internal': 

1057 return False 

1058 elif 'external' in ifc_ext_internal: 

1059 return True 

1060 else: 

1061 return None 

1062 # return not self.ifc.InternalOrExternalBoundary.lower() == 'internal' 

1063 

1064 def _get_opening_area(self, name): 

1065 """ 

1066 This function returns the opening area of the spaceboundary 

1067 """ 

1068 if self.opening_bounds: 

1069 return sum(opening_boundary.bound_area for opening_boundary 

1070 in self.opening_bounds) 

1071 return 0 

1072 

1073 def _get_net_bound_area(self, name): 

1074 """ 

1075 This function returns the net bound area of the spaceboundary 

1076 """ 

1077 return self.bound_area - self.opening_area 

1078 

1079 is_external = attribute.Attribute( 

1080 description="True if the Space Boundary is external", 

1081 functions=[_get_is_external] 

1082 ) 

1083 

1084 bound_shape = attribute.Attribute( 

1085 description="Bound shape element of the SB.", 

1086 functions=[_get_bound_shape] 

1087 ) 

1088 

1089 top_bottom = attribute.Attribute( 

1090 description="Info if the SB is top " 

1091 "(ceiling etc.) or bottom (floor etc.).", 

1092 functions=[_get_top_bottom] 

1093 ) 

1094 

1095 bound_center = attribute.Attribute( 

1096 description="The center of the space boundary.", 

1097 functions=[_get_bound_center] 

1098 ) 

1099 

1100 related_bound = attribute.Attribute( 

1101 description="Related space boundary.", 

1102 functions=[_get_related_bound] 

1103 ) 

1104 

1105 physical = attribute.Attribute( 

1106 description="If the Space Boundary is physical or not.", 

1107 functions=[_get_is_physical] 

1108 ) 

1109 

1110 opening_area = attribute.Attribute( 

1111 description="Opening area of the Space Boundary.", 

1112 functions = [_get_opening_area] 

1113 ) 

1114 

1115 net_bound_area = attribute.Attribute( 

1116 description="Net bound area of the Space Boundary", 

1117 functions=[_get_net_bound_area] 

1118 ) 

1119 

1120 def _get_bound_normal(self, name): 

1121 """ 

1122 This function returns the normal vector of the spaceboundary 

1123 """ 

1124 return PyOCCTools.simple_face_normal(self.bound_shape) 

1125 

1126 bound_normal = attribute.Attribute( 

1127 description="Normal vector of the Space Boundary.", 

1128 functions=[_get_bound_normal] 

1129 ) 

1130 

1131 level_description = attribute.Attribute( 

1132 functions=[get_level_description], 

1133 # Todo this should be removed in near future. We should either  

1134 # find # a way to distinguish the level of SB by something 

1135 # different or should check this during the creation of SBs 

1136 # and throw an error if the level is not defined. 

1137 default='2a' 

1138 # HACK: Rou's Model has 2a boundaries but, the description is None, 

1139 # default set to 2a to temporary solve this problem 

1140 ) 

1141 

1142 internal_external_type = attribute.Attribute( 

1143 description="Defines, whether the Space Boundary is internal" 

1144 " (Internal), or external, i.e. adjacent to open space " 

1145 "(that can be an partially enclosed space, such as terrace" 

1146 " (External", 

1147 ifc_attr_name="InternalOrExternalBoundary" 

1148 ) 

1149 

1150 

1151class ExtSpatialSpaceBoundary(SpaceBoundary): 

1152 """describes all space boundaries related to an IfcExternalSpatialElement instead of an IfcSpace""" 

1153 pass 

1154 

1155 

1156class SpaceBoundary2B(SpaceBoundary): 

1157 """describes all newly created space boundaries of type 2b to fill gaps within spaces""" 

1158 

1159 def __init__(self, *args, elements=None, **kwargs): 

1160 super(SpaceBoundary2B, self).__init__(*args, elements=None, **kwargs) 

1161 self.ifc = ifcopenshell.create_entity('IfcRelSpaceBoundary') 

1162 self.guid = None 

1163 self.bound_shape = None 

1164 self.thermal_zones = [] 

1165 self.bound_element = None 

1166 self.physical = True 

1167 self.is_external = False 

1168 self.related_bound = None 

1169 self.related_adb_bound = None 

1170 self.level_description = '2b' 

1171 

1172 

1173class BPSProductWithLayers(BPSProduct): 

1174 ifc_types = {} 

1175 

1176 def __init__(self, *args, **kwargs): 

1177 """BPSProductWithLayers __init__ function. 

1178 

1179 Convention in bim2sim for layerset is layer 0 is inside, 

1180 layer n is outside. 

1181 """ 

1182 super().__init__(*args, **kwargs) 

1183 self.layerset = None 

1184 

1185 def get_u_value(self, name): 

1186 """wall get_u_value function""" 

1187 layers_r = 0 

1188 for layer in self.layerset.layers: 

1189 if layer.thickness: 

1190 if layer.material.thermal_conduc and \ 

1191 layer.material.thermal_conduc > 0: 

1192 layers_r += layer.thickness / layer.material.thermal_conduc 

1193 

1194 if layers_r > 0: 

1195 return 1 / layers_r 

1196 return None 

1197 

1198 def get_thickness_by_layers(self, name): 

1199 """calculate the total thickness of the product based on the thickness 

1200 of each layer.""" 

1201 thickness = 0 

1202 for layer in self.layerset.layers: 

1203 if layer.thickness: 

1204 thickness += layer.thickness 

1205 return thickness 

1206 

1207 

1208class Wall(BPSProductWithLayers): 

1209 """Abstract wall class, only its subclasses Inner- and Outerwalls are used. 

1210 

1211 Every element where self.is_external is not True, is an InnerWall. 

1212 """ 

1213 ifc_types = { 

1214 "IfcWall": 

1215 ['*', 'MOVABLE', 'PARAPET', 'PARTITIONING', 'PLUMBINGWALL', 

1216 'SHEAR', 'SOLIDWALL', 'POLYGONAL', 'DOOR', 'GATE', 'TRAPDOOR'], 

1217 "IfcWallStandardCase": 

1218 ['*', 'MOVABLE', 'PARAPET', 'PARTITIONING', 'PLUMBINGWALL', 

1219 'SHEAR', 'SOLIDWALL', 'POLYGONAL', 'DOOR', 'GATE', 'TRAPDOOR'], 

1220 "IfcColumn": ['*'], # Hotfix. TODO: Implement appropriate classes 

1221 "IfcCurtainWall": ['*'] # Hotfix. TODO: Implement appropriate classes 

1222 # "IfcElementedCase": "?" # TODO 

1223 } 

1224 

1225 conditions = [ 

1226 condition.RangeCondition('u_value', 

1227 0 * ureg.W / ureg.K / ureg.meter ** 2, 

1228 5 * ureg.W / ureg.K / ureg.meter ** 2, 

1229 critical_for_creation=False), 

1230 condition.UValueCondition('u_value', 

1231 threshold=0.2, 

1232 critical_for_creation=False), 

1233 ] 

1234 

1235 pattern_ifc_type = [ 

1236 re.compile('Wall', flags=re.IGNORECASE), 

1237 re.compile('Wand', flags=re.IGNORECASE) 

1238 ] 

1239 

1240 def __init__(self, *args, **kwargs): 

1241 """wall __init__ function""" 

1242 super().__init__(*args, **kwargs) 

1243 

1244 def get_better_subclass(self): 

1245 return OuterWall if self.is_external else InnerWall 

1246 

1247 net_area = attribute.Attribute( 

1248 default_ps=("Qto_WallBaseQuantities", "NetSideArea"), 

1249 functions=[BPSProduct.get_net_bound_area], 

1250 unit=ureg.meter ** 2 

1251 ) 

1252 

1253 gross_area = attribute.Attribute( 

1254 default_ps=("Qto_WallBaseQuantities", "GrossSideArea"), 

1255 functions=[BPSProduct.get_bound_area], 

1256 unit=ureg.meter ** 2 

1257 ) 

1258 

1259 tilt = attribute.Attribute( 

1260 default=90 

1261 ) 

1262 

1263 u_value = attribute.Attribute( 

1264 default_ps=("Pset_WallCommon", "ThermalTransmittance"), 

1265 unit=ureg.W / ureg.K / ureg.meter ** 2, 

1266 functions=[BPSProductWithLayers.get_u_value], 

1267 ) 

1268 

1269 width = attribute.Attribute( 

1270 default_ps=("Qto_WallBaseQuantities", "Width"), 

1271 functions=[BPSProductWithLayers.get_thickness_by_layers], 

1272 unit=ureg.m 

1273 ) 

1274 

1275 inner_convection = attribute.Attribute( 

1276 unit=ureg.W / ureg.K / ureg.meter ** 2, 

1277 default=0.6 

1278 ) 

1279 

1280 is_load_bearing = attribute.Attribute( 

1281 default_ps=("Pset_WallCommon", "LoadBearing"), 

1282 ) 

1283 

1284 net_volume = attribute.Attribute( 

1285 default_ps=("Qto_WallBaseQuantities", "NetVolume"), 

1286 unit=ureg.meter ** 3 

1287 ) 

1288 

1289 gross_volume = attribute.Attribute( 

1290 default_ps=("Qto_WallBaseQuantities", "GrossVolume") 

1291 ) 

1292 

1293 

1294class Layer(BPSProduct): 

1295 """Represents the IfcMaterialLayer class.""" 

1296 ifc_types = { 

1297 "IfcMaterialLayer": ["*"], 

1298 } 

1299 guid_prefix = "Layer_" 

1300 

1301 conditions = [ 

1302 condition.RangeCondition('thickness', 

1303 0 * ureg.m, 

1304 10 * ureg.m, 

1305 critical_for_creation=False, incl_edges=False) 

1306 ] 

1307 

1308 def __init__(self, *args, **kwargs): 

1309 """layer __init__ function""" 

1310 super().__init__(*args, **kwargs) 

1311 self.to_layerset: List[LayerSet] = [] 

1312 self.parent = None 

1313 self.material = None 

1314 

1315 @staticmethod 

1316 def get_id(prefix=""): 

1317 prefix_length = len(prefix) 

1318 if prefix_length > 10: 

1319 raise AttributeError("Max prefix length is 10!") 

1320 ifcopenshell_guid = guid.new()[prefix_length + 1:] 

1321 return f"{prefix}{ifcopenshell_guid}" 

1322 

1323 @classmethod 

1324 def pre_validate(cls, ifc) -> bool: 

1325 return True 

1326 

1327 def validate_creation(self) -> bool: 

1328 return True 

1329 

1330 def _get_thickness(self, name): 

1331 """layer thickness function""" 

1332 if hasattr(self.ifc, 'LayerThickness'): 

1333 return self.ifc.LayerThickness * ureg.meter 

1334 else: 

1335 return float('nan') * ureg.meter 

1336 

1337 thickness = attribute.Attribute( 

1338 unit=ureg.m, 

1339 functions=[_get_thickness] 

1340 ) 

1341 

1342 is_ventilated = attribute.Attribute( 

1343 description="Indication of whether the material layer represents an " 

1344 "air layer (or cavity).", 

1345 ifc_attr_name="IsVentilated", 

1346 ) 

1347 

1348 description = attribute.Attribute( 

1349 description="Definition of the material layer in more descriptive " 

1350 "terms than given by attributes Name or Category.", 

1351 ifc_attr_name="Description", 

1352 ) 

1353 

1354 category = attribute.Attribute( 

1355 description="Category of the material layer, e.g. the role it has in" 

1356 " the layer set it belongs to (such as 'load bearing', " 

1357 "'thermal insulation' etc.). The list of keywords might be" 

1358 " extended by model view definitions, however the " 

1359 "following keywords shall apply in general:", 

1360 ifc_attr_name="Category", 

1361 ) 

1362 

1363 def __repr__(self): 

1364 return "<%s (material: %s>" \ 

1365 % (self.__class__.__name__, self.material) 

1366 

1367 

1368class LayerSet(BPSProduct): 

1369 """Represents a Layerset in bim2sim. 

1370 

1371 Convention in bim2sim for layerset is layer 0 is inside, 

1372 layer n is outside. 

1373 

1374 # TODO: when not enriching we currently don't check layer orientation. 

1375 """ 

1376 

1377 ifc_types = { 

1378 "IfcMaterialLayerSet": ["*"], 

1379 } 

1380 

1381 guid_prefix = "LayerSet_" 

1382 conditions = [ 

1383 condition.ListCondition('layers', 

1384 critical_for_creation=False), 

1385 condition.ThicknessCondition('total_thickness', 

1386 threshold=0.2, 

1387 critical_for_creation=False), 

1388 ] 

1389 

1390 def __init__(self, *args, **kwargs): 

1391 """layerset __init__ function""" 

1392 super().__init__(*args, **kwargs) 

1393 self.parents: List[BPSProductWithLayers] = [] 

1394 self.layers: List[Layer] = [] 

1395 

1396 @staticmethod 

1397 def get_id(prefix=""): 

1398 prefix_length = len(prefix) 

1399 if prefix_length > 10: 

1400 raise AttributeError("Max prefix length is 10!") 

1401 ifcopenshell_guid = guid.new()[prefix_length + 1:] 

1402 return f"{prefix}{ifcopenshell_guid}" 

1403 

1404 def get_total_thickness(self, name): 

1405 if hasattr(self.ifc, 'TotalThickness'): 

1406 if self.ifc.TotalThickness: 

1407 return self.ifc.TotalThickness * ureg.m 

1408 return sum(layer.thickness for layer in self.layers) 

1409 

1410 def _get_volume(self, name): 

1411 if hasattr(self, "net_volume"): 

1412 if self.net_volume: 

1413 vol = self.net_volume 

1414 return vol 

1415 # TODO This is not working currently, because with multiple parents 

1416 # we dont know the area or width of the parent 

1417 # elif self.parent.width: 

1418 # vol = self.parent.volume * self.parent.width / self.thickness 

1419 else: 

1420 vol = float('nan') * ureg.meter ** 3 

1421 # TODO see above 

1422 # elif self.parent.width: 

1423 # vol = self.parent.volume * self.parent.width / self.thickness 

1424 else: 

1425 vol = float('nan') * ureg.meter ** 3 

1426 return vol 

1427 

1428 thickness = attribute.Attribute( 

1429 unit=ureg.m, 

1430 functions=[get_total_thickness], 

1431 ) 

1432 

1433 name = attribute.Attribute( 

1434 description="The name by which the IfcMaterialLayerSet is known.", 

1435 ifc_attr_name="LayerSetName", 

1436 ) 

1437 

1438 volume = attribute.Attribute( 

1439 description="Volume of layer set", 

1440 functions=[_get_volume], 

1441 ) 

1442 

1443 def __repr__(self): 

1444 if self.name: 

1445 return "<%s (name: %s, layers: %d)>" \ 

1446 % (self.__class__.__name__, self.name, len(self.layers)) 

1447 else: 

1448 return "<%s (layers: %d)>" % (self.__class__.__name__, len(self.layers)) 

1449 

1450 

1451class OuterWall(Wall): 

1452 ifc_types = {} 

1453 

1454 def calc_cost_group(self) -> int: 

1455 """Calc cost group for OuterWall 

1456 

1457 Load bearing outer walls: 331 

1458 Not load bearing outer walls: 332 

1459 Rest: 330 

1460 """ 

1461 

1462 if self.is_load_bearing: 

1463 return 331 

1464 elif not self.is_load_bearing: 

1465 return 332 

1466 else: 

1467 return 330 

1468 

1469 

1470class InnerWall(Wall): 

1471 """InnerWalls are assumed to be always symmetric.""" 

1472 ifc_types = {} 

1473 

1474 def calc_cost_group(self) -> int: 

1475 """Calc cost group for InnerWall 

1476 

1477 Load bearing inner walls: 341 

1478 Not load bearing inner walls: 342 

1479 Rest: 340 

1480 """ 

1481 

1482 if self.is_load_bearing: 

1483 return 341 

1484 elif not self.is_load_bearing: 

1485 return 342 

1486 else: 

1487 return 340 

1488 

1489 

1490class Window(BPSProductWithLayers): 

1491 ifc_types = {"IfcWindow": ['*', 'WINDOW', 'SKYLIGHT', 'LIGHTDOME']} 

1492 

1493 pattern_ifc_type = [ 

1494 re.compile('Window', flags=re.IGNORECASE), 

1495 re.compile('Fenster', flags=re.IGNORECASE) 

1496 ] 

1497 

1498 def get_glazing_area(self, name): 

1499 """returns only the glazing area of the windows""" 

1500 if self.glazing_ratio: 

1501 return self.gross_area * self.glazing_ratio 

1502 return self.opening_area 

1503 

1504 def calc_cost_group(self) -> int: 

1505 """Calc cost group for Windows 

1506 

1507 Outer door: 334 

1508 """ 

1509 

1510 return 334 

1511 

1512 net_area = attribute.Attribute( 

1513 functions=[get_glazing_area], 

1514 unit=ureg.meter ** 2, 

1515 ) 

1516 

1517 gross_area = attribute.Attribute( 

1518 default_ps=("Qto_WindowBaseQuantities", "Area"), 

1519 functions=[BPSProduct.get_bound_area], 

1520 unit=ureg.meter ** 2 

1521 ) 

1522 

1523 glazing_ratio = attribute.Attribute( 

1524 default_ps=("Pset_WindowCommon", "GlazingAreaFraction"), 

1525 ) 

1526 

1527 width = attribute.Attribute( 

1528 default_ps=("Qto_WindowBaseQuantities", "Depth"), 

1529 functions=[BPSProductWithLayers.get_thickness_by_layers], 

1530 unit=ureg.m 

1531 ) 

1532 u_value = attribute.Attribute( 

1533 default_ps=("Pset_WindowCommon", "ThermalTransmittance"), 

1534 unit=ureg.W / ureg.K / ureg.meter ** 2, 

1535 functions=[BPSProductWithLayers.get_u_value], 

1536 ) 

1537 

1538 g_value = attribute.Attribute( # material 

1539 ) 

1540 

1541 a_conv = attribute.Attribute( 

1542 ) 

1543 

1544 shading_g_total = attribute.Attribute( 

1545 ) 

1546 

1547 shading_max_irr = attribute.Attribute( 

1548 ) 

1549 

1550 inner_convection = attribute.Attribute( 

1551 unit=ureg.W / ureg.K / ureg.meter ** 2, 

1552 ) 

1553 

1554 inner_radiation = attribute.Attribute( 

1555 unit=ureg.W / ureg.K / ureg.meter ** 2, 

1556 ) 

1557 

1558 outer_radiation = attribute.Attribute( 

1559 unit=ureg.W / ureg.K / ureg.meter ** 2, 

1560 ) 

1561 

1562 outer_convection = attribute.Attribute( 

1563 unit=ureg.W / ureg.K / ureg.meter ** 2, 

1564 ) 

1565 

1566 

1567class Door(BPSProductWithLayers): 

1568 ifc_types = {"IfcDoor": ['*', 'DOOR', 'GATE', 'TRAPDOOR']} 

1569 

1570 pattern_ifc_type = [ 

1571 re.compile('Door', flags=re.IGNORECASE), 

1572 re.compile('Tuer', flags=re.IGNORECASE) 

1573 ] 

1574 

1575 conditions = [ 

1576 condition.RangeCondition('glazing_ratio', 

1577 0 * ureg.dimensionless, 

1578 1 * ureg.dimensionless, True, 

1579 critical_for_creation=False), 

1580 ] 

1581 

1582 def get_better_subclass(self): 

1583 return OuterDoor if self.is_external else InnerDoor 

1584 

1585 def get_net_area(self, name): 

1586 if self.glazing_ratio: 

1587 return self.gross_area * (1 - self.glazing_ratio) 

1588 return self.gross_area - self.opening_area 

1589 

1590 net_area = attribute.Attribute( 

1591 functions=[get_net_area, ], 

1592 unit=ureg.meter ** 2, 

1593 ) 

1594 

1595 gross_area = attribute.Attribute( 

1596 default_ps=("Qto_DoorBaseQuantities", "Area"), 

1597 functions=[BPSProduct.get_bound_area], 

1598 unit=ureg.meter ** 2 

1599 ) 

1600 

1601 glazing_ratio = attribute.Attribute( 

1602 default_ps=("Pset_DoorCommon", "GlazingAreaFraction"), 

1603 ) 

1604 

1605 width = attribute.Attribute( 

1606 default_ps=("Qto_DoorBaseQuantities", "Width"), 

1607 functions=[BPSProductWithLayers.get_thickness_by_layers], 

1608 unit=ureg.m 

1609 ) 

1610 

1611 u_value = attribute.Attribute( 

1612 unit=ureg.W / ureg.K / ureg.meter ** 2, 

1613 functions=[BPSProductWithLayers.get_u_value], 

1614 ) 

1615 

1616 inner_convection = attribute.Attribute( 

1617 unit=ureg.W / ureg.K / ureg.meter ** 2, 

1618 default=0.6 

1619 ) 

1620 

1621 inner_radiation = attribute.Attribute( 

1622 unit=ureg.W / ureg.K / ureg.meter ** 2, 

1623 ) 

1624 

1625 outer_radiation = attribute.Attribute( 

1626 unit=ureg.W / ureg.K / ureg.meter ** 2, 

1627 ) 

1628 

1629 outer_convection = attribute.Attribute( 

1630 unit=ureg.W / ureg.K / ureg.meter ** 2, 

1631 ) 

1632 

1633 

1634class InnerDoor(Door): 

1635 ifc_types = {} 

1636 

1637 def calc_cost_group(self) -> int: 

1638 """Calc cost group for Innerdoors 

1639 

1640 Inner door: 344 

1641 """ 

1642 

1643 return 344 

1644 

1645 

1646class OuterDoor(Door): 

1647 ifc_types = {} 

1648 

1649 def calc_cost_group(self) -> int: 

1650 """Calc cost group for Outerdoors 

1651 

1652 Outer door: 334 

1653 """ 

1654 

1655 return 334 

1656 

1657 

1658class Slab(BPSProductWithLayers): 

1659 ifc_types = { 

1660 "IfcSlab": ['*', 'LANDING'] 

1661 } 

1662 

1663 def __init__(self, *args, **kwargs): 

1664 """slab __init__ function""" 

1665 super().__init__(*args, **kwargs) 

1666 

1667 def _calc_teaser_orientation(self, name) -> int: 

1668 """Returns the orientation of the slab in TEASER convention.""" 

1669 return -1 

1670 

1671 net_area = attribute.Attribute( 

1672 default_ps=("Qto_SlabBaseQuantities", "NetArea"), 

1673 functions=[BPSProduct.get_net_bound_area], 

1674 unit=ureg.meter ** 2 

1675 ) 

1676 

1677 gross_area = attribute.Attribute( 

1678 default_ps=("Qto_SlabBaseQuantities", "GrossArea"), 

1679 functions=[BPSProduct.get_bound_area], 

1680 unit=ureg.meter ** 2 

1681 ) 

1682 

1683 width = attribute.Attribute( 

1684 default_ps=("Qto_SlabBaseQuantities", "Width"), 

1685 functions=[BPSProductWithLayers.get_thickness_by_layers], 

1686 unit=ureg.m 

1687 ) 

1688 

1689 u_value = attribute.Attribute( 

1690 default_ps=("Pset_SlabCommon", "ThermalTransmittance"), 

1691 unit=ureg.W / ureg.K / ureg.meter ** 2, 

1692 functions=[BPSProductWithLayers.get_u_value], 

1693 ) 

1694 

1695 net_volume = attribute.Attribute( 

1696 default_ps=("Qto_SlabBaseQuantities", "NetVolume"), 

1697 unit=ureg.meter ** 3 

1698 ) 

1699 

1700 is_load_bearing = attribute.Attribute( 

1701 default_ps=("Pset_SlabCommon", "LoadBearing"), 

1702 ) 

1703 

1704 

1705class Roof(Slab): 

1706 # todo decomposed roofs dont have materials, layers etc. because these 

1707 # information are stored in the slab itself and not the decomposition 

1708 # is_external = True 

1709 ifc_types = { 

1710 "IfcRoof": 

1711 ['*', 'FLAT_ROOF', 'SHED_ROOF', 'GABLE_ROOF', 'HIP_ROOF', 

1712 'HIPPED_GABLE_ROOF', 'GAMBREL_ROOF', 'MANSARD_ROOF', 

1713 'BARREL_ROOF', 'RAINBOW_ROOF', 'BUTTERFLY_ROOF', 'PAVILION_ROOF', 

1714 'DOME_ROOF', 'FREEFORM'], 

1715 "IfcSlab": ['ROOF'] 

1716 } 

1717 

1718 def calc_cost_group(self) -> int: 

1719 """Calc cost group for Roofs 

1720 

1721 

1722 Load bearing: 361 

1723 Not load bearing: 363 

1724 """ 

1725 if self.is_load_bearing: 

1726 return 361 

1727 elif not self.is_load_bearing: 

1728 return 363 

1729 else: 

1730 return 300 

1731 

1732 

1733class InnerFloor(Slab): 

1734 """In bim2sim we handle all inner slabs as floors/inner floors. 

1735 

1736 Orientation of layerset is layer 0 is inside (floor surface of this room), 

1737 layer n is outside (ceiling surface of room below). 

1738 """ 

1739 ifc_types = { 

1740 "IfcSlab": ['FLOOR'] 

1741 } 

1742 

1743 def calc_cost_group(self) -> int: 

1744 """Calc cost group for Floors 

1745 

1746 Floor: 351 

1747 """ 

1748 return 351 

1749 

1750 

1751class GroundFloor(Slab): 

1752 # is_external = True # todo to be removed 

1753 ifc_types = { 

1754 "IfcSlab": ['BASESLAB'] 

1755 } 

1756 

1757 def _calc_teaser_orientation(self, name) -> int: 

1758 """Returns the orientation of the groundfloor in TEASER convention.""" 

1759 return -2 

1760 

1761 def calc_cost_group(self) -> int: 

1762 """Calc cost group for groundfloors 

1763 

1764 groundfloors: 322 

1765 """ 

1766 

1767 return 322 

1768 

1769 

1770 # pattern_ifc_type = [ 

1771 # re.compile('Bodenplatte', flags=re.IGNORECASE), 

1772 # re.compile('') 

1773 # ] 

1774 

1775 

1776class Site(BPSProduct): 

1777 def __init__(self, *args, **kwargs): 

1778 super().__init__(*args, **kwargs) 

1779 del self.building 

1780 self.buildings = [] 

1781 

1782 # todo move this to base elements as this relevant for other domains as well 

1783 ifc_types = {"IfcSite": ['*']} 

1784 

1785 gross_area = attribute.Attribute( 

1786 default_ps=("Qto_SiteBaseQuantities", "GrossArea"), 

1787 unit=ureg.meter ** 2 

1788 ) 

1789 

1790 location_latitude = attribute.Attribute( 

1791 ifc_attr_name="RefLatitude", 

1792 ) 

1793 

1794 location_longitude = attribute.Attribute( 

1795 ifc_attr_name="RefLongitude" 

1796 ) 

1797 

1798 

1799class Building(BPSProduct): 

1800 def __init__(self, *args, **kwargs): 

1801 super().__init__(*args, **kwargs) 

1802 self.thermal_zones = [] 

1803 self.storeys = [] 

1804 self.elements = [] 

1805 

1806 ifc_types = {"IfcBuilding": ['*']} 

1807 from_ifc_domains = [IFCDomain.arch] 

1808 

1809 conditions = [ 

1810 condition.RangeCondition('year_of_construction', 

1811 1900 * ureg.year, 

1812 date.today().year * ureg.year, 

1813 critical_for_creation=False), 

1814 ] 

1815 

1816 def _get_building_name(self, name): 

1817 """get building name""" 

1818 bldg_name = self.get_ifc_attribute('Name') 

1819 if bldg_name: 

1820 return bldg_name 

1821 else: 

1822 # todo needs to be adjusted for multiple buildings #165 

1823 bldg_name = 'Building' 

1824 return bldg_name 

1825 

1826 def _get_number_of_storeys(self, name): 

1827 return len(self.storeys) 

1828 

1829 def _get_avg_storey_height(self, name): 

1830 """Calculates the average height of all storeys.""" 

1831 storey_height_sum = 0 

1832 avg_height = None 

1833 if hasattr(self, "storeys"): 

1834 if len(self.storeys) > 0: 

1835 for storey in self.storeys: 

1836 if storey.height: 

1837 height = storey.height 

1838 elif storey.gross_height: 

1839 height = storey.gross_height 

1840 elif storey.net_height: 

1841 height = storey.net_height 

1842 else: 

1843 height = None 

1844 if height: 

1845 storey_height_sum += height 

1846 avg_height = storey_height_sum / len(self.storeys) 

1847 return avg_height 

1848 

1849 def _check_tz_ahu(self, name): 

1850 """Check if any TZs have AHU, then the building has one as well.""" 

1851 with_ahu = False 

1852 for tz in self.thermal_zones: 

1853 if tz.with_ahu: 

1854 with_ahu = True 

1855 break 

1856 return with_ahu 

1857 

1858 bldg_name = attribute.Attribute( 

1859 functions=[_get_building_name], 

1860 ) 

1861 

1862 year_of_construction = attribute.Attribute( 

1863 default_ps=("Pset_BuildingCommon", "YearOfConstruction"), 

1864 unit=ureg.year 

1865 ) 

1866 

1867 gross_area = attribute.Attribute( 

1868 default_ps=("Qto_BuildingBaseQuantities", "GrossFloorArea"), 

1869 unit=ureg.meter ** 2 

1870 ) 

1871 

1872 net_area = attribute.Attribute( 

1873 default_ps=("Qto_BuildingBaseQuantities", "NetFloorArea"), 

1874 unit=ureg.meter ** 2 

1875 ) 

1876 

1877 number_of_storeys = attribute.Attribute( 

1878 unit=ureg.dimensionless, 

1879 functions=[_get_number_of_storeys] 

1880 ) 

1881 

1882 occupancy_type = attribute.Attribute( 

1883 default_ps=("Pset_BuildingCommon", "OccupancyType"), 

1884 ) 

1885 

1886 avg_storey_height = attribute.Attribute( 

1887 unit=ureg.meter, 

1888 functions=[_get_avg_storey_height] 

1889 ) 

1890 

1891 with_ahu = attribute.Attribute( 

1892 functions=[_check_tz_ahu] 

1893 ) 

1894 

1895 ahu_heating = attribute.Attribute( 

1896 attr_type=bool 

1897 ) 

1898 

1899 ahu_cooling = attribute.Attribute( 

1900 attr_type=bool 

1901 ) 

1902 

1903 ahu_dehumidification = attribute.Attribute( 

1904 attr_type=bool 

1905 ) 

1906 

1907 ahu_humidification = attribute.Attribute( 

1908 attr_type=bool 

1909 ) 

1910 

1911 ahu_heat_recovery = attribute.Attribute( 

1912 attr_type=bool 

1913 ) 

1914 

1915 ahu_heat_recovery_efficiency = attribute.Attribute( 

1916 ) 

1917 

1918 

1919class Storey(BPSProduct): 

1920 ifc_types = {'IfcBuildingStorey': ['*']} 

1921 from_ifc_domains = [IFCDomain.arch] 

1922 

1923 def __init__(self, *args, **kwargs): 

1924 """storey __init__ function""" 

1925 super().__init__(*args, **kwargs) 

1926 self.elements = [] 

1927 

1928 spec_machines_internal_load = attribute.Attribute( 

1929 default_ps=("Pset_ThermalLoadDesignCriteria", 

1930 "ReceptacleLoadIntensity"), 

1931 unit=ureg.kilowatt / (ureg.meter ** 2) 

1932 ) 

1933 

1934 spec_lighting_internal_load = attribute.Attribute( 

1935 default_ps=("Pset_ThermalLoadDesignCriteria", "LightingLoadIntensity"), 

1936 unit=ureg.kilowatt / (ureg.meter ** 2) 

1937 ) 

1938 

1939 cooling_load = attribute.Attribute( 

1940 default_ps=("Pset_ThermalLoadAggregate", "TotalCoolingLoad"), 

1941 unit=ureg.kilowatt 

1942 ) 

1943 

1944 heating_load = attribute.Attribute( 

1945 default_ps=("Pset_ThermalLoadAggregate", "TotalHeatingLoad"), 

1946 unit=ureg.kilowatt 

1947 ) 

1948 

1949 air_per_person = attribute.Attribute( 

1950 default_ps=("Pset_ThermalLoadDesignCriteria", "OutsideAirPerPerson"), 

1951 unit=ureg.meter ** 3 / ureg.hour 

1952 ) 

1953 

1954 percent_load_to_radiant = attribute.Attribute( 

1955 default_ps=("Pset_ThermalLoadDesignCriteria", 

1956 "AppliancePercentLoadToRadiant"), 

1957 unit=ureg.percent 

1958 ) 

1959 

1960 gross_floor_area = attribute.Attribute( 

1961 default_ps=("Qto_BuildingStoreyBaseQuantities", "GrossFloorArea"), 

1962 unit=ureg.meter ** 2 

1963 ) 

1964 

1965 # todo make the lookup for height hierarchical 

1966 net_height = attribute.Attribute( 

1967 default_ps=("Qto_BuildingStoreyBaseQuantities", "NetHeight"), 

1968 unit=ureg.meter 

1969 ) 

1970 

1971 gross_height = attribute.Attribute( 

1972 default_ps=("Qto_BuildingStoreyBaseQuantities", "GrossHeight"), 

1973 unit=ureg.meter 

1974 ) 

1975 

1976 height = attribute.Attribute( 

1977 default_ps=("Qto_BuildingStoreyBaseQuantities", "Height"), 

1978 unit=ureg.meter 

1979 ) 

1980 

1981 

1982# collect all domain classes 

1983items: Set[BPSProduct] = set() 

1984for name, cls in inspect.getmembers( 

1985 sys.modules[__name__], 

1986 lambda member: inspect.isclass(member) # class at all 

1987 and issubclass(member, BPSProduct) # domain subclass 

1988 and member is not BPSProduct # but not base class 

1989 and member.__module__ == __name__): # declared here 

1990 items.add(cls)