From db0173ce0098b98c5b651f690d204f3780bf07e7 Mon Sep 17 00:00:00 2001 From: David Paul Jansen Date: Tue, 19 Nov 2024 16:32:19 +0100 Subject: [PATCH 1/3] add enums for flow_direction and flow_side and create new Task EnrichFlowDirection (not useful yet) --- bim2sim/elements/base_elements.py | 5 - bim2sim/elements/bps_elements.py | 8 - bim2sim/elements/graphs/hvac_graph.py | 102 ++--------- bim2sim/elements/hvac_elements.py | 158 +++++++++-------- .../PluginAixLib/bim2sim_aixlib/__init__.py | 1 + bim2sim/tasks/hvac/__init__.py | 1 + bim2sim/tasks/hvac/connect_elements.py | 40 ++++- bim2sim/tasks/hvac/enrich_flow_direction.py | 159 ++++++++++++++++++ bim2sim/tasks/hvac/reduce.py | 60 ------- bim2sim/utilities/types.py | 14 ++ docs/source/user-guide/PluginAixLib.md | 21 +++ 11 files changed, 333 insertions(+), 236 deletions(-) create mode 100644 bim2sim/tasks/hvac/enrich_flow_direction.py diff --git a/bim2sim/elements/base_elements.py b/bim2sim/elements/base_elements.py index 632baad6f0..e032e0e90b 100644 --- a/bim2sim/elements/base_elements.py +++ b/bim2sim/elements/base_elements.py @@ -206,11 +206,6 @@ def ifc_type(self): if self.ifc: return self.ifc.is_a() - @classmethod - def pre_validate(cls, ifc) -> bool: - """Check if ifc meets conditions to create element from it""" - raise NotImplementedError - def calc_position(self): """returns absolute position""" if hasattr(self.ifc, 'ObjectPlacement'): diff --git a/bim2sim/elements/bps_elements.py b/bim2sim/elements/bps_elements.py index 7da8f98024..5c8402aa81 100644 --- a/bim2sim/elements/bps_elements.py +++ b/bim2sim/elements/bps_elements.py @@ -617,10 +617,6 @@ def calc_position(self): return position - @classmethod - def pre_validate(cls, ifc) -> bool: - return True - def validate_creation(self) -> bool: if self.bound_area and self.bound_area < 1e-2 * ureg.meter ** 2: return True @@ -1322,10 +1318,6 @@ def get_id(prefix=""): ifcopenshell_guid = guid.new()[prefix_length + 1:] return f"{prefix}{ifcopenshell_guid}" - @classmethod - def pre_validate(cls, ifc) -> bool: - return True - def validate_creation(self) -> bool: return True diff --git a/bim2sim/elements/graphs/hvac_graph.py b/bim2sim/elements/graphs/hvac_graph.py index 62be7e0e71..38f85733f7 100644 --- a/bim2sim/elements/graphs/hvac_graph.py +++ b/bim2sim/elements/graphs/hvac_graph.py @@ -15,6 +15,7 @@ from networkx import json_graph from bim2sim.elements.base_elements import ProductBased, ElementEncoder +from bim2sim.utilities.types import FlowSide, FlowDirection logger = logging.getLogger(__name__) @@ -118,7 +119,7 @@ def get_type_chains( types: Iterable[Type[ProductBased]], include_singles: bool = False): """Get lists of consecutive elements of the given types. Elements are - ordered in the same way as the are connected. + ordered in the same way as they are connected. Args: element_graph: Graph object with elements as nodes. @@ -203,16 +204,15 @@ def plot(self, path: Path = None, ports: bool = False, dpi: int = 400, # https://plot.ly/python/network-graphs/ edge_colors_flow_side = { - 1: dict(edge_color='red'), - -1: dict(edge_color='blue'), - 0: dict(edge_color='grey'), - None: dict(edge_color='grey'), + FlowSide.supply_flow: dict(edge_color='red'), + FlowSide.return_flow: dict(edge_color='blue'), + FlowSide.unknown: dict(edge_color='grey'), } node_colors_flow_direction = { - 1: dict(node_color='white', edgecolors='blue'), - -1: dict(node_color='blue', edgecolors='black'), - 0: dict(node_color='grey', edgecolors='black'), - None: dict(node_color='grey', edgecolors='black'), + FlowDirection.source: dict(node_color='white', edgecolors='blue'), + FlowDirection.sink: dict(node_color='blue', edgecolors='black'), + FlowDirection.sink_and_source: dict(node_color='grey', edgecolors='black'), + FlowDirection.unknown: dict(node_color='grey', edgecolors='black'), } kwargs = {} @@ -293,6 +293,7 @@ def plot(self, path: Path = None, ports: bool = False, dpi: int = 400, node['label'] = node['label'].split('<')[1] except: pass + # TODO #633 use is_generator(), is_consumer() etc. node['label'] = node['label'].split('(ports')[0] if 'agg' in node['label'].lower(): node['label'] = node['label'].split('Agg0')[0] @@ -654,88 +655,7 @@ def group_parallels(graph, group_attr, cond, threshold=None): graphs.append(_graph) return graphs - def recurse_set_side(self, port, side, known: dict = None, - raise_error=True): - """Recursive set flow_side to connected ports""" - if known is None: - known = {} - - # set side suggestion - is_known = port in known - current_side = known.get(port, port.flow_side) - if not is_known: - known[port] = side - elif is_known and current_side == side: - return known - else: - # conflict - if raise_error: - raise AssertionError("Conflicting flow_side in %r" % port) - else: - logger.error("Conflicting flow_side in %r", port) - known[port] = None - return known - - # call neighbours - for neigh in self.neighbors(port): - if (neigh.parent.is_consumer() or neigh.parent.is_generator()) \ - and port.parent is neigh.parent: - # switch flag over consumers / generators - self.recurse_set_side(neigh, -side, known, raise_error) - else: - self.recurse_set_side(neigh, side, known, raise_error) - - return known - - def recurse_set_unknown_sides(self, port, visited: list = None, - masters: list = None): - """Recursive checks neighbours flow_side. - :returns tuple of - common flow_side (None if conflict) - list of checked ports - list of ports on which flow_side s are determined""" - - if visited is None: - visited = [] - if masters is None: - masters = [] - - # mark as visited to prevent deadloops - visited.append(port) - - if port.flow_side in (-1, 1): - # use port with known flow_side as master - masters.append(port) - return port.flow_side, visited, masters - - # call neighbours - neighbour_sides = {} - for neigh in self.neighbors(port): - if neigh not in visited: - if (neigh.parent.is_consumer() or neigh.parent.is_generator()) \ - and port.parent is neigh.parent: - # switch flag over consumers / generators - side, _, _ = self.recurse_set_unknown_sides( - neigh, visited, masters) - side = -side - else: - side, _, _ = self.recurse_set_unknown_sides( - neigh, visited, masters) - neighbour_sides[neigh] = side - - sides = set(neighbour_sides.values()) - if not sides: - return port.flow_side, visited, masters - elif len(sides) == 1: - # all neighbours have same site - side = sides.pop() - return side, visited, masters - elif len(sides) == 2 and 0 in sides: - side = (sides - {0}).pop() - return side, visited, masters - else: - # conflict - return None, visited, masters + @staticmethod def get_dir_paths_between(graph, nodes, include_edges=False): diff --git a/bim2sim/elements/hvac_elements.py b/bim2sim/elements/hvac_elements.py index 278cf011bc..1254407613 100644 --- a/bim2sim/elements/hvac_elements.py +++ b/bim2sim/elements/hvac_elements.py @@ -16,6 +16,8 @@ from bim2sim.elements.mapping.ifc2python import get_ports as ifc2py_get_ports from bim2sim.elements.mapping.ifc2python import get_predefined_type from bim2sim.elements.mapping.units import ureg +from bim2sim.utilities.types import FlowDirection, FlowSide + logger = logging.getLogger(__name__) quality_logger = logging.getLogger('bim2sim.QualityReport') @@ -34,19 +36,31 @@ def length_post_processing(value): class HVACPort(Port): - """Port of HVACProduct.""" - vl_pattern = re.compile('.*vorlauf.*', - re.IGNORECASE) # TODO: extend pattern - rl_pattern = re.compile('.*rücklauf.*', re.IGNORECASE) + """Port of HVACProduct. + + Definitions: + flow_direction: is the direction of the port which can be sink, source, + sink_and_source or unknown depending on the IFC data. + groups: based on IFC assignment this might be "vorlauf" or something else. + flow_side: defines if the port is part of the supply or return network. + E.g. the radiator is a splitter where one port is part of the supply and + the other port is part of the return network + + """ + vl_pattern = re.compile('.*(vorlauf|supply|feed|forward).*', re.IGNORECASE) + rl_pattern = re.compile('.*(rücklauf|return|recirculation|back).*', + re.IGNORECASE) def __init__( self, *args, groups: Set = None, - flow_direction: int = 0, **kwargs): + flow_direction: FlowDirection = FlowDirection.unknown, **kwargs): super().__init__(*args, **kwargs) self._flow_master = False - self._flow_direction = None + # self._flow_direction = None + self._flow_side = None + # groups and flow_direction coming from ifc2args kwargs self.groups = groups or set() self.flow_direction = flow_direction @@ -55,13 +69,16 @@ def ifc2args(cls, ifc) -> Tuple[tuple, dict]: args, kwargs = super().ifc2args(ifc) groups = {assg.RelatingGroup.ObjectType for assg in ifc.HasAssignments} - flow_direction = None if ifc.FlowDirection == 'SOURCE': - flow_direction = 1 + flow_direction = FlowDirection.source elif ifc.FlowDirection == 'SINK': - flow_direction = -1 + flow_direction = FlowDirection.sink elif ifc.FlowDirection in ['SINKANDSOURCE', 'SOURCEANDSINK']: - flow_direction = 0 + flow_direction = FlowDirection.sink_and_source + elif ifc.FlowDirection == 'NOTDEFINED': + flow_direction = FlowDirection.unknown + else: + flow_direction = FlowDirection.unknown kwargs['groups'] = groups kwargs['flow_direction'] = flow_direction @@ -90,10 +107,6 @@ def calc_position(self) -> np.array: quality_logger.info("Suspect position [0, 0, 0] for %s", self) return coordinates - @classmethod - def pre_validate(cls, ifc) -> bool: - return True - def validate_creation(self) -> bool: return True @@ -106,34 +119,34 @@ def flow_master(self): def flow_master(self, value: bool): self._flow_master = value - @property - def flow_direction(self): - """Flow direction of port - - -1 = medium flows into port - 1 = medium flows out of port - 0 = medium flow undirected - None = flow direction unknown""" - return self._flow_direction - - @flow_direction.setter - def flow_direction(self, value): - if self._flow_master: - raise AttributeError("Can't set flow direction for flow master.") - if value not in (-1, 0, 1, None): - raise AttributeError("Invalid value. Use one of (-1, 0, 1, None).") - self._flow_direction = value - - @property - def verbose_flow_direction(self): - """Flow direction of port""" - if self.flow_direction == -1: - return 'SINK' - if self.flow_direction == 0: - return 'SINKANDSOURCE' - if self.flow_direction == 1: - return 'SOURCE' - return 'UNKNOWN' + # @property + # def flow_direction(self): + # """Flow direction of port + # + # -1 = medium flows into port + # 1 = medium flows out of port + # 0 = medium flow undirected + # None = flow direction unknown""" + # return self._flow_direction + + # @flow_direction.setter + # def flow_direction(self, value): + # if self._flow_master: + # raise AttributeError("Can't set flow direction for flow master.") + # if value not in (-1, 0, 1, None): + # raise AttributeError("Invalid value. Use one of (-1, 0, 1, None).") + # self._flow_direction = value + + # @property + # def verbose_flow_direction(self): + # """Flow direction of port""" + # if self.flow_direction == -1: + # return 'SINK' + # if self.flow_direction == 0: + # return 'SINKANDSOURCE' + # if self.flow_direction == 1: + # return 'SOURCE' + # return 'UNKNOWN' @property def flow_side(self): @@ -150,39 +163,39 @@ def flow_side(self): @flow_side.setter def flow_side(self, value): - if value not in (-1, 0, 1): - raise ValueError("allowed values for flow_side are 1, 0, -1") previous = self._flow_side self._flow_side = value if previous: if previous != value: - logger.info("Overwriting flow_side for %r with %s" % ( - self, self.verbose_flow_side)) + logger.info( + f"Overwriting flow_side for {self} with {value.name}") else: - logger.debug( - "Set flow_side for %r to %s" % (self, self.verbose_flow_side)) - - @property - def verbose_flow_side(self): - if self.flow_side == 1: - return "VL" - if self.flow_side == -1: - return "RL" - return "UNKNOWN" + logger.debug(f"Set flow_side for {self} to {value.name}") def determine_flow_side(self): - """Check groups for hints of flow_side and returns flow_side if hints are definitely""" + """Check groups for hints of flow_side and returns flow_side if hints + are definitely. + + First the flow_direction and the type of the element + (generator/consumer) is checked for clear information. If no + information can be obtained the pattern matches are evaluated based on + the groups from IFC, that come from RelatingGroup assignment. + If there are mismatching information from flow_direction and patterns + the flow_side is set to unknown, otherwise it's set to supply_flow or + supply_flow. + """ vl = None rl = None + if self.parent.is_generator(): - if self.flow_direction == 1: + if self.flow_direction.name == "source": vl = True - elif self.flow_direction == -1: + elif self.flow_direction.name == "sink": rl = True elif self.parent.is_consumer(): - if self.flow_direction == 1: + if self.flow_direction.name == "source": rl = True - elif self.flow_direction == -1: + elif self.flow_direction.name == "sink": vl = True if not vl: vl = any(filter(self.vl_pattern.match, self.groups)) @@ -190,10 +203,10 @@ def determine_flow_side(self): rl = any(filter(self.rl_pattern.match, self.groups)) if vl and not rl: - return 1 + return FlowSide.supply_flow if rl and not vl: - return -1 - return 0 + return FlowSide.return_flow + return FlowSide.unknown class HVACProduct(ProductBased): @@ -312,8 +325,8 @@ def decide_inner_connections(self) -> Generator[DecisionBunch, None, None]: vl = port_dict[decision_vl.value] rl = port_dict[decision_rl.value] # set flow correct side - vl.flow_side = 1 - rl.flow_side = -1 + vl.flow_side = FlowSide.supply_flow + rl.flow_side = FlowSide.return_flow self.inner_connections.append((vl, rl)) def validate_ports(self): @@ -356,6 +369,9 @@ class HeatPump(HVACProduct): re.compile('W(ä|ae)rme.?pumpe', flags=re.IGNORECASE), ] + def is_generator(self): + return True + min_power = attribute.Attribute( description='Minimum power that HeatPump operates at.', unit=ureg.kilowatt, @@ -461,6 +477,10 @@ class CoolingTower(HVACProduct): re.compile('RKA', flags=re.IGNORECASE), ] + def is_consumer(self): + # TODO #733 check this + return True + min_power = attribute.Attribute( description='Minimum power that CoolingTower operates at.', unit=ureg.kilowatt, @@ -1293,6 +1313,9 @@ class AirTerminal(HVACProduct): unit=ureg.millimeter, ) + def is_consumer(self): + return True + class Medium(HVACProduct): # is deprecated? @@ -1313,6 +1336,9 @@ class CHP(HVACProduct): def expected_hvac_ports(self): return 2 + def is_generator(self): + return True + rated_power = attribute.Attribute( default_ps=('Pset_ElectricGeneratorTypeCommon', 'MaximumPowerOutput'), description="Rated power of CHP", diff --git a/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/__init__.py b/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/__init__.py index ac1767d968..6fde532b61 100644 --- a/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/__init__.py +++ b/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/__init__.py @@ -30,6 +30,7 @@ class PluginAixLib(Plugin): common.CreateElementsOnIfcTypes, hvac.ConnectElements, hvac.MakeGraph, + hvac.EnrichFlowDirection, hvac.ExpansionTanks, hvac.Reduce, hvac.DeadEnds, diff --git a/bim2sim/tasks/hvac/__init__.py b/bim2sim/tasks/hvac/__init__.py index d52f5c87ba..d3dbed899b 100644 --- a/bim2sim/tasks/hvac/__init__.py +++ b/bim2sim/tasks/hvac/__init__.py @@ -7,3 +7,4 @@ from .export import Export from .connect_elements import ConnectElements from .load_standardlibrary import LoadLibrariesStandardLibrary +from .enrich_flow_direction import EnrichFlowDirection diff --git a/bim2sim/tasks/hvac/connect_elements.py b/bim2sim/tasks/hvac/connect_elements.py index 0d48cf27af..961d5498b7 100644 --- a/bim2sim/tasks/hvac/connect_elements.py +++ b/bim2sim/tasks/hvac/connect_elements.py @@ -48,7 +48,7 @@ def run(self, elements: dict) -> dict: # Check ports self.logger.info("Checking ports of elements ...") - self.check_element_ports(elements) + self.remove_duplicate_ports(elements) # Make connections by relations self.logger.info("Connecting the relevant elements") self.logger.info(" - Connecting by relations ...") @@ -105,11 +105,38 @@ def run(self, elements: dict) -> dict: return elements, @staticmethod - def check_element_ports(elements: dict): - """Checks position of all ports for each element. + def remove_duplicate_ports(elements: dict): + """Checks position of all ports for each element and handles + overlapping ports. + + This method analyzes port positions within building elements + (e.g. pipes, fittings) and identifies overlapping ports that may + indicate data quality issues. When two ports of the same element + overlap and both connect to the same third port, they are merged into + a single bidirectional port. Args: - elements: dictionary of elements to be checked with GUID as key. + elements: Dictionary mapping GUIDs to element objects that should + be checked. + Each element must have a 'ports' attribute containing its + ports. + + Quality Checks: + - Warns if ports of the same element are closer than 1 unit + (atol=1) + - Identifies overlapping ports using numpy.allclose with rtol=1e-7 + + Port Merging: + If overlapping ports A and B are found that both connect to the + same port C: + - Port B is removed from the element + - Port A is set as bidirectional (SINKANDSOURCE) + - Port A becomes the flow master + - The change is logged for documentation + + WARNING: Poor quality of elements : Overlapping ports (port1 + and port2 @[x,y,z]) + INFO: Removing and set as SINKANDSOURCE. """ for ele in elements.values(): for port_a, port_b in itertools.combinations(ele.ports, 2): @@ -127,13 +154,14 @@ def check_element_ports(elements: dict): port not in [port_a, port_b]] if port_a in all_ports and port_b in all_ports and len( set(other_ports)) == 1: - # Both ports connected to same other port -> merge ports + # Both ports connected to same other port -> + # merge ports quality_logger.info( "Removing %s and set %s as SINKANDSOURCE.", port_b.ifc, port_a.ifc) ele.ports.remove(port_b) port_b.parent = None - port_a.flow_direction = 0 + port_a.flow_direction.value = 0 port_a.flow_master = True @staticmethod diff --git a/bim2sim/tasks/hvac/enrich_flow_direction.py b/bim2sim/tasks/hvac/enrich_flow_direction.py new file mode 100644 index 0000000000..6dbc5cd3f8 --- /dev/null +++ b/bim2sim/tasks/hvac/enrich_flow_direction.py @@ -0,0 +1,159 @@ +from bim2sim.elements.graphs.hvac_graph import HvacGraph +from bim2sim.tasks.base import ITask +import logging +from bim2sim.kernel.decision import BoolDecision, DecisionBunch + +logger = logging.getLogger(__name__) + + +class EnrichFlowDirection(ITask): + + reads = ('graph', ) + + def run(self, graph: HvacGraph): + self.set_flow_sides(graph) + + # Continue here #633 + def set_flow_sides(self, graph: HvacGraph): + """ Set flow sides for ports in HVAC graph based on known flow sides. + + This function iteratively sets flow sides for ports in the HVAC graph. + It uses a recursive method (`recurse_set_unknown_sides`) to determine + the flow side for each unset port. The function may prompt the user + for decisions in case of conflicts or unknown sides. + + Args: + graph: The HVAC graph. + + Yields: + DecisionBunch: A collection of decisions may be yielded during the + task. + """ + # TODO: needs testing! + # TODO: at least one master element required + accepted = [] + while True: + unset_port = None + for port in graph.get_nodes(): + if port.flow_side == 0 and graph.graph[port] \ + and port not in accepted: + unset_port = port + break + if unset_port: + side, visited, masters = self.recurse_set_unknown_sides( + unset_port) + if side in (-1, 1): + # apply suggestions + for port in visited: + port.flow_side = side + elif side == 0: + # TODO: ask user? + accepted.extend(visited) + elif masters: + # ask user to fix conflicts (and retry in next while loop) + for port in masters: + decision = BoolDecision( + "Use %r as VL (y) or RL (n)?" % port, + global_key= "Use_port_%s" % port.guid) + yield DecisionBunch([decision]) + use = decision.value + if use: + port.flow_side = 1 + else: + port.flow_side = -1 + else: + # can not be solved (no conflicting masters) + # TODO: ask user? + accepted.extend(visited) + else: + # done + logging.info("Flow_side set") + break + print('Test') + + # TODO not used yet + def recurse_set_side(self, port, side, known: dict = None, + raise_error=True): + """Recursive set flow_side to connected ports""" + if known is None: + known = {} + + # set side suggestion + is_known = port in known + current_side = known.get(port, port.flow_side) + if not is_known: + known[port] = side + elif is_known and current_side == side: + return known + else: + # conflict + if raise_error: + raise AssertionError("Conflicting flow_side in %r" % port) + else: + logger.error("Conflicting flow_side in %r", port) + known[port] = None + return known + + # call neighbours + for neigh in self.neighbors(port): + if (neigh.parent.is_consumer() or neigh.parent.is_generator()) \ + and port.parent is neigh.parent: + # switch flag over consumers / generators + self.recurse_set_side(neigh, -side, known, raise_error) + else: + self.recurse_set_side(neigh, side, known, raise_error) + + return known + + def recurse_set_unknown_sides(self, port, visited: list = None, + masters: list = None): + """Recursive checks neighbours flow_side. + :returns tuple of + common flow_side (None if conflict) + list of checked ports + list of ports on which flow_side s are determined""" + + if visited is None: + visited = [] + if masters is None: + masters = [] + + # mark as visited to prevent deadloops + visited.append(port) + + if port.flow_side in (-1, 1): + # use port with known flow_side as master + masters.append(port) + return port.flow_side, visited, masters + + # call neighbours + neighbour_sides = {} + for neigh in self.neighbors(port): + if neigh not in visited: + if (neigh.parent.is_consumer() or neigh.parent.is_generator()) \ + and port.parent is neigh.parent: + # switch flag over consumers / generators + side, _, _ = self.recurse_set_unknown_sides( + neigh, visited, masters) + side = -side + else: + side, _, _ = self.recurse_set_unknown_sides( + neigh, visited, masters) + neighbour_sides[neigh] = side + + sides = set(neighbour_sides.values()) + if not sides: + return port.flow_side, visited, masters + elif len(sides) == 1: + # all neighbours have same site + side = sides.pop() + return side, visited, masters + elif len(sides) == 2 and 0 in sides: + side = (sides - {0}).pop() + return side, visited, masters + else: + # conflict + return None, visited, masters + + + diff --git a/bim2sim/tasks/hvac/reduce.py b/bim2sim/tasks/hvac/reduce.py index 5f6bfe141b..c2b6267f2e 100644 --- a/bim2sim/tasks/hvac/reduce.py +++ b/bim2sim/tasks/hvac/reduce.py @@ -1,10 +1,7 @@ -import logging - from bim2sim.elements.aggregation.hvac_aggregations import UnderfloorHeating, \ Consumer, PipeStrand, ParallelPump, ConsumerHeatingDistributorModule, \ GeneratorOneFluid from bim2sim.elements.graphs.hvac_graph import HvacGraph -from bim2sim.kernel.decision import BoolDecision, DecisionBunch from bim2sim.tasks.base import ITask @@ -85,60 +82,3 @@ def run(self, graph: HvacGraph) -> (HvacGraph,): graph.plot(self.paths.export, ports=False, use_pyvis=True) return graph, - - @staticmethod - def set_flow_sides(graph: HvacGraph): - """ Set flow sides for ports in HVAC graph based on known flow sides. - - This function iteratively sets flow sides for ports in the HVAC graph. - It uses a recursive method (`recurse_set_unknown_sides`) to determine - the flow side for each unset port. The function may prompt the user - for decisions in case of conflicts or unknown sides. - - Args: - graph: The HVAC graph. - - Yields: - DecisionBunch: A collection of decisions may be yielded during the - task. - """ - # TODO: needs testing! - # TODO: at least one master element required - accepted = [] - while True: - unset_port = None - for port in graph.get_nodes(): - if port.flow_side == 0 and graph.graph[port] \ - and port not in accepted: - unset_port = port - break - if unset_port: - side, visited, masters = graph.recurse_set_unknown_sides( - unset_port) - if side in (-1, 1): - # apply suggestions - for port in visited: - port.flow_side = side - elif side == 0: - # TODO: ask user? - accepted.extend(visited) - elif masters: - # ask user to fix conflicts (and retry in next while loop) - for port in masters: - decision = BoolDecision( - "Use %r as VL (y) or RL (n)?" % port, - global_key= "Use_port_%s" % port.guid) - yield DecisionBunch([decision]) - use = decision.value - if use: - port.flow_side = 1 - else: - port.flow_side = -1 - else: - # can not be solved (no conflicting masters) - # TODO: ask user? - accepted.extend(visited) - else: - # done - logging.info("Flow_side set") - break diff --git a/bim2sim/utilities/types.py b/bim2sim/utilities/types.py index 4cd2f08926..609db7ab8b 100644 --- a/bim2sim/utilities/types.py +++ b/bim2sim/utilities/types.py @@ -65,3 +65,17 @@ class AttributeDataSource(Enum): manual_overwrite = auto() enrichment = auto() space_boundary = auto() + + +class FlowDirection(Enum): + """Used to describe the flow direction of ports.""" + sink_and_source = 0 + sink = -1 + source = 1 + unknown = 2 + + +class FlowSide(Enum): + supply_flow = 1 + return_flow = -1 + unknown = 0 diff --git a/docs/source/user-guide/PluginAixLib.md b/docs/source/user-guide/PluginAixLib.md index ec68a868b4..939292a507 100644 --- a/docs/source/user-guide/PluginAixLib.md +++ b/docs/source/user-guide/PluginAixLib.md @@ -205,6 +205,24 @@ taskDeadEnds --> taskLoadLibrariesAixLib taskLoadLibrariesAixLib --> taskExport ``` +### Port handling +#### Port creation +1. Ports get created during product creation as they are relation based +2. `ProductBased` base class has `get_ports()` function that is overwritten in `HVACProduct` +3. Ports are stored under self.ports in every HVACElement +4. IFC offers [according to schema](https://standards.buildingsmart.org/IFC/RELEASE/IFC4/ADD2_TC1/HTML/schema/ifcsharedbldgserviceelements/lexical/ifcflowdirectionenum.htm) the `FlowDirection` enumeration, which can be either "SOURCE", "SINK", or "SOURCEANDSINK" but some IFC files also hold "SINKANDSOURCE". +5. Groups and port `flow_directions` are assigned via `HVACPort.ifc2args()` method +6. `flow_side` is assigned via `determine_flow_side()` function that uses patters matches for the names of "vorlauf" "rücklauf" etc. and the port `flow_direction` + +#### Port Connection +Task: `ConnectElements` +`check_element_ports()` +`connections_by_relation()` +`set_flow_sides()` +`recurse_set_side()` +`recurse_set_unknown_sides()` + + This figure is generated by the script template_mermaid.py (see [Visualization of bim2sim plugin structure](genVisPlugins)). ## How to create a project? @@ -229,3 +247,6 @@ This figure is generated by the script template_mermaid.py (see [Visualization o ### What kind of results exist? ### What programs/tools to use for further analysis? + + +# \ No newline at end of file From 5b5898593ba46be90ac2737b62580dce66e46fb2 Mon Sep 17 00:00:00 2001 From: David Paul Jansen Date: Tue, 26 Nov 2024 14:16:51 +0100 Subject: [PATCH 2/3] start adjust of flow side enrichment --- bim2sim/elements/graphs/hvac_graph.py | 5 +---- bim2sim/tasks/hvac/enrich_flow_direction.py | 13 ++++++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bim2sim/elements/graphs/hvac_graph.py b/bim2sim/elements/graphs/hvac_graph.py index 38f85733f7..f002b89734 100644 --- a/bim2sim/elements/graphs/hvac_graph.py +++ b/bim2sim/elements/graphs/hvac_graph.py @@ -182,9 +182,6 @@ def get_connections(self): return [edge for edge in self.edges if not edge[0].parent is edge[1].parent] - # def get_nodes(self): - # """Returns list of nodes represented by graph""" - # return list(self.nodes) def plot(self, path: Path = None, ports: bool = False, dpi: int = 400, use_pyvis=False): @@ -293,7 +290,7 @@ def plot(self, path: Path = None, ports: bool = False, dpi: int = 400, node['label'] = node['label'].split('<')[1] except: pass - # TODO #633 use is_generator(), is_consumer() etc. + # TODO #733 use is_generator(), is_consumer() etc. node['label'] = node['label'].split('(ports')[0] if 'agg' in node['label'].lower(): node['label'] = node['label'].split('Agg0')[0] diff --git a/bim2sim/tasks/hvac/enrich_flow_direction.py b/bim2sim/tasks/hvac/enrich_flow_direction.py index 6dbc5cd3f8..6676fed1a0 100644 --- a/bim2sim/tasks/hvac/enrich_flow_direction.py +++ b/bim2sim/tasks/hvac/enrich_flow_direction.py @@ -2,6 +2,8 @@ from bim2sim.tasks.base import ITask import logging from bim2sim.kernel.decision import BoolDecision, DecisionBunch +from bim2sim.utilities.types import FlowSide + logger = logging.getLogger(__name__) @@ -11,9 +13,10 @@ class EnrichFlowDirection(ITask): reads = ('graph', ) def run(self, graph: HvacGraph): - self.set_flow_sides(graph) + yield from self.set_flow_sides(graph) + print('test') - # Continue here #633 + #Todo Continue here #733 def set_flow_sides(self, graph: HvacGraph): """ Set flow sides for ports in HVAC graph based on known flow sides. @@ -31,11 +34,12 @@ def set_flow_sides(self, graph: HvacGraph): """ # TODO: needs testing! # TODO: at least one master element required + print('test') accepted = [] while True: unset_port = None - for port in graph.get_nodes(): - if port.flow_side == 0 and graph.graph[port] \ + for port in list(graph.nodes): + if port.flow_side == FlowSide.unknown and graph.graph[port] \ and port not in accepted: unset_port = port break @@ -69,7 +73,6 @@ def set_flow_sides(self, graph: HvacGraph): # done logging.info("Flow_side set") break - print('Test') # TODO not used yet def recurse_set_side(self, port, side, known: dict = None, From e5be962d4c782f03fda438523be0a890d0bb1958 Mon Sep 17 00:00:00 2001 From: Hoepp J <155448650+HoeppJ@users.noreply.github.com> Date: Thu, 26 Jun 2025 15:52:11 +0200 Subject: [PATCH 3/3] bugfix --- .../PluginAixLib/bim2sim_aixlib/models/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/models/__init__.py b/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/models/__init__.py index 15a1690a2b..0881e34584 100644 --- a/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/models/__init__.py +++ b/bim2sim/plugins/PluginAixLib/bim2sim_aixlib/models/__init__.py @@ -283,9 +283,9 @@ def __init__(self, value=10000 * n_consumers) def get_port_name(self, port): - if port.verbose_flow_direction == 'SINK': + if port.flow_direction.name == 'sink': return 'port_a' - if port.verbose_flow_direction == 'SOURCE': + if port.flow_direction.name == 'source': return 'port_b' else: return super().get_port_name(port) @@ -340,9 +340,9 @@ def __init__(self, element): value=10000) def get_port_name(self, port): - if port.verbose_flow_direction == 'SINK': + if port.flow_direction.name == 'sink': return 'port_a' - if port.verbose_flow_direction == 'SOURCE': + if port.flow_direction.name == 'source': return 'port_b' else: return super().get_port_name(port)