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
« 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
10import numpy as np
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
19logger = logging.getLogger(__name__)
20quality_logger = logging.getLogger('bim2sim.QualityReport')
23def diameter_post_processing(value):
24 if isinstance(value, (list, set)):
25 return sum(value) / len(value)
26 return value
29def length_post_processing(value):
30 if isinstance(value, (list, set)):
31 return max(value)
32 return value
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)
41 def __init__(
42 self, *args, groups: Set = None,
43 flow_direction: int = 0, **kwargs):
44 super().__init__(*args, **kwargs)
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
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
65 kwargs['groups'] = groups
66 kwargs['flow_direction'] = flow_direction
67 return args, kwargs
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)
88 if all(coordinates == np.array([0, 0, 0])):
89 quality_logger.info("Suspect position [0, 0, 0] for %s", self)
90 return coordinates
92 @classmethod
93 def pre_validate(cls, ifc) -> bool:
94 return True
96 def validate_creation(self) -> bool:
97 return True
99 @property
100 def flow_master(self):
101 """Lock flow direction for port"""
102 return self._flow_master
104 @flow_master.setter
105 def flow_master(self, value: bool):
106 self._flow_master = value
108 @property
109 def flow_direction(self):
110 """Flow direction of port
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
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
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'
137 @property
138 def flow_side(self):
139 """
140 Flow side of port.
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
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))
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"
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))
191 if vl and not rl:
192 return 1
193 if rl and not vl:
194 return -1
195 return 0
198class HVACProduct(ProductBased):
199 domain = 'HVAC'
201 def __init__(self, *args, **kwargs):
202 super().__init__(*args, **kwargs)
203 self.inner_connections: List[Tuple[HVACPort, HVACPort]] \
204 = self.get_inner_connections()
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__} ")
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
235 def get_inner_connections(self) -> List[Tuple[HVACPort, HVACPort]]:
236 """Returns inner connections of Element.
238 By default each port is connected to each other port.
239 Overwrite for other connections."""
241 connections = []
242 for port0, port1 in itertools.combinations(self.ports, 2):
243 connections.append((port0, port1))
244 return connections
246 def decide_inner_connections(self) -> Generator[DecisionBunch, None, None]:
247 """Generator method yielding decisions to set inner connections."""
249 if len(self.ports) < 2:
250 # not possible to connect anything
251 return
253 # TODO: extend pattern
254 vl_pattern = re.compile('.*vorlauf.*', re.IGNORECASE)
255 rl_pattern = re.compile('.*rücklauf.*', re.IGNORECASE)
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
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
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))
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
319 def is_generator(self):
320 return False
322 def is_consumer(self):
323 return False
325 def calc_cost_group(self) -> [int]:
326 """Default cost group for HVAC elements is 400"""
327 return 400
329 def __repr__(self):
330 return "<%s (guid: %s, ports: %d)>" % (
331 self.__class__.__name__, self.guid, len(self.ports))
334class HeatPump(HVACProduct):
335 """"HeatPump"""
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
344 pattern_ifc_type = [
345 re.compile('Heat.?pump', flags=re.IGNORECASE),
346 re.compile('W(ä|ae)rme.?pumpe', flags=re.IGNORECASE),
347 ]
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 )
381 @property
382 def expected_hvac_ports(self):
383 return 4
386class Chiller(HVACProduct):
387 """"Chiller"""
389 ifc_types = {
390 'IfcChiller': ['*', 'AIRCOOLED', 'WATERCOOLED', 'HEATRECOVERY']}
392 pattern_ifc_type = [
393 re.compile('Chiller', flags=re.IGNORECASE),
394 re.compile('K(ä|ae)lte.?maschine', flags=re.IGNORECASE),
395 ]
397 rated_power = attribute.Attribute(
398 description='Rated power of Chiller.',
399 default_ps=('Pset_ChillerTypeCommon', 'NominalCapacity'),
400 unit=ureg.kilowatt,
401 )
403 nominal_power_consumption = attribute.Attribute(
404 description="nominal power consumption of chiller",
405 default_ps=('Pset_ChillerTypeCommon', 'NominalPowerConsumption'),
406 unit=ureg.kilowatt,
407 )
409 nominal_COP = attribute.Attribute(
410 description="Chiller efficiency at nominal load",
411 default_ps=('Pset_ChillerTypeCommon', 'NominalEfficiency'),
412 )
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 )
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 )
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 )
433 nominal_condensing_temperature = attribute.Attribute(
434 description='Nominal condenser temperature',
435 default_ps=('Pset_ChillerTypeCommon', 'NominalCondensingTemperature'),
436 unit=ureg.celsius,
437 )
439 nominal_evaporating_temperature = attribute.Attribute(
440 description='Nominal condenser temperature',
441 default_ps=('Pset_ChillerTypeCommon', 'NominalEvaporatingTemperature'),
442 unit=ureg.celsius,
443 )
445 min_power = attribute.Attribute(
446 description='Minimum power at which Chiller operates at.',
447 unit=ureg.kilowatt,
448 )
450 @property
451 def expected_hvac_ports(self):
452 return 4
455class CoolingTower(HVACProduct):
456 """"CoolingTower"""
458 ifc_types = {
459 'IfcCoolingTower':
460 ['*', 'NATURALDRAFT', 'MECHANICALINDUCEDDRAFT',
461 'MECHANICALFORCEDDRAFT']
462 }
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 ]
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 )
487 @property
488 def expected_hvac_ports(self):
489 return 2
492class HeatExchanger(HVACProduct):
493 """"Heat exchanger"""
495 ifc_types = {'IfcHeatExchanger': ['*', 'PLATE', 'SHELLANDTUBE']}
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 ]
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 )
517 @property
518 def expected_hvac_ports(self):
519 return 4
522class Boiler(HVACProduct):
523 """Boiler"""
524 ifc_types = {'IfcBoiler': ['*', 'WATER', 'STEAM']}
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 ]
532 @property
533 def expected_hvac_ports(self):
534 return 2
536 def is_generator(self):
537 """Boiler is a generator function."""
538 return True
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
548 water_volume = attribute.Attribute(
549 description="Water volume of boiler",
550 default_ps=('Pset_BoilerTypeCommon', 'WaterStorageCapacity'),
551 unit=ureg.meter ** 3,
552 )
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 )
560 nominal_power_consumption = attribute.Attribute(
561 description="nominal energy consumption of boiler",
562 default_ps=('Pset_BoilerTypeCommon', 'NominalEnergyConsumption'),
563 unit=ureg.kilowatt,
564 )
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 )
574 energy_source = attribute.Attribute(
575 description="Final energy source of boiler",
576 default_ps=('Pset_BoilerTypeCommon', 'EnergySource'),
577 )
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 )
586 part_load_ratio_range = attribute.Attribute(
587 description="Allowable part load ratio range (Bounded value).",
588 default_ps=('Pset_BoilerTypeCommon', 'NominalPartLoadRatio'),
589 )
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)
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
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 )
612 def _calc_nominal_efficiency(self, name):
613 """function to calculate the boiler nominal efficiency using the
614 efficiency curve"""
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
628 nominal_efficiency = attribute.Attribute(
629 description="""Boiler efficiency at nominal load""",
630 functions=[_calc_nominal_efficiency],
631 unit=ureg.dimensionless,
632 )
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
639 rated_power = attribute.Attribute(
640 description="Rated power of boiler",
641 unit=ureg.kilowatt,
642 functions=[_calc_rated_power],
643 )
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
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 )
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
673 min_power = attribute.Attribute(
674 description="Minimum power that boiler operates at",
675 unit=ureg.kilowatt,
676 functions=[_calc_min_power],
677 )
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
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 )
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
705 dT_water = attribute.Attribute(
706 description="Nominal temperature difference",
707 unit=ureg.kelvin,
708 functions=[_calc_dT_water],
709 )
712class Pipe(HVACProduct):
713 ifc_types = {
714 "IfcPipeSegment":
715 ['*', 'CULVERT', 'FLEXIBLESEGMENT', 'RIGIDSEGMENT', 'GUTTER',
716 'SPOOL']
717 }
719 @property
720 def expected_hvac_ports(self):
721 return 2
723 conditions = [
724 condition.RangeCondition("diameter", 5.0 * ureg.millimeter,
725 300.00 * ureg.millimeter) # ToDo: unit?!
726 ]
728 def _calc_diameter_from_radius(self, name) -> ureg.Quantity:
729 if self.radius:
730 return self.radius*2
731 else:
732 return None
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
746 radius = attribute.Attribute(
747 patterns=[
748 re.compile('.*Radius.*', flags=re.IGNORECASE)
749 ],
750 unit=ureg.millimeter
751 )
753 outer_diameter = attribute.Attribute(
754 description="Outer diameter of pipe",
755 default_ps=('Pset_PipeSegmentTypeCommon', 'OuterDiameter'),
756 unit=ureg.millimeter,
757 )
759 inner_diameter = attribute.Attribute(
760 description="Inner diameter of pipe",
761 default_ps=('Pset_PipeSegmentTypeCommon', 'InnerDiameter'),
762 unit=ureg.millimeter,
763 )
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
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 )
786 roughness_coefficient = attribute.Attribute(
787 description="Interior roughness coefficient of pipe",
788 default_ps=('Pset_PipeSegmentOccurrence',
789 'InteriorRoughnessCoefficient'),
790 unit=ureg.millimeter,
791 )
793 @staticmethod
794 def get_lenght_from_shape(ifc_representation):
795 """Search for extruded depth in representations
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)
813 return candidates[0]
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 ]
827 @property
828 def expected_hvac_ports(self):
829 return (2, 3)
831 conditions = [
832 condition.RangeCondition("diameter", 5.0 * ureg.millimeter,
833 300.00 * ureg.millimeter)
834 ]
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
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 )
858 pressure_class = attribute.Attribute(
859 unit=ureg.pascal,
860 default_ps=('Pset_PipeFittingTypeCommon', 'PressureClass')
861 )
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 )
869 roughness_coefficient = attribute.Attribute(
870 description="Roughness coefficient of pipe fitting",
871 default_ps=('Pset_PipeFittingOccurrence',
872 'InteriorRoughnessCoefficient'),
873 unit=ureg.millimeter,
874 )
876 @staticmethod
877 def _diameter_post_processing(value):
878 if isinstance(value, list):
879 return np.average(value).item()
880 return value
882 def get_better_subclass(self) -> Union[None, Type['IFCBased']]:
883 if len(self.ports) == 3:
884 return Junction
887class Junction(PipeFitting):
888 ifc_types = {
889 "IfcPipeFitting": ['JUNCTION']
890 }
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 ]
898 @property
899 def expected_hvac_ports(self):
900 return 3
902 volume = attribute.Attribute(
903 description="Volume of the junction",
904 unit=ureg.meter ** 3
905 )
908class SpaceHeater(HVACProduct):
909 ifc_types = {'IfcSpaceHeater': ['*', 'CONVECTOR', 'RADIATOR']}
911 pattern_ifc_type = [
912 re.compile('Heizk(ö|oe)rper', flags=re.IGNORECASE),
913 re.compile('Space.?heater', flags=re.IGNORECASE)
914 ]
916 @property
917 def expected_hvac_ports(self):
918 return 2
920 def is_consumer(self):
921 return True
923 number_of_panels = attribute.Attribute(
924 description="Number of panels of heater",
925 default_ps=('Pset_SpaceHeaterTypeCommon', 'NumberOfPanels'),
926 )
928 number_of_sections = attribute.Attribute(
929 description="Number of sections of heater",
930 default_ps=('Pset_SpaceHeaterTypeCommon', 'NumberOfSections'),
931 )
933 thermal_efficiency = attribute.Attribute(
934 description="Thermal efficiency of heater",
935 default_ps=('Pset_SpaceHeaterTypeCommon', 'ThermalEfficiency'),
936 unit=ureg.dimensionless,
937 )
939 body_mass = attribute.Attribute(
940 description="Body mass of heater",
941 default_ps=('Pset_SpaceHeaterTypeCommon', 'BodyMass'),
942 unit=ureg.kg,
943 )
945 length = attribute.Attribute(
946 description="Lenght of heater",
947 default_ps=('Qto_SpaceHeaterBaseQuantities', 'Length'),
948 unit=ureg.meter,
949 )
951 height = attribute.Attribute(
952 description="Height of heater",
953 unit=ureg.meter
954 )
956 temperature_classification = attribute.Attribute(
957 # [HighTemperature, LowTemperature, Other, NotKnown, Unset]
958 description="Temperature classification of heater",
959 default_ps=('Pset_SpaceHeaterTypeCommon', 'TemperatureClassification'),
960 )
962 rated_power = attribute.Attribute(
963 description="Rated power of SpaceHeater",
964 default_ps=('Pset_SpaceHeaterTypeCommon', 'OutputCapacity'),
965 unit=ureg.kilowatt,
966 )
968 flow_temperature = attribute.Attribute(
969 description="Flow temperature",
970 unit=ureg.celsius,
971 )
973 return_temperature = attribute.Attribute(
974 description="Return temperature",
975 unit=ureg.celsius,
976 )
978 medium = attribute.Attribute(
979 # [Steam, Water, Other, NotKnown, Unset]
980 description="Medium of SpaceHeater",
981 default_ps=('Pset_SpaceHeaterTypeCommon', 'HeatTransferMedium'),
982 )
984 heat_capacity = attribute.Attribute(
985 description="Heat capacity of heater",
986 default_ps=('Pset_SpaceHeaterTypeCommon', 'ThermalMassHeatCapacity'),
987 unit=ureg.joule / ureg.kelvin,
988 )
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
995 dT_water = attribute.Attribute(
996 description="Nominal temperature difference",
997 unit=ureg.kelvin,
998 functions=[_calc_dT_water],
999 )
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 ]
1012 @property
1013 def expected_hvac_ports(self):
1014 return 1
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 ]
1030 conditions = [
1031 condition.RangeCondition('volume', 50 * ureg.liter,
1032 math.inf * ureg.liter)
1033 ]
1035 @property
1036 def expected_hvac_ports(self):
1037 return float('inf')
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
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 )
1052 height = attribute.Attribute(
1053 description="Height of the tank",
1054 default_ps=('Pset_TankTypeCommon', 'NominalDepth'),
1055 unit=ureg.meter
1056 )
1058 diameter = attribute.Attribute(
1059 description="Diameter of the tank",
1060 default_ps=('Pset_TankTypeCommon', 'NominalLengthOrDiameter'),
1061 unit=ureg.meter,
1062 )
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 )
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 )
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
1088 @property
1089 def expected_hvac_ports(self):
1090 return (2, float('inf'))
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 ]
1098 # volume = attribute.Attribute(
1099 # description="Volume of the Distributor",
1100 # unit=ureg.meter ** 3
1101 # )
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 )
1113class Pump(HVACProduct):
1114 ifc_types = {
1115 "IfcPump":
1116 ['*', 'CIRCULATOR', 'ENDSUCTION', 'SPLITCASE',
1117 'SUBMERSIBLEPUMP', 'SUMPPUMP', 'VERTICALINLINE',
1118 'VERTICALTURBINE']
1119 }
1121 @property
1122 def expected_hvac_ports(self):
1123 return 2
1125 pattern_ifc_type = [
1126 re.compile('Pumpe', flags=re.IGNORECASE),
1127 re.compile('Pump', flags=re.IGNORECASE)
1128 ]
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 )
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
1149 rated_power = attribute.Attribute(
1150 description="Rated power of pump",
1151 unit=ureg.kilowatt,
1152 functions=[_calc_rated_power],
1153 )
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 )
1162 rated_volume_flow = attribute.Attribute(
1163 description="Rated volume flow of pump",
1164 unit=ureg.m ** 3 / ureg.hour,
1165 )
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 )
1174 rated_height = attribute.Attribute(
1175 description="Rated height or rated pressure difference of pump",
1176 unit=ureg.meter,
1177 )
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 )
1185 diameter = attribute.Attribute(
1186 unit=ureg.meter,
1187 )
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 }
1201 @property
1202 def expected_hvac_ports(self):
1203 return 2
1205 # expected_hvac_ports = 2
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 ]
1213 conditions = [
1214 condition.RangeCondition("diameter", 5.0 * ureg.millimeter,
1215 500.00 * ureg.millimeter)
1216 ]
1218 nominal_pressure_difference = attribute.Attribute(
1219 description="Nominal pressure difference of valve",
1220 default_ps=('Pset_ValveTypeCommon', 'CloseOffRating'),
1221 unit=ureg.pascal,
1222 )
1224 kv_value = attribute.Attribute(
1225 description="kv_value of valve",
1226 default_ps=('Pset_ValveTypeCommon', 'FlowCoefficient'),
1227 )
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 )
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 )
1247 length = attribute.Attribute(
1248 description='Length of Valve',
1249 unit=ureg.meter,
1250 )
1252 nominal_mass_flow_rate = attribute.Attribute(
1253 description='Nominal mass flow rate of the valve',
1254 unit=ureg.kg / ureg.s,
1255 )
1258class ThreeWayValve(Valve):
1259 ifc_types = {
1260 "IfcValve":
1261 ['MIXING']
1262 }
1264 pattern_ifc_type = [
1265 re.compile('3-Wege.*?ventil', flags=re.IGNORECASE)
1266 ]
1268 @property
1269 def expected_hvac_ports(self):
1270 return 3
1273class Duct(HVACProduct):
1274 ifc_types = {"IfcDuctSegment": ['*', 'RIGIDSEGMENT', 'FLEXIBLESEGMENT']}
1276 pattern_ifc_type = [
1277 re.compile('Duct.?segment', flags=re.IGNORECASE)
1278 ]
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 )
1290class DuctFitting(HVACProduct):
1291 ifc_types = {
1292 "IfcDuctFitting":
1293 ['*', 'BEND', 'CONNECTOR', 'ENTRY', 'EXIT', 'JUNCTION',
1294 'OBSTRUCTION', 'TRANSITION']
1295 }
1297 pattern_ifc_type = [
1298 re.compile('Duct.?fitting', flags=re.IGNORECASE)
1299 ]
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 )
1311class AirTerminal(HVACProduct):
1312 ifc_types = {
1313 "IfcAirTerminal":
1314 ['*', 'DIFFUSER', 'GRILLE', 'LOUVRE', 'REGISTER']
1315 }
1317 pattern_ifc_type = [
1318 re.compile('Air.?terminal', flags=re.IGNORECASE)
1319 ]
1321 diameter = attribute.Attribute(
1322 description='Terminal diameter',
1323 unit=ureg.millimeter,
1324 )
1327class Medium(HVACProduct):
1328 # is deprecated?
1329 ifc_types = {"IfcDistributionSystem": ['*']}
1330 pattern_ifc_type = [
1331 re.compile('Medium', flags=re.IGNORECASE)
1332 ]
1334 @property
1335 def expected_hvac_ports(self):
1336 return 0
1339class CHP(HVACProduct):
1340 ifc_types = {'IfcElectricGenerator': ['CHP']}
1342 @property
1343 def expected_hvac_ports(self):
1344 return 2
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 )
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 )
1367 # water_volume = attribute.Attribute(
1368 # description="Water volume CHP chp",
1369 # unit=ureg.meter ** 3,
1370 # )
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)