Coverage for bim2sim/elements/hvac_elements.py: 67%

502 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-12 17:09 +0000

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

2import inspect 

3import itertools 

4import logging 

5import math 

6import re 

7import sys 

8from typing import Set, List, Tuple, Generator, Union, Type 

9 

10import numpy as np 

11 

12from bim2sim.kernel.decision import ListDecision, DecisionBunch 

13from bim2sim.elements.mapping import condition, attribute 

14from bim2sim.elements.base_elements import Port, ProductBased, IFCBased 

15from bim2sim.elements.mapping.ifc2python import get_ports as ifc2py_get_ports 

16from bim2sim.elements.mapping.ifc2python import get_predefined_type 

17from bim2sim.elements.mapping.units import ureg 

18 

19logger = logging.getLogger(__name__) 

20quality_logger = logging.getLogger('bim2sim.QualityReport') 

21 

22 

23def diameter_post_processing(value): 

24 if isinstance(value, (list, set)): 

25 return sum(value) / len(value) 

26 return value 

27 

28 

29def length_post_processing(value): 

30 if isinstance(value, (list, set)): 

31 return max(value) 

32 return value 

33 

34 

35class HVACPort(Port): 

36 """Port of HVACProduct.""" 

37 vl_pattern = re.compile('.*vorlauf.*', 

38 re.IGNORECASE) # TODO: extend pattern 

39 rl_pattern = re.compile('.*rücklauf.*', re.IGNORECASE) 

40 

41 def __init__( 

42 self, *args, groups: Set = None, 

43 flow_direction: int = 0, **kwargs): 

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

45 

46 self._flow_master = False 

47 self._flow_direction = None 

48 self._flow_side = None 

49 self.groups = groups or set() 

50 self.flow_direction = flow_direction 

51 

52 @classmethod 

53 def ifc2args(cls, ifc) -> Tuple[tuple, dict]: 

54 args, kwargs = super().ifc2args(ifc) 

55 groups = {assg.RelatingGroup.ObjectType 

56 for assg in ifc.HasAssignments} 

57 flow_direction = None 

58 if ifc.FlowDirection == 'SOURCE': 

59 flow_direction = 1 

60 elif ifc.FlowDirection == 'SINK': 

61 flow_direction = -1 

62 elif ifc.FlowDirection in ['SINKANDSOURCE', 'SOURCEANDSINK']: 

63 flow_direction = 0 

64 

65 kwargs['groups'] = groups 

66 kwargs['flow_direction'] = flow_direction 

67 return args, kwargs 

68 

69 def _calc_position(self, name) -> np.array: 

70 """returns absolute position as np.array""" 

71 try: 

72 relative_placement = \ 

73 self.parent.ifc.ObjectPlacement.RelativePlacement 

74 x_direction = np.array( 

75 relative_placement.RefDirection.DirectionRatios) 

76 z_direction = np.array(relative_placement.Axis.DirectionRatios) 

77 except AttributeError: 

78 x_direction = np.array([1, 0, 0]) 

79 z_direction = np.array([0, 0, 1]) 

80 y_direction = np.cross(z_direction, x_direction) 

81 directions = np.array((x_direction, y_direction, z_direction)).T 

82 port_coordinates_relative = \ 

83 np.array( 

84 self.ifc.ObjectPlacement.RelativePlacement.Location.Coordinates) 

85 coordinates = self.parent.position + np.matmul(directions, 

86 port_coordinates_relative) 

87 

88 if all(coordinates == np.array([0, 0, 0])): 

89 quality_logger.info("Suspect position [0, 0, 0] for %s", self) 

90 return coordinates 

91 

92 @classmethod 

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

94 return True 

95 

96 def validate_creation(self) -> bool: 

97 return True 

98 

99 @property 

100 def flow_master(self): 

101 """Lock flow direction for port""" 

102 return self._flow_master 

103 

104 @flow_master.setter 

105 def flow_master(self, value: bool): 

106 self._flow_master = value 

107 

108 @property 

109 def flow_direction(self): 

110 """Flow direction of port 

111 

112 -1 = medium flows into port 

113 1 = medium flows out of port 

114 0 = medium flow undirected 

115 None = flow direction unknown""" 

116 return self._flow_direction 

117 

118 @flow_direction.setter 

119 def flow_direction(self, value): 

120 if self._flow_master: 

121 raise AttributeError("Can't set flow direction for flow master.") 

122 if value not in (-1, 0, 1, None): 

123 raise AttributeError("Invalid value. Use one of (-1, 0, 1, None).") 

124 self._flow_direction = value 

125 

126 @property 

127 def verbose_flow_direction(self): 

128 """Flow direction of port""" 

129 if self.flow_direction == -1: 

130 return 'SINK' 

131 if self.flow_direction == 0: 

132 return 'SINKANDSOURCE' 

133 if self.flow_direction == 1: 

134 return 'SOURCE' 

135 return 'UNKNOWN' 

136 

137 @property 

138 def flow_side(self): 

139 """ 

140 Flow side of port. 

141 

142 1 = supply flow (Vorlauf) 

143 -1 = return flow (Rücklauf) 

144 0 = unknown 

145 """ 

146 if self._flow_side is None: 

147 self._flow_side = self.determine_flow_side() 

148 return self._flow_side 

149 

150 @flow_side.setter 

151 def flow_side(self, value): 

152 if value not in (-1, 0, 1): 

153 raise ValueError("allowed values for flow_side are 1, 0, -1") 

154 previous = self._flow_side 

155 self._flow_side = value 

156 if previous: 

157 if previous != value: 

158 logger.info("Overwriting flow_side for %r with %s" % ( 

159 self, self.verbose_flow_side)) 

160 else: 

161 logger.debug( 

162 "Set flow_side for %r to %s" % (self, self.verbose_flow_side)) 

163 

164 @property 

165 def verbose_flow_side(self): 

166 if self.flow_side == 1: 

167 return "VL" 

168 if self.flow_side == -1: 

169 return "RL" 

170 return "UNKNOWN" 

171 

172 def determine_flow_side(self): 

173 """Check groups for hints of flow_side and returns flow_side if hints are definitely""" 

174 vl = None 

175 rl = None 

176 if self.parent.is_generator(): 

177 if self.flow_direction == 1: 

178 vl = True 

179 elif self.flow_direction == -1: 

180 rl = True 

181 elif self.parent.is_consumer(): 

182 if self.flow_direction == 1: 

183 rl = True 

184 elif self.flow_direction == -1: 

185 vl = True 

186 if not vl: 

187 vl = any(filter(self.vl_pattern.match, self.groups)) 

188 if not rl: 

189 rl = any(filter(self.rl_pattern.match, self.groups)) 

