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
« 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
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 return
209 raise NotImplementedError(f"Please define the expected number of ports "
210 f"for the class {self.__class__.__name__} ")
212 @property
213 def shape(self):
214 shape = self.calc_product_shape()
215 return shape
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
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
250 def get_inner_connections(self) -> List[Tuple[HVACPort, HVACPort]]:
251 """Returns inner connections of Element.
253 By default each port is connected to each other port.
254 Overwrite for other connections."""
256 connections = []
257 for port0, port1 in itertools.combinations(self.ports, 2):
258 connections.append((port0, port1))
259 return connections
261 def decide_inner_connections(self) -> Generator[DecisionBunch, None, None]:
262 """Generator method yielding decisions to set inner connections."""
264 if len(self.ports) < 2:
265 # not possible to connect anything
266 return
268 # TODO: extend pattern
269 vl_pattern = re.compile('.*vorlauf.*', re.IGNORECASE)
270 rl_pattern = re.compile('.*rücklauf.*', re.IGNORECASE)
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
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
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))
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
334 def is_generator(self):
335 return False
337 def is_consumer(self):
338 return False
340 def calc_cost_group(self) -> [int]:
341 """Default cost group for HVAC elements is 400"""
342 return 400
344 def __repr__(self):
345 return "<%s (guid: %s, ports: %d)>" % (
346 self.__class__.__name__, self.guid, len(self.ports))
349class HeatPump(HVACProduct):
350 """"HeatPump"""
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
359 pattern_ifc_type = [
360 re.compile('Heat.?pump', flags=re.IGNORECASE),
361 re.compile('W(ä|ae)rme.?pumpe', flags=re.IGNORECASE),
362 ]
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 )
396 @property
397 def expected_hvac_ports(self):
398 return 4
401class Chiller(HVACProduct):
402 """"Chiller"""
404 ifc_types = {
405 'IfcChiller': ['*', 'AIRCOOLED', 'WATERCOOLED', 'HEATRECOVERY']}
407 pattern_ifc_type = [
408 re.compile('Chiller', flags=re.IGNORECASE),
409 re.compile('K(ä|ae)lte.?maschine', flags=re.IGNORECASE),
410 ]
412 rated_power = attribute.Attribute(
413 description='Rated power of Chiller.',
414 default_ps=('Pset_ChillerTypeCommon', 'NominalCapacity'),
415 unit=ureg.kilowatt,
416 )
418 nominal_power_consumption = attribute.Attribute(
419 description="nominal power consumption of chiller",
420 default_ps=('Pset_ChillerTypeCommon', 'NominalPowerConsumption'),
421 unit=ureg.kilowatt,
422 )
424 nominal_COP = attribute.Attribute(
425 description="Chiller efficiency at nominal load",
426 default_ps=('Pset_ChillerTypeCommon', 'NominalEfficiency'),
427 )
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 )
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 )
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 )
448 nominal_condensing_temperature = attribute.Attribute(
449 description='Nominal condenser temperature',
450 default_ps=('Pset_ChillerTypeCommon', 'NominalCondensingTemperature'),
451 unit=ureg.celsius,
452 )
454 nominal_evaporating_temperature = attribute.Attribute(
455 description='Nominal condenser temperature',
456 default_ps=('Pset_ChillerTypeCommon', 'NominalEvaporatingTemperature'),
457 unit=ureg.celsius,
458 )
460 min_power = attribute.Attribute(
461 description='Minimum power at which Chiller operates at.',
462 unit=ureg.kilowatt,
463 )
465 @property
466 def expected_hvac_ports(self):
467 return 4
470class CoolingTower(HVACProduct):
471 """"CoolingTower"""
473 ifc_types = {
474 'IfcCoolingTower':
475 ['*', 'NATURALDRAFT', 'MECHANICALINDUCEDDRAFT',
476 'MECHANICALFORCEDDRAFT']
477 }
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 ]
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 )
502 @property
503 def expected_hvac_ports(self):
504 return 2
507class HeatExchanger(HVACProduct):
508 """"Heat exchanger"""
510 ifc_types = {'IfcHeatExchanger': ['*', 'PLATE', 'SHELLANDTUBE']}
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 ]
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 )
532 @property
533 def expected_hvac_ports(self):
534 return 4
537class Boiler(HVACProduct):
538 """Boiler"""
539 ifc_types = {'IfcBoiler': ['*', 'WATER', 'STEAM']}
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 ]
547 @property
548 def expected_hvac_ports(self):
549 return 2
551 def is_generator(self):
552 """Boiler is a generator function."""
553 return True
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
563 water_volume = attribute.Attribute(
564 description="Water volume of boiler",
565 default_ps=('Pset_BoilerTypeCommon', 'WaterStorageCapacity'),
566 unit=ureg.meter ** 3,
567 )
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 )
575 nominal_power_consumption = attribute.Attribute(
576 description="nominal energy consumption of boiler",
577 default_ps=('Pset_BoilerTypeCommon', 'NominalEnergyConsumption'),
578 unit=ureg.kilowatt,
579 )
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 )
589 energy_source = attribute.Attribute(
590 description="Final energy source of boiler",
591 default_ps=('Pset_BoilerTypeCommon', 'EnergySource'),
592 )
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 )
601 part_load_ratio_range = attribute.Attribute(
602 description="Allowable part load ratio range (Bounded value).",
603 default_ps=('Pset_BoilerTypeCommon', 'NominalPartLoadRatio'),
604 )
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)
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
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 )
627 def _calc_nominal_efficiency(self, name):
628 """function to calculate the boiler nominal efficiency using the
629 efficiency curve"""
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
643 nominal_efficiency = attribute.Attribute(
644 description="""Boiler efficiency at nominal load""",
645 functions=[_calc_nominal_efficiency],
646 unit=ureg.dimensionless,
647 )
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
654 rated_power = attribute.Attribute(
655 description="Rated power of boiler",
656 unit=ureg.kilowatt,
657 functions=[_calc_rated_power],
658 )
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
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 )
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
688 min_power = attribute.Attribute(
689 description="Minimum power that boiler operates at",
690 unit=ureg.kilowatt,
691 functions=[_calc_min_power],
692 )
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
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 )
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
720 dT_water = attribute.Attribute(
721 description="Nominal temperature difference",
722 unit=ureg.kelvin,
723 functions=[_calc_dT_water],
724 )
727class Pipe(HVACProduct):
728 ifc_types = {
729 "IfcPipeSegment":
730 ['*', 'CULVERT', 'FLEXIBLESEGMENT', 'RIGIDSEGMENT', 'GUTTER',
731 'SPOOL']
732 }
734 @property
735 def expected_hvac_ports(self):
736 return 2
738 conditions = [
739 condition.RangeCondition("diameter", 5.0 * ureg.millimeter,
740 300.00 * ureg.millimeter) # ToDo: unit?!
741 ]
743 def _calc_diameter_from_radius(self, name) -> ureg.Quantity:
744 if self.radius:
745 return self.radius*2
746 else:
747 return None
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
761 radius = attribute.Attribute(
762 patterns=[
763 re.compile('.*Radius.*', flags=re.IGNORECASE)
764 ],
765 unit=ureg.millimeter
766 )
768 outer_diameter = attribute.Attribute(
769 description="Outer diameter of pipe",
770 default_ps=('Pset_PipeSegmentTypeCommon', 'OuterDiameter'),
771 unit=ureg.millimeter,
772 )
774 inner_diameter = attribute.Attribute(
775 description="Inner diameter of pipe",
776 default_ps=('Pset_PipeSegmentTypeCommon', 'InnerDiameter'),
777 unit=ureg.millimeter,
778 )
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
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 )
801 roughness_coefficient = attribute.Attribute(
802 description="Interior roughness coefficient of pipe",
803 default_ps=('Pset_PipeSegmentOccurrence',
804 'InteriorRoughnessCoefficient'),
805 unit=ureg.millimeter,
806 )
808 @staticmethod
809 def get_lenght_from_shape(ifc_representation):
810 """Search for extruded depth in representations
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)
828 return candidates[0]
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 ]
842 @property
843 def expected_hvac_ports(self):
844 return (2, 3)
846 conditions = [
847 condition.RangeCondition("diameter", 5.0 * ureg.millimeter,
848 300.00 * ureg.millimeter)
849 ]
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
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 )
873 pressure_class = attribute.Attribute(
874 unit=ureg.pascal,
875 default_ps=('Pset_PipeFittingTypeCommon', 'PressureClass')
876 )
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 )
884 roughness_coefficient = attribute.Attribute(
885 description="Roughness coefficient of pipe fitting",
886 default_ps=('Pset_PipeFittingOccurrence',
887 'InteriorRoughnessCoefficient'),
888 unit=ureg.millimeter,
889 )
891 @staticmethod
892 def _diameter_post_processing(value):
893 if isinstance(value, list):
894 return np.average(value).item()
895 return value
897 def get_better_subclass(self) -> Union[None, Type['IFCBased']]:
898 if len(self.ports) == 3:
899 return Junction
902class Junction(PipeFitting):
903 ifc_types = {
904 "IfcPipeFitting": ['JUNCTION']
905 }
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 ]
913 @property
914 def expected_hvac_ports(self):
915 return 3
917 volume = attribute.Attribute(
918 description="Volume of the junction",
919 unit=ureg.meter ** 3
920 )
923class SpaceHeater(HVACProduct):
924 ifc_types = {'IfcSpaceHeater': ['*', 'CONVECTOR', 'RADIATOR']}
926 pattern_ifc_type = [
927 re.compile('Heizk(ö|oe)rper', flags=re.IGNORECASE),
928 re.compile('Space.?heater', flags=re.IGNORECASE)
929 ]
931 @property
932 def expected_hvac_ports(self):
933 return 2
935 def is_consumer(self):
936 return True
938 number_of_panels = attribute.Attribute(
939 description="Number of panels of heater",
940 default_ps=('Pset_SpaceHeaterTypeCommon', 'NumberOfPanels'),
941 )
943 number_of_sections = attribute.Attribute(
944 description="Number of sections of heater",
945 default_ps=('Pset_SpaceHeaterTypeCommon', 'NumberOfSections'),
946 )
948 thermal_efficiency = attribute.Attribute(
949 description="Thermal efficiency of heater",
950 default_ps=('Pset_SpaceHeaterTypeCommon', 'ThermalEfficiency'),
951 unit=ureg.dimensionless,
952 )
954 body_mass = attribute.Attribute(
955 description="Body mass of heater",
956 default_ps=('Pset_SpaceHeaterTypeCommon', 'BodyMass'),
957 unit=ureg.kg,
958 )
960 length = attribute.Attribute(
961 description="Lenght of heater",
962 default_ps=('Qto_SpaceHeaterBaseQuantities', 'Length'),
963 unit=ureg.meter,
964 )
966 height = attribute.Attribute(
967 description="Height of heater",
968 unit=ureg.meter
969 )
971 temperature_classification = attribute.Attribute(
972 # [HighTemperature, LowTemperature, Other, NotKnown, Unset]
973 description="Temperature classification of heater",
974 default_ps=('Pset_SpaceHeaterTypeCommon', 'TemperatureClassification'),
975 )
977 rated_power = attribute.Attribute(
978 description="Rated power of SpaceHeater",
979 default_ps=('Pset_SpaceHeaterTypeCommon', 'OutputCapacity'),
980 unit=ureg.kilowatt,
981 )
983 flow_temperature = attribute.Attribute(
984 description="Flow temperature",
985 unit=ureg.celsius,
986 )
988 return_temperature = attribute.Attribute(
989 description="Return temperature",
990 unit=ureg.celsius,
991 )
993 medium = attribute.Attribute(
994 # [Steam, Water, Other, NotKnown, Unset]
995 description="Medium of SpaceHeater",
996 default_ps=('Pset_SpaceHeaterTypeCommon', 'HeatTransferMedium'),
997 )
999 heat_capacity = attribute.Attribute(
1000 description="Heat capacity of heater",
1001 default_ps=('Pset_SpaceHeaterTypeCommon', 'ThermalMassHeatCapacity'),
1002 unit=ureg.joule / ureg.kelvin,
1003 )
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
1010 dT_water = attribute.Attribute(
1011 description="Nominal temperature difference",
1012 unit=ureg.kelvin,
1013 functions=[_calc_dT_water],
1014 )
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 ]
1027 @property
1028 def expected_hvac_ports(self):
1029 return 1
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 ]
1045 conditions = [
1046 condition.RangeCondition('volume', 50 * ureg.liter,
1047 math.inf * ureg.liter)
1048 ]
1050 @property
1051 def expected_hvac_ports(self):
1052 return float('inf')
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
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 )
1067 height = attribute.Attribute(
1068 description="Height of the tank",
1069 default_ps=('Pset_TankTypeCommon', 'NominalDepth'),
1070 unit=ureg.meter
1071 )
1073 diameter = attribute.Attribute(
1074 description="Diameter of the tank",
1075 default_ps=('Pset_TankTypeCommon', 'NominalLengthOrDiameter'),
1076 unit=ureg.meter,
1077 )
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 )
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 )
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
1103 @property
1104 def expected_hvac_ports(self):
1105 return (2, float('inf'))
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 ]
1113 # volume = attribute.Attribute(
1114 # description="Volume of the Distributor",
1115 # unit=ureg.meter ** 3
1116 # )
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 )
1128class Pump(HVACProduct):
1129 ifc_types = {
1130 "IfcPump":
1131 ['*', 'CIRCULATOR', 'ENDSUCTION', 'SPLITCASE',
1132 'SUBMERSIBLEPUMP', 'SUMPPUMP', 'VERTICALINLINE',
1133 'VERTICALTURBINE']
1134 }
1136 @property
1137 def expected_hvac_ports(self):
1138 return 2
1140 pattern_ifc_type = [
1141 re.compile('Pumpe', flags=re.IGNORECASE),
1142 re.compile('Pump', flags=re.IGNORECASE)
1143 ]
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 )
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
1164 rated_power = attribute.Attribute(
1165 description="Rated power of pump",
1166 unit=ureg.kilowatt,
1167 functions=[_calc_rated_power],
1168 )
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 )
1177 rated_volume_flow = attribute.Attribute(
1178 description="Rated volume flow of pump",
1179 unit=ureg.m ** 3 / ureg.hour,
1180 )
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 )
1189 rated_height = attribute.Attribute(
1190 description="Rated height or rated pressure difference of pump",
1191 unit=ureg.meter,
1192 )
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 )
1200 diameter = attribute.Attribute(
1201 unit=ureg.meter,
1202 )
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 }
1216 @property
1217 def expected_hvac_ports(self):
1218 return 2
1220 # expected_hvac_ports = 2
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 ]
1228 conditions = [
1229 condition.RangeCondition("diameter", 5.0 * ureg.millimeter,
1230 500.00 * ureg.millimeter)
1231 ]
1233 nominal_pressure_difference = attribute.Attribute(
1234 description="Nominal pressure difference of valve",
1235 default_ps=('Pset_ValveTypeCommon', 'CloseOffRating'),
1236 unit=ureg.pascal,
1237 )
1239 kv_value = attribute.Attribute(
1240 description="kv_value of valve",
1241 default_ps=('Pset_ValveTypeCommon', 'FlowCoefficient'),
1242 )
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 )
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 )
1262 length = attribute.Attribute(
1263 description='Length of Valve',
1264 unit=ureg.meter,
1265 )
1267 nominal_mass_flow_rate = attribute.Attribute(
1268 description='Nominal mass flow rate of the valve',
1269 unit=ureg.kg / ureg.s,
1270 )
1273class ThreeWayValve(Valve):
1274 ifc_types = {
1275 "IfcValve":
1276 ['MIXING']
1277 }
1279 pattern_ifc_type = [
1280 re.compile('3-Wege.*?ventil', flags=re.IGNORECASE)
1281 ]
1283 @property
1284 def expected_hvac_ports(self):
1285 return 3
1288class Duct(HVACProduct):
1289 ifc_types = {"IfcDuctSegment": ['*', 'RIGIDSEGMENT', 'FLEXIBLESEGMENT']}
1291 pattern_ifc_type = [
1292 re.compile('Duct.?segment', flags=re.IGNORECASE)
1293 ]
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 )
1305class DuctFitting(HVACProduct):
1306 ifc_types = {
1307 "IfcDuctFitting":
1308 ['*', 'BEND', 'CONNECTOR', 'ENTRY', 'EXIT', 'JUNCTION',
1309 'OBSTRUCTION', 'TRANSITION']
1310 }
1312 pattern_ifc_type = [
1313 re.compile('Duct.?fitting', flags=re.IGNORECASE)
1314 ]
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 )
1326class AirTerminal(HVACProduct):
1327 ifc_types = {
1328 "IfcAirTerminal":
1329 ['*', 'DIFFUSER', 'GRILLE', 'LOUVRE', 'REGISTER']
1330 }
1332 pattern_ifc_type = [
1333 re.compile('Air.?terminal', flags=re.IGNORECASE)
1334 ]
1336 diameter = attribute.Attribute(
1337 description='Terminal diameter',
1338 unit=ureg.millimeter,
1339 )
1342class Medium(HVACProduct):
1343 # is deprecated?
1344 ifc_types = {"IfcDistributionSystem": ['*']}
1345 pattern_ifc_type = [
1346 re.compile('Medium', flags=re.IGNORECASE)
1347 ]
1349 @property
1350 def expected_hvac_ports(self):
1351 return 0
1354class CHP(HVACProduct):
1355 ifc_types = {'IfcElectricGenerator': ['CHP']}
1357 @property
1358 def expected_hvac_ports(self):
1359 return 2
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 )
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 )
1382 # water_volume = attribute.Attribute(
1383 # description="Water volume CHP chp",
1384 # unit=ureg.meter ** 3,
1385 # )
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)