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

514 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-01 10:24 +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 return 

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

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

211 

212 @property 

213 def shape(self): 

214 shape = self.calc_product_shape() 

215 return shape 

216 

217 @property 

218 def volume(self): 

219 if hasattr(self, "net_volume"): 

220 if self.net_volume: 

221 vol = self.net_volume 

222 return vol 

223 vol = self.calc_volume_from_ifc_shape() 

224 return vol 

225 

226 def get_ports(self) -> list: 

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

228 ports = ifc2py_get_ports(self.ifc) 

229 hvac_ports = [] 

230 for port in ports: 

231 port_valid = True 

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

233 port_valid = False 

234 else: 

235 predefined_type = get_predefined_type(port) 

236 if predefined_type in [ 

237 'CABLE', 'CABLECARRIER', 'WIRELESS']: 

238 port_valid = False 

239 if port_valid: 

240 hvac_ports.append(HVACPort.from_ifc( 

241 ifc=port, parent=self)) 

242 else: 

243 logger.warning( 

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

245 port.is_a(), 

246 self.__class__.__name__, 

247 self.guid) 

248 return hvac_ports 

249 

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

251 """Returns inner connections of Element. 

252 

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

254 Overwrite for other connections.""" 

255 

256 connections = [] 

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

258 connections.append((port0, port1)) 

259 return connections 

260 

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

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

263 

264 if len(self.ports) < 2: 

265 # not possible to connect anything 

266 return 

267 

268 # TODO: extend pattern 

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

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

271 

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

273 score_vl = {} 

274 score_rl = {} 

275 for port in self.ports: 

276 bonus_vl = 0 

277 bonus_rl = 0 

278 # connected to pipe 

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

280 in [Pipe, PipeFitting]: 

281 bonus_vl += 1 

282 bonus_rl += 1 

283 # string hints 

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

285 bonus_vl += 1 

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

287 bonus_rl += 1 

288 # flow direction 

289 if port.flow_direction == 1: 

290 bonus_vl += .5 

291 if port.flow_direction == -1: 

292 bonus_rl += .5 

293 score_vl[port] = bonus_vl 

294 score_rl[port] = bonus_rl 

295 

296 # created sorted choices 

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

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

299 reverse=True)] 

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

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

302 reverse=True)] 

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

304 choices=choices_vl, 

305 default=choices_vl[0], # best guess 

306 key='VL', 

307 global_key='VL_port_of_' + self.guid) 

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

309 choices=choices_rl, 

310 default=choices_rl[0], # best guess 

311 key='RL', 

312 global_key='RL_port_of_' + self.guid) 

313 decisions = DecisionBunch((decision_vl, decision_rl)) 

314 yield decisions 

315 

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

317 vl = port_dict[decision_vl.value] 

318 rl = port_dict[decision_rl.value] 

319 # set flow correct side 

320 vl.flow_side = 1 

321 rl.flow_side = -1 

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

323 

324 def validate_ports(self): 

325 if isinstance(self.expected_hvac_ports, tuple): 

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

327 <= self.expected_hvac_ports[-1]: 

328 return True 

329 else: 

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

331 return True 

332 return False 

333 

334 def is_generator(self): 

335 return False 

336 

337 def is_consumer(self): 

338 return False 

339 

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

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

342 return 400 

343 

344 def __repr__(self): 

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

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

347 

348 

349class HeatPump(HVACProduct): 

350 """"HeatPump""" 

351 

352 ifc_types = { 

353 'IfcUnitaryEquipment': ['*'] 

354 } 

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

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

357 # identify heat pumps 

358 

359 pattern_ifc_type = [ 

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

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

362 ] 

363 

364 min_power = attribute.Attribute( 

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

366 unit=ureg.kilowatt, 

367 ) 

368 rated_power = attribute.Attribute( 

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

370 unit=ureg.kilowatt, 

371 ) 

372 efficiency = attribute.Attribute( 

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

374 '[percentage_of_rated_power,efficiency]', 

375 unit=ureg.dimensionless 

376 ) 

377 vdi_performance_data_table=attribute.Attribute( 

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

379 ) 

380 is_reversible = attribute.Attribute( 

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

382 unit=ureg.dimensionless 

383 ) 