190 

191 if vl and not rl: 

192 return 1 

193 if rl and not vl: 

194 return -1 

195 return 0 

196 

197 

198class HVACProduct(ProductBased): 

199 domain = 'HVAC' 

200 

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

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

203 self.inner_connections: List[Tuple[HVACPort, HVACPort]] \ 

204 = self.get_inner_connections() 

205 

206 @property 

207 def expected_hvac_ports(self): 

208 raise NotImplementedError(f"Please define the expected number of ports " 

209 f"for the class {self.__class__.__name__} ") 

210 

211 def get_ports(self) -> list: 

212 """Returns a list of ports of this product.""" 

213 ports = ifc2py_get_ports(self.ifc) 

214 hvac_ports = [] 

215 for port in ports: 

216 port_valid = True 

217 if port.is_a() != 'IfcDistributionPort': 

218 port_valid = False 

219 else: 

220 predefined_type = get_predefined_type(port) 

221 if predefined_type in [ 

222 'CABLE', 'CABLECARRIER', 'WIRELESS']: 

223 port_valid = False 

224 if port_valid: 

225 hvac_ports.append(HVACPort.from_ifc( 

226 ifc=port, parent=self)) 

227 else: 

228 logger.warning( 

229 "Not included %s as Port in %s with GUID %s", 

230 port.is_a(), 

231 self.__class__.__name__, 

232 self.guid) 

233 return hvac_ports 

234 

235 def get_inner_connections(self) -> List[Tuple[HVACPort, HVACPort]]: 

236 """Returns inner connections of Element. 

237 

238 By default each port is connected to each other port. 

239 Overwrite for other connections.""" 

240 

241 connections = [] 

242 for port0, port1 in itertools.combinations(self.ports, 2): 

243 connections.append((port0, port1)) 

244 return connections 

245 

246 def decide_inner_connections(self) -> Generator[DecisionBunch, None, None]: 

247 """Generator method yielding decisions to set inner connections.""" 

248 

249 if len(self.ports) < 2: 

250 # not possible to connect anything 

251 return 

252 

253 # TODO: extend pattern 

254 vl_pattern = re.compile('.*vorlauf.*', re.IGNORECASE) 

255 rl_pattern = re.compile('.*rücklauf.*', re.IGNORECASE) 

256 

257 # use score for ports to help user find best match_graph 

258 score_vl = {} 

259 score_rl = {} 

260 for port in self.ports: 

261 bonus_vl = 0 

262 bonus_rl = 0 

263 # connected to pipe 

264 if port.connection and type(port.connection.parent) \ 

265 in [Pipe, PipeFitting]: 

266 bonus_vl += 1 

267 bonus_rl += 1 

268 # string hints 

269 if any(filter(vl_pattern.match, port.groups)): 

270 bonus_vl += 1 

271 if any(filter(rl_pattern.match, port.groups)): 

272 bonus_rl += 1 

273 # flow direction 

274 if port.flow_direction == 1: 

275 bonus_vl += .5 

276 if port.flow_direction == -1: 

277 bonus_rl += .5 

278 score_vl[port] = bonus_vl 

279 score_rl[port] = bonus_rl 

280 

281 # created sorted choices 

282 choices_vl = [port.guid for port, score in 

283 sorted(score_vl.items(), key=lambda item: item[1], 

284 reverse=True)] 

285 choices_rl = [port.guid for port, score in 

286 sorted(score_rl.items(), key=lambda item: item[1], 

287 reverse=True)] 

288 decision_vl = ListDecision(f"Please select VL Port for {self}.", 

289 choices=choices_vl, 

290 default=choices_vl[0], # best guess 

291 key='VL', 

292 global_key='VL_port_of_' + self.guid) 

293 decision_rl = ListDecision(f"Please select RL Port for {self}.", 

294 choices=choices_rl, 

295 default=choices_rl[0], # best guess 

296 key='RL', 

297 global_key='RL_port_of_' + self.guid) 

298 decisions = DecisionBunch((decision_vl, decision_rl)) 

299 yield decisions 

300 

301 port_dict = {port.guid: port for port in self.ports} 

302 vl = port_dict[decision_vl.value] 

303 rl = port_dict[decision_rl.value] 

304 # set flow correct side 

305 vl.flow_side = 1 

306 rl.flow_side = -1 

307 self.inner_connections.append((vl, rl)) 

308 

309 def validate_ports(self): 

310 if isinstance(self.expected_hvac_ports, tuple): 

311 if self.expected_hvac_ports[0] <= len(self.ports) \ 

312 <= self.expected_hvac_ports[-1]: 

313 return True 

314 else: 

315 if len(self.ports) == self.expected_hvac_ports: 

316 return True 

317 return False 

318 

319 def is_generator(self): 

320 return False 

321 

322 def is_consumer(self): 

323 return False 

324 

325 def calc_cost_group(self) -> [int]: 

326 """Default cost group for HVAC elements is 400""" 

327 return 400 

328 

329 def __repr__(self): 

330 return "<%s (guid: %s, ports: %d)>" % ( 

331 self.__class__.__name__, self.guid, len(self.ports)) 

332 

333 

334class HeatPump(HVACProduct): 

335 """"HeatPump""" 

336 

337 ifc_types = { 

338 'IfcUnitaryEquipment': ['*'] 

339 } 

340 # IFC Schema does not support Heatpumps directly, but default of unitary 

341 # equipment is set to HeatPump now and expected ports to 4 to try to 

342 # identify heat pumps 

343 

344 pattern_ifc_type = [ 

345 re.compile('Heat.?pump', flags=re.IGNORECASE), 

346 re.compile('W(ä|ae)rme.?pumpe', flags=re.IGNORECASE), 

347 ] 

348 

349 min_power = attribute.Attribute( 

350 description='Minimum power that heat pump operates at.', 

351 unit=ureg.kilowatt, 

352 ) 

353 rated_power = attribute.Attribute( 

354 description='Rated power of heat pump.', 

355 unit=ureg.kilowatt, 

356 ) 

357 efficiency = attribute.Attribute( 

358 description='Efficiency of heat pump provided as list with pairs of ' 

359 '[percentage_of_rated_power,efficiency]', 

360 unit=ureg.dimensionless 

361 ) 

362 vdi_performance_data_table=attribute.Attribute( 

363 description="temp dummy to test vdi table export", 

364 ) 

365 is_reversible = attribute.Attribute( 

366 description="Does the heat pump support cooling as well?", 

367 unit=ureg.dimensionless 

368 ) 

369 rated_cooling_power = attribute.Attribute( 

370 description='Rated power of heat pump in cooling mode.', 

371 unit=ureg.kilowatt, 

372 ) 

373 COP = attribute.Attribute( 

374 description="The COP of the heat pump, definition based on VDI 3805-22", 

375 unit=ureg.dimensionless 

376 ) 

377 internal_pump = attribute.Attribute( 

378 description="The COP of the heat pump, definition based on VDI 3805-22", 

379 ) 

380 

381 @property 

382 def expected_hvac_ports(self): 

383 return 4 

384 

385 

386class Chiller(HVACProduct): 

387 """"Chiller""" 

388 

389 ifc_types = { 

390 'IfcChiller': ['*', 'AIRCOOLED', 'WATERCOOLED', 'HEATRECOVERY']} 

391 

392 pattern_ifc_type = [ 

393 re.compile('Chiller', flags=re.IGNORECASE), 

394 re.compile('K(ä|ae)lte.?maschine', flags=re.IGNORECASE), 

395 ] 

396 

397 rated_power = attribute.Attribute( 

398 description='Rated power of Chiller.', 

399 default_ps=('Pset_ChillerTypeCommon', 'NominalCapacity'), 

400 unit=ureg.kilowatt, 

401 ) 

402 

403 nominal_power_consumption = attribute.Attribute( 

404 description="nominal power consumption of chiller", 

405 default_ps=('Pset_ChillerTypeCommon', 'NominalPowerConsumption'), 

406 unit=ureg.kilowatt, 

407 ) 

408 

409 nominal_COP = attribute.Attribute( 

410 description="Chiller efficiency at nominal load", 

411 default_ps=('Pset_ChillerTypeCommon', 'NominalEfficiency'), 

412 ) 

413 

414 capacity_curve = attribute.Attribute( 

415 # (Capacity[W], CondensingTemperature[K], EvaporatingTemperature[K]) 

416 description="Chiller's thermal power as function of fluid temperature", 

417 default_ps=('Pset_ChillerTypeCommon', 'CapacityCurve'), 

418 ) 

419 

420 COP = attribute.Attribute( 

421 # (COP, CondensingTemperature[K], EvaporatingTemperature[K]) 

422 description="Chiller's COP as function of fluid temperature", 

423 default_ps=('Pset_ChillerTypeCommon', 'CoefficientOfPerformanceCurve'), 

424 ) 

425 

426 full_load_ratio = attribute.Attribute( 

427 # (FracFullLoadPower, PartLoadRatio) 

428 description="Chiller's thermal partial load power as function of fluid " 

429 "temperature", 

430 default_ps=('Pset_ChillerTypeCommon', 'FullLoadRatioCurve'), 

431 ) 

432 

433 nominal_condensing_temperature = attribute.Attribute( 

434 description='Nominal condenser temperature', 

435 default_ps=('Pset_ChillerTypeCommon', 'NominalCondensingTemperature'), 

436 unit=ureg.celsius, 

437 ) 

438 

439 nominal_evaporating_temperature = attribute.Attribute( 

440 description='Nominal condenser temperature', 

441 default_ps=('Pset_ChillerTypeCommon', 'NominalEvaporatingTemperature'), 

442 unit=ureg.celsius, 

443 ) 

444 

445 min_power = attribute.Attribute( 

446 description='Minimum power at which Chiller operates at.', 

447 unit=ureg.kilowatt, 

448 ) 

449 

450 @property 

451 def expected_hvac_ports(self): 

452 return 4 

453 

454 

455class CoolingTower(HVACProduct): 

456 """"CoolingTower""" 

457 

458 ifc_types = { 

459 'IfcCoolingTower': 

460 ['*', 'NATURALDRAFT', 'MECHANICALINDUCEDDRAFT', 

461 'MECHANICALFORCEDDRAFT'] 

462 } 

463 

464 pattern_ifc_type = [ 

465 re.compile('Cooling.?Tower', flags=re.IGNORECASE), 

466 re.compile('Recooling.?Plant', flags=re.IGNORECASE), 

467 re.compile('K(ü|ue)hl.?turm', flags=re.IGNORECASE), 

468 re.compile('R(ü|ue)ck.?K(ü|ue)hl.?(werk|turm|er)', flags=re.IGNORECASE), 

469 re.compile('RKA', flags=re.IGNORECASE), 

470 ] 

471 

472 min_power = attribute.Attribute( 

473 description='Minimum power that CoolingTower operates at.', 

474 unit=ureg.kilowatt, 

475 ) 

476 rated_power = attribute.Attribute( 

477 description='Rated power of CoolingTower.', 

478 default_ps=('Pset_CoolingTowerTypeCommon', 'NominalCapacity'), 

479 unit=ureg.kilowatt, 

480 ) 

481 efficiency = attribute.Attribute( 

482 description='Efficiency of CoolingTower provided as list with pairs of ' 

483 '[percentage_of_rated_power,efficiency]', 

484 unit=ureg.dimensionless, 

485 ) 

486 

487 @property 

488 def expected_hvac_ports(self): 

489 return 2 

490 

491 

492class HeatExchanger(HVACProduct): 

493 """"Heat exchanger""" 

494 

495 ifc_types = {'IfcHeatExchanger': ['*', 'PLATE', 'SHELLANDTUBE']} 

496 

497 pattern_ifc_type = [ 

498 re.compile('Heat.?Exchanger', flags=re.IGNORECASE), 

499 re.compile('W(ä|ae)rme.?(ü|e)bertrager', flags=re.IGNORECASE), 

500 re.compile('W(ä|ae)rme.?tauscher', flags=re.IGNORECASE), 

501 ] 

502 

503 min_power = attribute.Attribute( 

504 description='Minimum power that HeatExchange operates at.', 

505 unit=ureg.kilowatt, 

506 ) 

507 rated_power = attribute.Attribute( 

508 description='Rated power of HeatExchange.', 

509 unit=ureg.kilowatt, 

510 ) 

511 efficiency = attribute.Attribute( 

512 description='Efficiency of HeatExchange provided as list with pairs of ' 

513 '[percentage_of_rated_power,efficiency]', 

514 unit=ureg.dimensionless, 

515 ) 

516 

517 @property 

518 def expected_hvac_ports(self): 

519 return 4 

520 

521 

522class Boiler(HVACProduct): 

523 """Boiler""" 

524 ifc_types = {'IfcBoiler': ['*', 'WATER', 'STEAM']} 

525 

526 pattern_ifc_type = [ 

527 # re.compile('Heat.?pump', flags=re.IGNORECASE), 

528 re.compile('Kessel', flags=re.IGNORECASE), 

529 re.compile('Boiler', flags=re.IGNORECASE), 

530 ] 

531 

532 @property 

533 def expected_hvac_ports(self): 

534 return 2 

535 

536 def is_generator(self): 