384 rated_cooling_power = attribute.Attribute( 

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

386 unit=ureg.kilowatt, 

387 ) 

388 COP = attribute.Attribute( 

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

390 unit=ureg.dimensionless 

391 ) 

392 internal_pump = attribute.Attribute( 

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

394 ) 

395 

396 @property 

397 def expected_hvac_ports(self): 

398 return 4 

399 

400 

401class Chiller(HVACProduct): 

402 """"Chiller""" 

403 

404 ifc_types = { 

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

406 

407 pattern_ifc_type = [ 

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

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

410 ] 

411 

412 rated_power = attribute.Attribute( 

413 description='Rated power of Chiller.', 

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

415 unit=ureg.kilowatt, 

416 ) 

417 

418 nominal_power_consumption = attribute.Attribute( 

419 description="nominal power consumption of chiller", 

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

421 unit=ureg.kilowatt, 

422 ) 

423 

424 nominal_COP = attribute.Attribute( 

425 description="Chiller efficiency at nominal load", 

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

427 ) 

428 

429 capacity_curve = attribute.Attribute( 

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

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

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

433 ) 

434 

435 COP = attribute.Attribute( 

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

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

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

439 ) 

440 

441 full_load_ratio = attribute.Attribute( 

442 # (FracFullLoadPower, PartLoadRatio) 

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

444 "temperature", 

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

446 ) 

447 

448 nominal_condensing_temperature = attribute.Attribute( 

449 description='Nominal condenser temperature', 

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

451 unit=ureg.celsius, 

452 ) 

453 

454 nominal_evaporating_temperature = attribute.Attribute( 

455 description='Nominal condenser temperature', 

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

457 unit=ureg.celsius, 

458 ) 

459 

460 min_power = attribute.Attribute( 

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

462 unit=ureg.kilowatt, 

463 ) 

464 

465 @property 

466 def expected_hvac_ports(self): 

467 return 4 

468 

469 

470class CoolingTower(HVACProduct): 

471 """"CoolingTower""" 

472 

473 ifc_types = { 

474 'IfcCoolingTower': 

475 ['*', 'NATURALDRAFT', 'MECHANICALINDUCEDDRAFT', 

476 'MECHANICALFORCEDDRAFT'] 

477 } 

478 

479 pattern_ifc_type = [ 

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

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

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

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

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

485 ] 

486 

487 min_power = attribute.Attribute( 

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

489 unit=ureg.kilowatt, 

490 ) 

491 rated_power = attribute.Attribute( 

492 description='Rated power of CoolingTower.', 

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

494 unit=ureg.kilowatt, 

495 ) 

496 efficiency = attribute.Attribute( 

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

498 '[percentage_of_rated_power,efficiency]', 

499 unit=ureg.dimensionless, 

500 ) 

501 

502 @property 

503 def expected_hvac_ports(self): 

504 return 2 

505 

506 

507class HeatExchanger(HVACProduct): 

508 """"Heat exchanger""" 

509 

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

511 

512 pattern_ifc_type = [ 

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

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

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

516 ] 

517 

518 min_power = attribute.Attribute( 

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

520 unit=ureg.kilowatt, 

521 ) 

522 rated_power = attribute.Attribute( 

523 description='Rated power of HeatExchange.', 

524 unit=ureg.kilowatt, 

525 ) 

526 efficiency = attribute.Attribute( 

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

528 '[percentage_of_rated_power,efficiency]', 

529 unit=ureg.dimensionless, 

530 ) 

531 

532 @property 

533 def expected_hvac_ports(self): 

534 return 4 

535 

536 

537class Boiler(HVACProduct): 

538 """Boiler""" 

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

540 

541 pattern_ifc_type = [ 

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

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

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

545 ] 

546 

547 @property 

548 def expected_hvac_ports(self): 

549 return 2 

550 

551 def is_generator(self): 

552 """Boiler is a generator function.""" 

553 return True 

554 

555 def get_inner_connections(self): 

556 # TODO see #167 

557 if len(self.ports) > 2: 

558 return [] 

559 else: 

560 connections = super().get_inner_connections() 

561 return connections 

562 

563 water_volume = attribute.Attribute( 

564 description="Water volume of boiler", 

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

566 unit=ureg.meter ** 3, 

567 ) 

568 

569 dry_mass = attribute.Attribute( 

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

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

572 unit=ureg.kg, 

573 ) 

574 

575 nominal_power_consumption = attribute.Attribute( 

576 description="nominal energy consumption of boiler", 

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

578 unit=ureg.kilowatt, 

579 ) 

580 

581 efficiency = attribute.Attribute( 

582 # (Efficiency, PartialLoadFactor) 

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

584 "percentage_of_rated_power and efficiency", 

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

586 unit=ureg.dimensionless, 

587 ) 

588 

589 energy_source = attribute.Attribute( 

590 description="Final energy source of boiler", 

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

592 ) 

593 

594 operating_mode = attribute.Attribute( 

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

596 description="Boiler's operating mode", 

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

598 unit=ureg.dimensionless, 

599 ) 

600 

601 part_load_ratio_range = attribute.Attribute( 

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

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

604 ) 

605 

606 def _get_minimal_part_load_ratio(self, name): 

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

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

609 # in ifc2python 

610 if hasattr(self, "part_load_ratio_range"): 

611 return min(self.part_load_ratio_range) 

612 

613 def _normalise_value_zero_to_one(self, value): 

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

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

616 return value * 0.01 

617 

618 minimal_part_load_ratio = attribute.Attribute( 

619 description="Minimal part load ratio", 

620 functions=[_get_minimal_part_load_ratio], 

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

622 # 0 and 1 

623 ifc_postprocessing=[_normalise_value_zero_to_one] 

624 ) 

625 

626 

627 def _calc_nominal_efficiency(self, name): 

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

629 efficiency curve""" 

630 

631 if isinstance(self.efficiency, list): 

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

633 nominal_eff = efficiency_curve.get(1, None) 

634 if nominal_eff: 

635 return nominal_eff 

636 else: 

637 # ToDo: linear regression 

638 raise NotImplementedError 

639 else: 

640 # WORKAROUND: input of lists is not yet implemented 

641 return self.efficiency 

642 

643 nominal_efficiency = attribute.Attribute( 

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

645 functions=[_calc_nominal_efficiency], 

646 unit=ureg.dimensionless, 

647 ) 

648 

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

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

651 efficiency and the nominal power consumption""" 

652 return self.nominal_efficiency * self.nominal_power_consumption 

653 

654 rated_power = attribute.Attribute( 

655 description="Rated power of boiler", 

656 unit=ureg.kilowatt, 

657 functions=[_calc_rated_power], 

658 ) 

659 

660 def _calc_partial_load_efficiency(self, name): 

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

662 nominal partial ratio and the efficiency curve""" 

663 if isinstance(self.efficiency, list): 

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

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

666 None) 

667 if partial_eff: 

668 return partial_eff 

669 else: 

670 # ToDo: linear regression 

671 raise NotImplementedError 

672 else: 

673 # WORKAROUND: input of lists is not yet implemented 

674 return self.efficiency 

675 

676 partial_load_efficiency = attribute.Attribute( 

677 description="Boiler efficiency at partial load", 

678 functions=[_calc_partial_load_efficiency], 

679 unit=ureg.dimensionless, 

680 default=0.15 

681 ) 

682 

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

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

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

686 return self.partial_load_efficiency * self.nominal_power_consumption 

687 

688 min_power = attribute.Attribute( 

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

690 unit=ureg.kilowatt, 

691 functions=[_calc_min_power], 

692 ) 

693 

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

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

696 power and the rated power""" 

697 return self.min_power / self.rated_power 

698 

699 min_PLR = attribute.Attribute( 

700 description="Minimum Part load ratio", 

701 unit=ureg.dimensionless, 

702 functions=[_calc_min_PLR], 

703 ) 

704 flow_temperature = attribute.Attribute( 

705 description="Nominal inlet temperature", 

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

707 unit=ureg.celsius, 

708 ) 

709 return_temperature = attribute.Attribute( 

710 description="Nominal outlet temperature", 

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

712 unit=ureg.celsius, 

713 ) 

714 

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

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

717 return and flow temperature""" 

718 return self.return_temperature - self.flow_temperature 

719 

720 dT_water = attribute.Attribute( 

721 description="Nominal temperature difference", 

722 unit=ureg.kelvin, 

723 functions=[_calc_dT_water], 

724 ) 

725 

726 

727class Pipe(HVACProduct): 

728 ifc_types = { 

729 "IfcPipeSegment": 

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

731 'SPOOL'] 

732 } 

733 

734 @property 

735 def expected_hvac_ports(self): 

736 return 2 

737 

738 conditions = [ 

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

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

741 ] 

742 

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

744 if self.radius: 

745 return self.radius*2 

746 else: 

747 return None 

748 

749 diameter = attribute.Attribute( 

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

751 unit=ureg.millimeter, 

752 patterns=[ 

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

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

755 ], 

756 functions=[_calc_diameter_from_radius], 

757 ifc_postprocessing=diameter_post_processing, 

758 ) 

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

760 

761 radius = attribute.Attribute( 

762 patterns=[ 

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

764 ], 

765 unit=ureg.millimeter 

766 ) 

767 

768 outer_diameter = attribute.Attribute( 

769 description="Outer diameter of pipe", 

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

771 unit=ureg.millimeter, 

772 ) 

773 

774 inner_diameter = attribute.Attribute( 

775 description="Inner diameter of pipe", 

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

777 unit=ureg.millimeter, 

778 ) 

779 

780 def _length_from_geometry(self, name): 

781 """ 

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

783 """ 

784 try: 

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

786 * ureg.meter 

787 except AttributeError: 

788 return None 

789 

790 length = attribute.Attribute( 

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

792 unit=ureg.meter, 

793 patterns=[ 

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

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

796 ], 

797 ifc_postprocessing=length_post_processing, 

798 functions=[_length_from_geometry], 

799 ) 

800 

801 roughness_coefficient = attribute.Attribute( 

802 description="Interior roughness coefficient of pipe", 

803 default_ps=('Pset_PipeSegmentOccurrence', 

804 'InteriorRoughnessCoefficient'), 

805 unit=ureg.millimeter, 

806 ) 

807 

808 @staticmethod 

809 def get_lenght_from_shape(ifc_representation): 

810 """Search for extruded depth in representations 

811 

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

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

814 candidates = [] 

815 try: 

816 for representation in ifc_representation.Representations: 

817 for item in representation.Items: 

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

819 candidates.append(item.Depth) 

820 except: 

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

822 if not candidates: 

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

824 if len(candidates) > 1: 

825 raise AttributeError( 

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

827 

828 return candidates[0] 

829 

830 

831class PipeFitting(HVACProduct): 

832 ifc_types = { 

833 "IfcPipeFitting": 

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

835 'OBSTRUCTION', 'TRANSITION'] 

836 } 

837 pattern_ifc_type = [ 

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

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

840 ] 

841 

842 @property 

843 def expected_hvac_ports(self): 

844 return (2, 3) 

845 

846 conditions = [ 

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

848 300.00 * ureg.millimeter) 

849 ] 