537 """Boiler is a generator function.""" 

538 return True 

539 

540 def get_inner_connections(self): 

541 # TODO see #167 

542 if len(self.ports) > 2: 

543 return [] 

544 else: 

545 connections = super().get_inner_connections() 

546 return connections 

547 

548 water_volume = attribute.Attribute( 

549 description="Water volume of boiler", 

550 default_ps=('Pset_BoilerTypeCommon', 'WaterStorageCapacity'), 

551 unit=ureg.meter ** 3, 

552 ) 

553 

554 dry_mass = attribute.Attribute( 

555 description="Weight of the element, not including contained fluid.", 

556 default_ps=('Qto_BoilerBaseQuantities', 'GrossWeight'), 

557 unit=ureg.kg, 

558 ) 

559 

560 nominal_power_consumption = attribute.Attribute( 

561 description="nominal energy consumption of boiler", 

562 default_ps=('Pset_BoilerTypeCommon', 'NominalEnergyConsumption'), 

563 unit=ureg.kilowatt, 

564 ) 

565 

566 efficiency = attribute.Attribute( 

567 # (Efficiency, PartialLoadFactor) 

568 description="Efficiency of boiler provided as list with pairs of " 

569 "percentage_of_rated_power and efficiency", 

570 default_ps=('Pset_BoilerTypeCommon', 'PartialLoadEfficiencyCurves'), 

571 unit=ureg.dimensionless, 

572 ) 

573 

574 energy_source = attribute.Attribute( 

575 description="Final energy source of boiler", 

576 default_ps=('Pset_BoilerTypeCommon', 'EnergySource'), 

577 ) 

578 

579 operating_mode = attribute.Attribute( 

580 # [fixed, twostep, modulating, other, unknown, unset] 

581 description="Boiler's operating mode", 

582 default_ps=('Pset_BoilerTypeCommon', 'OperatingMode'), 

583 unit=ureg.dimensionless, 

584 ) 

585 

586 part_load_ratio_range = attribute.Attribute( 

587 description="Allowable part load ratio range (Bounded value).", 

588 default_ps=('Pset_BoilerTypeCommon', 'NominalPartLoadRatio'), 

589 ) 

590 

591 def _get_minimal_part_load_ratio(self, name): 

592 """Calculates the minimal part load ratio based on the given range.""" 

593 # TODO this is not tested yet but should work with the new BoundedValue 

594 # in ifc2python 

595 if hasattr(self, "part_load_ratio_range"): 

596 return min(self.part_load_ratio_range) 

597 

598 def _normalise_value_zero_to_one(self, value): 

599 if (max(self.part_load_ratio_range) == 100 

600 and min(self.part_load_ratio_range) == 0): 

601 return value * 0.01 

602 

603 minimal_part_load_ratio = attribute.Attribute( 

604 description="Minimal part load ratio", 

605 functions=[_get_minimal_part_load_ratio], 

606 # TODO use ifc_post_processing to make sure that ranged value are between 

607 # 0 and 1 

608 ifc_postprocessing=[_normalise_value_zero_to_one] 

609 ) 

610 

611 

612 def _calc_nominal_efficiency(self, name): 

613 """function to calculate the boiler nominal efficiency using the 

614 efficiency curve""" 

615 

616 if isinstance(self.efficiency, list): 

617 efficiency_curve = {y: x for x, y in self.efficiency} 

618 nominal_eff = efficiency_curve.get(1, None) 

619 if nominal_eff: 

620 return nominal_eff 

621 else: 

622 # ToDo: linear regression 

623 raise NotImplementedError 

624 else: 

625 # WORKAROUND: input of lists is not yet implemented 

626 return self.efficiency 

627 

628 nominal_efficiency = attribute.Attribute( 

629 description="""Boiler efficiency at nominal load""", 

630 functions=[_calc_nominal_efficiency], 

631 unit=ureg.dimensionless, 

632 ) 

633 

634 def _calc_rated_power(self, name) -> ureg.Quantity: 

635 """Function to calculate the rated power of the boiler using the nominal 

636 efficiency and the nominal power consumption""" 

637 return self.nominal_efficiency * self.nominal_power_consumption 

638 

639 rated_power = attribute.Attribute( 

640 description="Rated power of boiler", 

641 unit=ureg.kilowatt, 

642 functions=[_calc_rated_power], 

643 ) 

644 

645 def _calc_partial_load_efficiency(self, name): 

646 """Function to calculate the boiler efficiency at partial load using the 

647 nominal partial ratio and the efficiency curve""" 

648 if isinstance(self.efficiency, list): 

649 efficiency_curve = {y: x for x, y in self.efficiency} 

650 partial_eff = efficiency_curve.get(max(self.part_load_ratio_range), 

651 None) 

652 if partial_eff: 

653 return partial_eff 

654 else: 

655 # ToDo: linear regression 

656 raise NotImplementedError 

657 else: 

658 # WORKAROUND: input of lists is not yet implemented 

659 return self.efficiency 

660 

661 partial_load_efficiency = attribute.Attribute( 

662 description="Boiler efficiency at partial load", 

663 functions=[_calc_partial_load_efficiency], 

664 unit=ureg.dimensionless, 

665 default=0.15 

666 ) 

667 

668 def _calc_min_power(self, name) -> ureg.Quantity: 

669 """Function to calculate the minimum power that boiler operates at, 

670 using the partial load efficiency and the nominal power consumption""" 

671 return self.partial_load_efficiency * self.nominal_power_consumption 

672 

673 min_power = attribute.Attribute( 

674 description="Minimum power that boiler operates at", 

675 unit=ureg.kilowatt, 

676 functions=[_calc_min_power], 

677 ) 

678 

679 def _calc_min_PLR(self, name) -> ureg.Quantity: 

680 """Function to calculate the minimal PLR of the boiler using the minimal 

681 power and the rated power""" 

682 return self.min_power / self.rated_power 

683 

684 min_PLR = attribute.Attribute( 

685 description="Minimum Part load ratio", 

686 unit=ureg.dimensionless, 

687 functions=[_calc_min_PLR], 

688 ) 

689 flow_temperature = attribute.Attribute( 

690 description="Nominal inlet temperature", 

691 default_ps=('Pset_BoilerTypeCommon', 'WaterInletTemperatureRange'), 

692 unit=ureg.celsius, 

693 ) 

694 return_temperature = attribute.Attribute( 

695 description="Nominal outlet temperature", 

696 default_ps=('Pset_BoilerTypeCommon', 'OutletTemperatureRange'), 

697 unit=ureg.celsius, 

698 ) 

699 

700 def _calc_dT_water(self, name) -> ureg.Quantity: 