850 

851 diameter = attribute.Attribute( 

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

853 unit=ureg.millimeter, 

854 patterns=[ 

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

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

857 ], 

858 ifc_postprocessing=diameter_post_processing, 

859 ) 

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

861 

862 length = attribute.Attribute( 

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

864 unit=ureg.meter, 

865 patterns=[ 

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

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

868 ], 

869 default=0, 

870 ifc_postprocessing=length_post_processing 

871 ) 

872 

873 pressure_class = attribute.Attribute( 

874 unit=ureg.pascal, 

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

876 ) 

877 

878 pressure_loss_coefficient = attribute.Attribute( 

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

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

881 unit=ureg.pascal, 

882 ) 

883 

884 roughness_coefficient = attribute.Attribute( 

885 description="Roughness coefficient of pipe fitting", 

886 default_ps=('Pset_PipeFittingOccurrence', 

887 'InteriorRoughnessCoefficient'), 

888 unit=ureg.millimeter, 

889 ) 

890 

891 @staticmethod 

892 def _diameter_post_processing(value): 

893 if isinstance(value, list): 

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

895 return value 

896 

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

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

899 return Junction 

900 

901 

902class Junction(PipeFitting): 

903 ifc_types = { 

904 "IfcPipeFitting": ['JUNCTION'] 

905 } 