701 """Function to calculate the delta temperature of the boiler using the 

702 return and flow temperature""" 

703 return self.return_temperature - self.flow_temperature 

704 

705 dT_water = attribute.Attribute( 

706 description="Nominal temperature difference", 

707 unit=ureg.kelvin, 

708 functions=[_calc_dT_water], 

709 ) 

710 

711 

712class Pipe(HVACProduct): 

713 ifc_types = { 

714 "IfcPipeSegment": 

715 ['*', 'CULVERT', 'FLEXIBLESEGMENT', 'RIGIDSEGMENT', 'GUTTER', 

716 'SPOOL'] 

717 } 

718 

719 @property 

720 def expected_hvac_ports(self): 

721 return 2 

722 

723 conditions = [ 

724 condition.RangeCondition("diameter", 5.0 * ureg.millimeter, 

725 300.00 * ureg.millimeter) # ToDo: unit?! 

726 ] 

727 

728 def _calc_diameter_from_radius(self, name) -> ureg.Quantity: 

729 if self.radius: 

730 return self.radius*2 

731 else: 

732 return None 

733 

734 diameter = attribute.Attribute( 

735 default_ps=('Pset_PipeSegmentTypeCommon', 'NominalDiameter'), 

736 unit=ureg.millimeter, 

737 patterns=[ 

738 re.compile('.*Durchmesser.*', flags=re.IGNORECASE), 

739 re.compile('.*Diameter.*', flags=re.IGNORECASE), 

740 ], 

741 functions=[_calc_diameter_from_radius], 

742 ifc_postprocessing=diameter_post_processing, 

743 ) 

744 # TODO #432 implement function to get diamter from shape 

745 

746 radius = attribute.Attribute( 

747 patterns=[ 

748 re.compile('.*Radius.*', flags=re.IGNORECASE) 

749 ], 

750 unit=ureg.millimeter 

751 ) 

752 

753 outer_diameter = attribute.Attribute( 

754 description="Outer diameter of pipe", 

755 default_ps=('Pset_PipeSegmentTypeCommon', 'OuterDiameter'), 

756 unit=ureg.millimeter, 

757 ) 

758 

759 inner_diameter = attribute.Attribute( 

760 description="Inner diameter of pipe", 

761 default_ps=('Pset_PipeSegmentTypeCommon', 'InnerDiameter'), 

762 unit=ureg.millimeter, 

763 ) 

764 

765 def _length_from_geometry(self, name): 

766 """ 

767 Function to calculate the length of the pipe from the geometry 

768 """ 

769 try: 

770 return Pipe.get_lenght_from_shape(self.ifc.Representation) \ 

771 * ureg.meter 

772 except AttributeError: 

773 return None 

774 

775 length = attribute.Attribute( 

776 default_ps=('Qto_PipeSegmentBaseQuantities', 'Length'), 

777 unit=ureg.meter, 

778 patterns=[ 

779 re.compile('.*Länge.*', flags=re.IGNORECASE), 

780 re.compile('.*Length.*', flags=re.IGNORECASE), 

781 ], 

782 ifc_postprocessing=length_post_processing, 

783 functions=[_length_from_geometry], 

784 ) 

785 

786 roughness_coefficient = attribute.Attribute( 

787 description="Interior roughness coefficient of pipe", 

788 default_ps=('Pset_PipeSegmentOccurrence', 

789 'InteriorRoughnessCoefficient'), 

790 unit=ureg.millimeter, 

791 ) 

792 

793 @staticmethod 

794 def get_lenght_from_shape(ifc_representation): 

795 """Search for extruded depth in representations 

796 

797 Warning: Found extrusion may net be the required length! 

798 :raises: AttributeError if not exactly one extrusion is found""" 

799 candidates = [] 

800 try: 

801 for representation in ifc_representation.Representations: 

802 for item in representation.Items: 

803 if item.is_a() == 'IfcExtrudedAreaSolid': 

804 candidates.append(item.Depth) 

805 except: 

806 raise AttributeError("Failed to determine length.") 

807 if not candidates: 

808 raise AttributeError("No representation to determine length.") 

809 if len(candidates) > 1: 

810 raise AttributeError( 

811 "Too many representations to dertermine length %s." % candidates) 

812 

813 return candidates[0] 

814 

815 

816class PipeFitting(HVACProduct): 

817 ifc_types = { 

818 "IfcPipeFitting": 

819 ['*', 'BEND', 'CONNECTOR', 'ENTRY', 'EXIT', 'JUNCTION', 

820 'OBSTRUCTION', 'TRANSITION'] 

821 } 

822 pattern_ifc_type = [ 

823 re.compile('Bogen', flags=re.IGNORECASE), 

824 re.compile('Bend', flags=re.IGNORECASE), 

825 ] 

826 

827 @property 

828 def expected_hvac_ports(self): 

829 return (2, 3) 

830 

831 conditions = [ 

832 condition.RangeCondition("diameter", 5.0 * ureg.millimeter, 

833 300.00 * ureg.millimeter) 

834 ] 

835 

836 diameter = attribute.Attribute( 

837 default_ps=('Pset_PipeFittingTypeCommon', 'NominalDiameter'), 

838 unit=ureg.millimeter, 

839 patterns=[ 

840 re.compile('.*Durchmesser.*', flags=re.IGNORECASE), 

841 re.compile('.*Diameter.*', flags=re.IGNORECASE), 

842 ], 

843 ifc_postprocessing=diameter_post_processing, 

844 ) 

845 # TODO #432 implement function to get diamter from shape 

846 

847 length = attribute.Attribute( 

848 default_ps=("Qto_PipeFittingBaseQuantities", "Length"), 

849 unit=ureg.meter, 

850 patterns=[ 

851 re.compile('.*Länge.*', flags=re.IGNORECASE), 

852 re.compile('.*Length.*', flags=re.IGNORECASE), 

853 ], 

854 default=0, 

855 ifc_postprocessing=length_post_processing 

856 ) 

857 

858 pressure_class = attribute.Attribute( 

859 unit=ureg.pascal, 

860 default_ps=('Pset_PipeFittingTypeCommon', 'PressureClass') 

861 ) 

862 

863 pressure_loss_coefficient = attribute.Attribute( 

864 description="Pressure loss coefficient of pipe fitting", 

865 default_ps=('Pset_PipeFittingTypeCommon', 'FittingLossFactor'), 

866 unit=ureg.pascal, 

867 ) 

868 

869 roughness_coefficient = attribute.Attribute( 

870 description="Roughness coefficient of pipe fitting", 

871 default_ps=('Pset_PipeFittingOccurrence', 

872 'InteriorRoughnessCoefficient'), 

873 unit=ureg.millimeter, 

874 ) 

875 

876 @staticmethod 

877 def _diameter_post_processing(value): 

878 if isinstance(value, list): 

879 return np.average(value).item() 

880 return value 

881 

882 def get_better_subclass(self) -> Union[None, Type['IFCBased']]: 

883 if len(self.ports) == 3: 

884 return Junction 

885 

886 

887class Junction(PipeFitting): 

888 ifc_types = { 

889 "IfcPipeFitting": ['JUNCTION'] 

890 } 

891 

892 pattern_ifc_type = [ 

893 re.compile('T-St(ü|ue)ck', flags=re.IGNORECASE), 

894 re.compile('T-Piece', flags=re.IGNORECASE), 

895 re.compile('Kreuzst(ü|ue)ck', flags=re.IGNORECASE) 

896 ] 

897 

898 @property 

899 def expected_hvac_ports(self): 

900 return 3 

901 

902 volume = attribute.Attribute( 

903 description="Volume of the junction", 

904 unit=ureg.meter ** 3 

905 ) 

906 

907 

908class SpaceHeater(HVACProduct): 

909 ifc_types = {'IfcSpaceHeater': ['*', 'CONVECTOR', 'RADIATOR']} 

910 

911 pattern_ifc_type = [ 

912 re.compile('Heizk(ö|oe)rper', flags=re.IGNORECASE), 

913 re.compile('Space.?heater', flags=re.IGNORECASE) 

914 ] 

915 

916 @property 

917 def expected_hvac_ports(self): 

918 return 2 

919 

920 def is_consumer(self): 

921 return True 

922 

923 number_of_panels = attribute.Attribute( 

924 description="Number of panels of heater", 

925 default_ps=('Pset_SpaceHeaterTypeCommon', 'NumberOfPanels'), 

926 ) 

927 

928 number_of_sections = attribute.Attribute( 

929 description="Number of sections of heater", 

930 default_ps=('Pset_SpaceHeaterTypeCommon', 'NumberOfSections'), 

931 ) 

932 

933 thermal_efficiency = attribute.Attribute( 

934 description="Thermal efficiency of heater", 

935 default_ps=('Pset_SpaceHeaterTypeCommon', 'ThermalEfficiency'), 

936 unit=ureg.dimensionless, 

937 ) 

938 

939 body_mass = attribute.Attribute( 

940 description="Body mass of heater", 

941 default_ps=('Pset_SpaceHeaterTypeCommon', 'BodyMass'), 

942 unit=ureg.kg, 

943 ) 

944 

945 length = attribute.Attribute( 

946 description="Lenght of heater", 

947 default_ps=('Qto_SpaceHeaterBaseQuantities', 'Length'), 

948 unit=ureg.meter, 

949 ) 

950 

951 height = attribute.Attribute( 

952 description="Height of heater", 

953 unit=ureg.meter 

954 ) 

955 

956 temperature_classification = attribute.Attribute( 

957 # [HighTemperature, LowTemperature, Other, NotKnown, Unset] 

958 description="Temperature classification of heater", 

959 default_ps=('Pset_SpaceHeaterTypeCommon', 'TemperatureClassification'), 

960 ) 

961 

962 rated_power = attribute.Attribute( 

963 description="Rated power of SpaceHeater", 

964 default_ps=('Pset_SpaceHeaterTypeCommon', 'OutputCapacity'), 

965 unit=ureg.kilowatt, 

966 ) 

967 

968 flow_temperature = attribute.Attribute( 

969 description="Flow temperature", 

970 unit=ureg.celsius, 

971 ) 

972 

973 return_temperature = attribute.Attribute( 

974 description="Return temperature", 

975 unit=ureg.celsius, 

976 ) 

977 

978 medium = attribute.Attribute( 

979 # [Steam, Water, Other, NotKnown, Unset] 

980 description="Medium of SpaceHeater", 

981 default_ps=('Pset_SpaceHeaterTypeCommon', 'HeatTransferMedium'), 

982 ) 

983 

984 heat_capacity = attribute.Attribute( 

985 description="Heat capacity of heater", 

986 default_ps=('Pset_SpaceHeaterTypeCommon', 'ThermalMassHeatCapacity'), 

987 unit=ureg.joule / ureg.kelvin, 

988 ) 

989 

990 def _calc_dT_water(self, name) -> ureg.Quantity: 

991 """Function to calculate the delta temperature of the boiler using the 

992 return and flow temperature""" 

993 return self.flow_temperature - self.return_temperature 

994 

995 dT_water = attribute.Attribute( 

996 description="Nominal temperature difference", 

997 unit=ureg.kelvin, 

998 functions=[_calc_dT_water], 

999 ) 

1000 

1001 

1002class ExpansionTank(HVACProduct): 

1003 ifc_types = { 

1004 "IfcTank": 

1005 ['BREAKPRESSURE', 'EXPANSION', 'FEEDANDEXPANSION'] 

1006 } 

1007 pattern_ifc_type = [ 

1008 re.compile('Expansion.?Tank', flags=re.IGNORECASE), 

1009 re.compile('Ausdehnungs.?gef(ä|ae)(ss|ß)', flags=re.IGNORECASE), 

1010 ] 

1011 

1012 @property 

1013 def expected_hvac_ports(self): 

1014 return 1 

1015 

1016 

1017class Storage(HVACProduct): 

1018 ifc_types = { 

1019 "IfcTank": 

1020 ['*', 'BASIN', 'STORAGE', 'VESSEL'] 

1021 } 

1022 pattern_ifc_type = [ 

1023 re.compile('Speicher', flags=re.IGNORECASE), 

1024 re.compile('Puffer.?speicher', flags=re.IGNORECASE), 

1025 re.compile('Trinkwarmwasser.?speicher', flags=re.IGNORECASE), 

1026 re.compile('Trinkwarmwasser.?speicher', flags=re.IGNORECASE), 

1027 re.compile('storage', flags=re.IGNORECASE), 

1028 ] 

1029 

1030 conditions = [ 

1031 condition.RangeCondition('volume', 50 * ureg.liter, 

1032 math.inf * ureg.liter) 

1033 ] 

1034 

1035 @property 

1036 def expected_hvac_ports(self): 

1037 return float('inf') 

1038 

1039 def _calc_volume(self, name) -> ureg.Quantity: 

1040 """ 

1041 Calculate volume of storage. 

1042 """ 

1043 return self.height * self.diameter ** 2 / 4 * math.pi 

1044 

1045 storage_type = attribute.Attribute( 

1046 # [Ice, Water, RainWater, WasteWater, PotableWater, Fuel, Oil, Other, 

1047 # NotKnown, Unset] 

1048 description="Tanks's storage type (fluid type)", 

1049 default_ps=('Pset_TankTypeCommon', 'StorageType'), 

1050 ) 