906 

907 pattern_ifc_type = [ 

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

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

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

911 ] 

912 

913 @property 

914 def expected_hvac_ports(self): 

915 return 3 

916 

917 volume = attribute.Attribute( 

918 description="Volume of the junction", 

919 unit=ureg.meter ** 3 

920 ) 

921 

922 

923class SpaceHeater(HVACProduct): 

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

925 

926 pattern_ifc_type = [ 

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

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

929 ] 

930 

931 @property 

932 def expected_hvac_ports(self): 

933 return 2 

934 

935 def is_consumer(self): 

936 return True 

937 

938 number_of_panels = attribute.Attribute( 

939 description="Number of panels of heater", 

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

941 ) 

942 

943 number_of_sections = attribute.Attribute( 

944 description="Number of sections of heater", 

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

946 ) 

947 

948 thermal_efficiency = attribute.Attribute( 

949 description="Thermal efficiency of heater", 

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

951 unit=ureg.dimensionless, 

952 ) 

953 

954 body_mass = attribute.Attribute( 

955 description="Body mass of heater", 

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

957 unit=ureg.kg, 

958 ) 

959 

960 length = attribute.Attribute( 

961 description="Lenght of heater", 

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

963 unit=ureg.meter, 

964 ) 

965 

966 height = attribute.Attribute( 

967 description="Height of heater", 

968 unit=ureg.meter 

969 ) 