1051 

1052 height = attribute.Attribute( 

1053 description="Height of the tank", 

1054 default_ps=('Pset_TankTypeCommon', 'NominalDepth'), 

1055 unit=ureg.meter 

1056 ) 

1057 

1058 diameter = attribute.Attribute( 

1059 description="Diameter of the tank", 

1060 default_ps=('Pset_TankTypeCommon', 'NominalLengthOrDiameter'), 

1061 unit=ureg.meter, 

1062 ) 

1063 

1064 volume = attribute.Attribute( 

1065 description="Volume of the tank", 

1066 default_ps=('Pset_TankTypeCommon', 'NominalCapacity'), 

1067 unit=ureg.meter ** 3, 

1068 functions=[_calc_volume] 

1069 ) 

1070 

1071 number_of_sections = attribute.Attribute( 

1072 description="Number of sections of the tank", 

1073 default_ps=('Pset_TankTypeCommon', 'NumberOfSections'), 

1074 unit=ureg.dimensionless, 

1075 ) 

1076 

1077 

1078class Distributor(HVACProduct): 

1079 ifc_types = { 

1080 "IfcDistributionChamberElement": 

1081 ['*', 'FORMEDDUCT', 'INSPECTIONCHAMBER', 'INSPECTIONPIT', 

1082 'MANHOLE', 'METERCHAMBER', 'SUMP', 'TRENCH', 'VALVECHAMBER'], 

1083 "IfcPipeFitting": 

1084 ['NOTDEFINED', 'USERDEFINED'] 

1085 } 

1086 # TODO why is pipefitting for DH found as Pipefitting and not distributor 

1087 

1088 @property 

1089 def expected_hvac_ports(self): 

1090 return (2, float('inf')) 

1091 

1092 pattern_ifc_type = [ 

1093 re.compile('Distribution.?chamber', flags=re.IGNORECASE), 

1094 re.compile('Distributor', flags=re.IGNORECASE), 

1095 re.compile('Verteiler', flags=re.IGNORECASE) 

1096 ] 

1097 

1098 # volume = attribute.Attribute( 

1099 # description="Volume of the Distributor", 

1100 # unit=ureg.meter ** 3 

1101 # ) 

1102 

1103 nominal_power = attribute.Attribute( 

1104 description="Nominal power of Distributor", 

1105 unit=ureg.kilowatt 

1106 ) 

1107 rated_mass_flow = attribute.Attribute( 

1108 description="Rated mass flow of Distributor", 

1109 unit=ureg.kg / ureg.s, 

1110 ) 

1111 

1112 

1113class Pump(HVACProduct): 

1114 ifc_types = { 

1115 "IfcPump": 

1116 ['*', 'CIRCULATOR', 'ENDSUCTION', 'SPLITCASE', 

1117 'SUBMERSIBLEPUMP', 'SUMPPUMP', 'VERTICALINLINE', 

1118 'VERTICALTURBINE'] 

1119 } 

1120 

1121 @property 

1122 def expected_hvac_ports(self): 

1123 return 2 

1124 

1125 pattern_ifc_type = [ 

1126 re.compile('Pumpe', flags=re.IGNORECASE), 

1127 re.compile('Pump', flags=re.IGNORECASE) 

1128 ] 

1129 

1130 rated_current = attribute.Attribute( 

1131 description="Rated current of pump", 

1132 default_ps=('Pset_ElectricalDeviceCommon', 'RatedCurrent'), 

1133 unit=ureg.ampere, 

1134 ) 

1135 rated_voltage = attribute.Attribute( 

1136 description="Rated current of pump", 

1137 default_ps=('Pset_ElectricalDeviceCommon', 'RatedVoltage'), 

1138 unit=ureg.volt, 

1139 ) 

1140 

1141 def _calc_rated_power(self, name) -> ureg.Quantity: 

1142 """Function to calculate the pump rated power using the rated current 

1143 and rated voltage""" 

1144 if self.rated_current and self.rated_voltage: 

1145 return self.rated_current * self.rated_voltage 

1146 else: 

1147 return None 

1148 

1149 rated_power = attribute.Attribute( 

1150 description="Rated power of pump", 

1151 unit=ureg.kilowatt, 

1152 functions=[_calc_rated_power], 

1153 ) 

1154 

1155 # Even if this is a bounded value, currently only the set point is used 

1156 rated_mass_flow = attribute.Attribute( 

1157 description="Rated mass flow of pump", 

1158 default_ps=('Pset_PumpTypeCommon', 'FlowRateRange'), 

1159 unit=ureg.kg / ureg.s, 

1160 ) 

1161 

1162 rated_volume_flow = attribute.Attribute( 

1163 description="Rated volume flow of pump", 

1164 unit=ureg.m ** 3 / ureg.hour, 

1165 ) 

1166 

1167 # Even if this is a bounded value, currently only the set point is used 

1168 rated_pressure_difference = attribute.Attribute( 

1169 description="Rated height or rated pressure difference of pump", 

1170 default_ps=('Pset_PumpTypeCommon', 'FlowResistanceRange'), 

1171 unit=ureg.newton / (ureg.m ** 2), 

1172 ) 

1173 

1174 rated_height = attribute.Attribute( 

1175 description="Rated height or rated pressure difference of pump", 

1176 unit=ureg.meter, 

1177 ) 

1178 

1179 nominal_rotation_speed = attribute.Attribute( 

1180 description="nominal rotation speed of pump", 

1181 default_ps=('Pset_PumpTypeCommon', 'NominalRotationSpeed'), 

1182 unit=1 / ureg.s, 

1183 ) 

1184 

1185 diameter = attribute.Attribute( 

1186 unit=ureg.meter, 

1187 ) 

1188 

1189 

1190class Valve(HVACProduct): 

1191 ifc_types = { 

1192 "IfcValve": 

1193 ['*', 'AIRRELEASE', 'ANTIVACUUM', 'CHANGEOVER', 'CHECK', 

1194 'COMMISSIONING', 'DIVERTING', 'DRAWOFFCOCK', 'DOUBLECHECK', 

1195 'DOUBLEREGULATING', 'FAUCET', 'FLUSHING', 'GASCOCK', 

1196 'GASTAP', 'ISOLATING', 'MIXING', 'PRESSUREREDUCING', 

1197 'PRESSURERELIEF', 'REGULATING', 'SAFETYCUTOFF', 'STEAMTRAP', 

1198 'STOPCOCK'] 

1199 } 

1200 

1201 @property 

1202 def expected_hvac_ports(self): 

1203 return 2 

1204 

1205 # expected_hvac_ports = 2 

1206 

1207 pattern_ifc_type = [ 

1208 re.compile('Valve', flags=re.IGNORECASE), 

1209 re.compile('Drossel', flags=re.IGNORECASE), 

1210 re.compile('Ventil', flags=re.IGNORECASE) 

1211 ] 

1212 

1213 conditions = [ 

1214 condition.RangeCondition("diameter", 5.0 * ureg.millimeter, 

1215 500.00 * ureg.millimeter) 

1216 ] 

1217 

1218 nominal_pressure_difference = attribute.Attribute( 

1219 description="Nominal pressure difference of valve", 

1220 default_ps=('Pset_ValveTypeCommon', 'CloseOffRating'), 

1221 unit=ureg.pascal, 

1222 ) 

1223 

1224 kv_value = attribute.Attribute( 

1225 description="kv_value of valve", 

1226 default_ps=('Pset_ValveTypeCommon', 'FlowCoefficient'), 

1227 ) 

1228 

1229 valve_pattern = attribute.Attribute( 

1230 # [SinglePort, Angled2Port, Straight2Port, Straight3Port, 

1231 # Crossover2Port, Other, NotKnown, Unset] 

1232 description="Nominal pressure difference of valve", 

1233 default_ps=('Pset_ValveTypeCommon', 'ValvePattern'), 

1234 ) 

1235 

1236 diameter = attribute.Attribute( 

1237 description='Valve diameter', 

1238 default_ps=('Pset_ValveTypeCommon', 'Size'), 

1239 unit=ureg.millimeter, 

1240 patterns=[ 

1241 re.compile('.*Durchmesser.*', flags=re.IGNORECASE), 

1242 re.compile('.*Diameter.*', flags=re.IGNORECASE), 

1243 re.compile('.*DN.*', flags=re.IGNORECASE), 

1244 ], 

1245 ) 

1246 

1247 length = attribute.Attribute( 

1248 description='Length of Valve', 

1249 unit=ureg.meter, 

1250 ) 

1251 

1252 nominal_mass_flow_rate = attribute.Attribute( 

1253 description='Nominal mass flow rate of the valve', 

1254 unit=ureg.kg / ureg.s, 

1255 ) 

1256 

1257 

1258class ThreeWayValve(Valve): 

1259 ifc_types = { 

1260 "IfcValve": 

1261 ['MIXING'] 

1262 } 

1263 

1264 pattern_ifc_type = [ 

1265 re.compile('3-Wege.*?ventil', flags=re.IGNORECASE) 

1266 ] 

1267 

1268 @property 

1269 def expected_hvac_ports(self): 

1270 return 3 

1271 

1272 

1273class Duct(HVACProduct): 

1274 ifc_types = {"IfcDuctSegment": ['*', 'RIGIDSEGMENT', 'FLEXIBLESEGMENT']} 

1275 

1276 pattern_ifc_type = [ 

1277 re.compile('Duct.?segment', flags=re.IGNORECASE) 

1278 ] 

1279 

1280 diameter = attribute.Attribute( 

1281 description='Duct diameter', 

1282 unit=ureg.millimeter, 

1283 ) 

1284 length = attribute.Attribute( 

1285 description='Length of Duct', 

1286 unit=ureg.meter, 

1287 ) 

1288 

1289 

1290class DuctFitting(HVACProduct): 

1291 ifc_types = { 

1292 "IfcDuctFitting": 

1293 ['*', 'BEND', 'CONNECTOR', 'ENTRY', 'EXIT', 'JUNCTION', 

1294 'OBSTRUCTION', 'TRANSITION'] 

1295 } 

1296 

1297 pattern_ifc_type = [ 

1298 re.compile('Duct.?fitting', flags=re.IGNORECASE) 

1299 ] 

1300 

1301 diameter = attribute.Attribute( 

1302 description='Duct diameter', 

1303 unit=ureg.millimeter, 

1304 ) 

1305 length = attribute.Attribute( 

1306 description='Length of Duct', 

1307 unit=ureg.meter, 

1308 ) 

1309 

1310 

1311class AirTerminal(HVACProduct): 

1312 ifc_types = { 

1313 "IfcAirTerminal": 

1314 ['*', 'DIFFUSER', 'GRILLE', 'LOUVRE', 'REGISTER'] 

1315 } 

1316 

1317 pattern_ifc_type = [ 

1318 re.compile('Air.?terminal', flags=re.IGNORECASE) 

1319 ] 

1320 

1321 diameter = attribute.Attribute( 

1322 description='Terminal diameter', 

1323 unit=ureg.millimeter, 

1324 ) 

1325 

1326 

1327class Medium(HVACProduct): 

1328 # is deprecated? 

1329 ifc_types = {"IfcDistributionSystem": ['*']} 

1330 pattern_ifc_type = [ 

1331 re.compile('Medium', flags=re.IGNORECASE) 

1332 ] 

1333 

1334 @property 

1335 def expected_hvac_ports(self): 

1336 return 0 

1337 

1338 

1339class CHP(HVACProduct): 

1340 ifc_types = {'IfcElectricGenerator': ['CHP']} 

1341 

1342 @property 

1343 def expected_hvac_ports(self): 

1344 return 2 

1345 

1346 rated_power = attribute.Attribute( 

1347 default_ps=('Pset_ElectricGeneratorTypeCommon', 'MaximumPowerOutput'), 

1348 description="Rated power of CHP", 

1349 patterns=[ 

1350 re.compile('.*Nennleistung', flags=re.IGNORECASE), 

1351 re.compile('.*capacity', flags=re.IGNORECASE), 

1352 ], 

1353 unit=ureg.kilowatt, 

1354 ) 

1355 

1356 efficiency = attribute.Attribute( 

1357 default_ps=( 

1358 'Pset_ElectricGeneratorTypeCommon', 'ElectricGeneratorEfficiency'), 

1359 description="Electric efficiency of CHP", 

1360 patterns=[ 

1361 re.compile('.*electric.*efficiency', flags=re.IGNORECASE), 

1362 re.compile('.*el.*efficiency', flags=re.IGNORECASE), 

1363 ], 

1364 unit=ureg.dimensionless, 

1365 ) 

1366 

1367 # water_volume = attribute.Attribute( 

1368 # description="Water volume CHP chp", 

1369 # unit=ureg.meter ** 3, 

1370 # ) 

1371 

1372 

1373# collect all domain classes 

1374items: Set[HVACProduct] = set() 

1375for name, cls in inspect.getmembers( 

1376 sys.modules[__name__], 

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

1378 and issubclass(member, HVACProduct) # domain subclass 

1379 and member is not HVACProduct # but not base class 

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

1381 items.add(cls)