970 

971 temperature_classification = attribute.Attribute( 

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

973 description="Temperature classification of heater", 

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

975 ) 

976 

977 rated_power = attribute.Attribute( 

978 description="Rated power of SpaceHeater", 

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

980 unit=ureg.kilowatt, 

981 ) 

982 

983 flow_temperature = attribute.Attribute( 

984 description="Flow temperature", 

985 unit=ureg.celsius, 

986 ) 

987 

988 return_temperature = attribute.Attribute( 

989 description="Return temperature", 

990 unit=ureg.celsius, 

991 ) 

992 

993 medium = attribute.Attribute( 

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

995 description="Medium of SpaceHeater", 

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

997 ) 

998 

999 heat_capacity = attribute.Attribute( 

1000 description="Heat capacity of heater", 

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

1002 unit=ureg.joule / ureg.kelvin, 

1003 ) 

1004 

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

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

1007 return and flow temperature""" 

1008 return self.flow_temperature - self.return_temperature 

1009 

1010 dT_water = attribute.Attribute( 

1011 description="Nominal temperature difference", 

1012 unit=ureg.kelvin, 

1013 functions=[_calc_dT_water], 

1014 ) 

1015 

1016 

1017class ExpansionTank(HVACProduct): 

1018 ifc_types = { 

1019 "IfcTank": 

1020 ['BREAKPRESSURE', 'EXPANSION', 'FEEDANDEXPANSION'] 

1021 } 

1022 pattern_ifc_type = [ 

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

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

1025 ] 

1026 

1027 @property 

1028 def expected_hvac_ports(self): 

1029 return 1 

1030 

1031 

1032class Storage(HVACProduct): 

1033 ifc_types = { 

1034 "IfcTank": 

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

1036 } 

1037 pattern_ifc_type = [ 

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

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

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

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

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

1043 ] 

1044 

1045 conditions = [ 

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

1047 math.inf * ureg.liter) 

1048 ] 

1049 

1050 @property 

1051 def expected_hvac_ports(self): 

1052 return float('inf') 

1053 

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

1055 """ 

1056 Calculate volume of storage. 

1057 """ 

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

1059 

1060 storage_type = attribute.Attribute( 

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

1062 # NotKnown, Unset] 

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

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

1065 ) 

1066 

1067 height = attribute.Attribute( 

1068 description="Height of the tank", 

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

1070 unit=ureg.meter 

1071 ) 

1072 

1073 diameter = attribute.Attribute( 

1074 description="Diameter of the tank", 

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

1076 unit=ureg.meter, 

1077 ) 

1078 

1079 volume = attribute.Attribute( 

1080 description="Volume of the tank", 

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

1082 unit=ureg.meter ** 3, 

1083 functions=[_calc_volume] 

1084 ) 

1085 

1086 number_of_sections = attribute.Attribute( 

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

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

1089 unit=ureg.dimensionless, 

1090 ) 

1091 

1092 

1093class Distributor(HVACProduct): 

1094 ifc_types = { 

1095 "IfcDistributionChamberElement": 

1096 ['*', 'FORMEDDUCT', 'INSPECTIONCHAMBER', 'INSPECTIONPIT', 

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

1098 "IfcPipeFitting": 

1099 ['NOTDEFINED', 'USERDEFINED'] 

1100 } 

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

1102 

1103 @property 

1104 def expected_hvac_ports(self): 

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

1106 

1107 pattern_ifc_type = [ 

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

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

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

1111 ] 

1112 

1113 # volume = attribute.Attribute( 

1114 # description="Volume of the Distributor", 

1115 # unit=ureg.meter ** 3 

1116 # ) 

1117 

1118 nominal_power = attribute.Attribute( 

1119 description="Nominal power of Distributor", 

1120 unit=ureg.kilowatt 

1121 ) 

1122 rated_mass_flow = attribute.Attribute( 

1123 description="Rated mass flow of Distributor", 

1124 unit=ureg.kg / ureg.s, 

1125 ) 

1126 

1127 

1128class Pump(HVACProduct): 

1129 ifc_types = { 

1130 "IfcPump": 

1131 ['*', 'CIRCULATOR', 'ENDSUCTION', 'SPLITCASE', 

1132 'SUBMERSIBLEPUMP', 'SUMPPUMP', 'VERTICALINLINE', 

1133 'VERTICALTURBINE'] 

1134 } 

1135 

1136 @property 

1137 def expected_hvac_ports(self): 

1138 return 2 

1139 

1140 pattern_ifc_type = [ 

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

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

1143 ] 

1144 

1145 rated_current = attribute.Attribute( 

1146 description="Rated current of pump", 

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

1148 unit=ureg.ampere, 

1149 ) 

1150 rated_voltage = attribute.Attribute( 

1151 description="Rated current of pump", 

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

1153 unit=ureg.volt, 

1154 ) 

1155 

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

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

1158 and rated voltage""" 

1159 if self.rated_current and self.rated_voltage: 

1160 return self.rated_current * self.rated_voltage 

1161 else: 

1162 return None 

1163 

1164 rated_power = attribute.Attribute( 

1165 description="Rated power of pump", 

1166 unit=ureg.kilowatt, 

1167 functions=[_calc_rated_power], 

1168 ) 

1169 

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

1171 rated_mass_flow = attribute.Attribute( 

1172 description="Rated mass flow of pump", 

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

1174 unit=ureg.kg / ureg.s, 

1175 ) 

1176 

1177 rated_volume_flow = attribute.Attribute( 

1178 description="Rated volume flow of pump", 

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

1180 ) 

1181 

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

1183 rated_pressure_difference = attribute.Attribute( 

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

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

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

1187 ) 

1188 

1189 rated_height = attribute.Attribute( 

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

1191 unit=ureg.meter, 

1192 ) 

1193 

1194 nominal_rotation_speed = attribute.Attribute( 

1195 description="nominal rotation speed of pump", 

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

1197 unit=1 / ureg.s, 

1198 ) 

1199 

1200 diameter = attribute.Attribute( 

1201 unit=ureg.meter, 

1202 ) 

1203 

1204 

1205class Valve(HVACProduct): 

1206 ifc_types = { 

1207 "IfcValve": 

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

1209 'COMMISSIONING', 'DIVERTING', 'DRAWOFFCOCK', 'DOUBLECHECK', 

1210 'DOUBLEREGULATING', 'FAUCET', 'FLUSHING', 'GASCOCK', 

1211 'GASTAP', 'ISOLATING', 'MIXING', 'PRESSUREREDUCING', 

1212 'PRESSURERELIEF', 'REGULATING', 'SAFETYCUTOFF', 'STEAMTRAP', 

1213 'STOPCOCK'] 

1214 } 

1215 

1216 @property 

1217 def expected_hvac_ports(self): 

1218 return 2 

1219 

1220 # expected_hvac_ports = 2 

1221 

1222 pattern_ifc_type = [ 

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

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

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

1226 ] 

1227 

1228 conditions = [ 

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

1230 500.00 * ureg.millimeter) 

1231 ] 

1232 

1233 nominal_pressure_difference = attribute.Attribute( 

1234 description="Nominal pressure difference of valve", 

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

1236 unit=ureg.pascal, 

1237 ) 

1238 

1239 kv_value = attribute.Attribute( 

1240 description="kv_value of valve", 

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

1242 ) 

1243 

1244 valve_pattern = attribute.Attribute( 

1245 # [SinglePort, Angled2Port, Straight2Port, Straight3Port, 

1246 # Crossover2Port, Other, NotKnown, Unset] 

1247 description="Nominal pressure difference of valve", 

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

1249 ) 

1250 

1251 diameter = attribute.Attribute( 

1252 description='Valve diameter', 

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

1254 unit=ureg.millimeter, 

1255 patterns=[ 

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

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

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

1259 ], 

1260 ) 

1261 

1262 length = attribute.Attribute( 

1263 description='Length of Valve', 

1264 unit=ureg.meter, 

1265 ) 

1266 

1267 nominal_mass_flow_rate = attribute.Attribute( 

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

1269 unit=ureg.kg / ureg.s, 

1270 ) 

1271 

1272 

1273class ThreeWayValve(Valve): 

1274 ifc_types = { 

1275 "IfcValve": 

1276 ['MIXING'] 

1277 } 

1278 

1279 pattern_ifc_type = [ 

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

1281 ] 

1282 

1283 @property 

1284 def expected_hvac_ports(self): 

1285 return 3 

1286 

1287 

1288class Duct(HVACProduct): 

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

1290 

1291 pattern_ifc_type = [ 

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

1293 ] 

1294 

1295 diameter = attribute.Attribute( 

1296 description='Duct diameter', 

1297 unit=ureg.millimeter, 

1298 ) 

1299 length = attribute.Attribute( 

1300 description='Length of Duct', 

1301 unit=ureg.meter, 

1302 ) 

1303 

1304 

1305class DuctFitting(HVACProduct): 

1306 ifc_types = { 

1307 "IfcDuctFitting": 

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

1309 'OBSTRUCTION', 'TRANSITION'] 

1310 } 

1311 

1312 pattern_ifc_type = [ 

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

1314 ] 

1315 

1316 diameter = attribute.Attribute( 

1317 description='Duct diameter', 

1318 unit=ureg.millimeter, 

1319 ) 

1320 length = attribute.Attribute( 

1321 description='Length of Duct', 

1322 unit=ureg.meter, 

1323 ) 

1324 

1325 

1326class AirTerminal(HVACProduct): 

1327 ifc_types = { 

1328 "IfcAirTerminal": 

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

1330 } 

1331 

1332 pattern_ifc_type = [ 

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

1334 ] 

1335 

1336 diameter = attribute.Attribute( 

1337 description='Terminal diameter', 

1338 unit=ureg.millimeter, 

1339 ) 

1340 

1341 

1342class Medium(HVACProduct): 

1343 # is deprecated? 

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

1345 pattern_ifc_type = [ 

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

1347 ] 

1348 

1349 @property 

1350 def expected_hvac_ports(self): 

1351 return 0 

1352 

1353 

1354class CHP(HVACProduct): 

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

1356 

1357 @property 

1358 def expected_hvac_ports(self): 

1359 return 2 

1360 

1361 rated_power = attribute.Attribute( 

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

1363 description="Rated power of CHP", 

1364 patterns=[ 

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

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

1367 ], 

1368 unit=ureg.kilowatt, 

1369 ) 

1370 

1371 efficiency = attribute.Attribute( 

1372 default_ps=( 

1373 'Pset_ElectricGeneratorTypeCommon', 'ElectricGeneratorEfficiency'), 

1374 description="Electric efficiency of CHP", 

1375 patterns=[ 

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

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

1378 ], 

1379 unit=ureg.dimensionless, 

1380 ) 

1381 

1382 # water_volume = attribute.Attribute( 

1383 # description="Water volume CHP chp", 

1384 # unit=ureg.meter ** 3, 

1385 # ) 

1386 

1387 

1388# collect all domain classes 

1389items: Set[HVACProduct] = set() 

1390for name, cls in inspect.getmembers( 

1391 sys.modules[__name__], 

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

1393 and issubclass(member, HVACProduct) # domain subclass 

1394 and member is not HVACProduct # but not base class 

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

1396 items.add(cls)