Skip to main content

ifc_systems/connectivity/
traversal.rs

1//! Deterministic upstream/downstream queries, without geometry.
2//!
3//! # Why this needs flow, and why it is separate from `NetworkGraph`
4//!
5//! `ConnectionGraph` and `NetworkGraph` are UNDIRECTED: they answer "what is
6//! joined to what". That cannot answer "what does this pump feed", because
7//! reachability alone walks backwards through the supply as happily as
8//! forwards.
9//!
10//! Direction comes from `IfcDistributionPort.FlowDirection`, which is stated
11//! per port, not per connection. The orientation rules are:
12//!
13//! - Leaving an element through a `SINK` port is not flow. A sink is where
14//!   material ENTERS the element, so material moves INTO it there.
15//! - Arriving at a `SOURCE` port is not flow, for the mirror reason.
16//! - `SOURCEANDSINK` is bidirectional by definition, so both are allowed.
17//! - `NOTDEFINED`, or an absent attribute, states nothing. Those edges are
18//!   traversable in BOTH directions, and every query that used one says so.
19//!
20//! That last rule is the important one. Silently treating unstated direction
21//! as "no flow" would make queries on the very common under-specified file
22//! return empty and look authoritative. Treating it as bidirectional keeps
23//! the answer complete, and `used_undirected` tells the caller the result
24//! rests on an assumption the file did not make.
25
26use std::collections::{BTreeMap, BTreeSet, VecDeque};
27
28use ifc_model::EntityId;
29
30use crate::connectivity::ConnectionGraph;
31use crate::flow::FlowDirection;
32use crate::port::Port;
33
34/// Which way a query walks.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum Direction {
37    /// Follow flow: what this element feeds.
38    Downstream,
39    /// Walk against flow: what feeds this element.
40    Upstream,
41}
42
43/// The answer to a directed query.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct FlowQuery {
46    /// Elements reached, ascending by id. Excludes the starting element.
47    ///
48    /// Ascending rather than discovery order: BFS order depends on adjacency
49    /// insertion, so two files stating the same network could disagree. A
50    /// caller wanting distance should ask for it explicitly.
51    pub elements: Vec<EntityId>,
52    /// Whether any traversed edge had unstated direction.
53    ///
54    /// `true` means the result depends on treating an unstated port as
55    /// bidirectional. The elements are still correct as a superset, but the
56    /// file did not actually license all of them.
57    pub used_undirected: bool,
58}
59
60/// A network oriented by flow direction.
61///
62/// Built from a connection graph plus ports, because direction lives on the
63/// port and connectivity lives on the relationship. Neither alone is enough.
64pub struct FlowNetwork {
65    /// port -> owning element.
66    owner: BTreeMap<EntityId, EntityId>,
67    /// element -> its ports.
68    owned: BTreeMap<EntityId, Vec<EntityId>>,
69    /// port -> its stated direction.
70    direction: BTreeMap<EntityId, FlowDirection>,
71    /// Undirected port-to-port adjacency, from the connection graph.
72    adjacency: BTreeMap<EntityId, BTreeSet<EntityId>>,
73}
74
75impl FlowNetwork {
76    /// Orient a connection graph using the ports' flow directions.
77    pub fn build(graph: &ConnectionGraph, ports: &[Port]) -> Self {
78        let mut owner = BTreeMap::new();
79        let mut owned: BTreeMap<EntityId, Vec<EntityId>> = BTreeMap::new();
80        let mut direction = BTreeMap::new();
81        for port in ports {
82            direction.insert(port.id, port.flow);
83            if let Some(element) = port.element {
84                owner.insert(port.id, element);
85                owned.entry(element).or_default().push(port.id);
86            }
87        }
88        let mut adjacency: BTreeMap<EntityId, BTreeSet<EntityId>> = BTreeMap::new();
89        for connection in graph.connections() {
90            // relating/related is authoring order, not flow: both directions
91            // go in, and orientation comes solely from port FlowDirection.
92            adjacency
93                .entry(connection.relating)
94                .or_default()
95                .insert(connection.related);
96            adjacency
97                .entry(connection.related)
98                .or_default()
99                .insert(connection.relating);
100        }
101        Self {
102            owner,
103            owned,
104            direction,
105            adjacency,
106        }
107    }
108
109    fn stated(&self, port: EntityId) -> FlowDirection {
110        self.direction
111            .get(&port)
112            .copied()
113            .unwrap_or(FlowDirection::NotDefined)
114    }
115
116    /// May material leave an element through this port, walking `direction`?
117    ///
118    /// Returns `(allowed, stated)`. `stated` is false when the port declares
119    /// nothing, which the caller records so the answer stays honest.
120    fn may_exit(&self, port: EntityId, direction: Direction) -> (bool, bool) {
121        match (self.stated(port), direction) {
122            // Exiting through a source is flow; through a sink it is not.
123            (FlowDirection::Source, Direction::Downstream) => (true, true),
124            (FlowDirection::Sink, Direction::Downstream) => (false, true),
125            // Walking upstream inverts the test.
126            (FlowDirection::Sink, Direction::Upstream) => (true, true),
127            (FlowDirection::Source, Direction::Upstream) => (false, true),
128            (FlowDirection::SourceAndSink, _) => (true, true),
129            // Unstated: allowed, but flagged.
130            (FlowDirection::NotDefined, _) => (true, false),
131        }
132    }
133
134    /// May material enter an element through this port, walking `direction`?
135    fn may_enter(&self, port: EntityId, direction: Direction) -> (bool, bool) {
136        // Entering is the mirror of exiting, so reuse the rule inverted
137        // rather than restating it and risking the two drifting apart.
138        match (self.stated(port), direction) {
139            (FlowDirection::Sink, Direction::Downstream) => (true, true),
140            (FlowDirection::Source, Direction::Downstream) => (false, true),
141            (FlowDirection::Source, Direction::Upstream) => (true, true),
142            (FlowDirection::Sink, Direction::Upstream) => (false, true),
143            (FlowDirection::SourceAndSink, _) => (true, true),
144            (FlowDirection::NotDefined, _) => (true, false),
145        }
146    }
147
148    /// Elements reachable from `element` following (or opposing) flow.
149    ///
150    /// Cycle-safe: a ring main revisits elements, and the visited set is what
151    /// makes this terminate. Ring mains are normal topology, not corruption.
152    pub fn query(&self, element: EntityId, direction: Direction) -> FlowQuery {
153        let mut seen_elements = BTreeSet::new();
154        let mut queue = VecDeque::new();
155        let mut used_undirected = false;
156        seen_elements.insert(element);
157        queue.push_back(element);
158
159        while let Some(current) = queue.pop_front() {
160            let Some(ports) = self.owned.get(&current) else {
161                continue;
162            };
163            for &exit in ports {
164                let (can_exit, exit_stated) = self.may_exit(exit, direction);
165                if !can_exit {
166                    continue;
167                }
168                for next_port in self.adjacency.get(&exit).into_iter().flatten() {
169                    let (can_enter, enter_stated) = self.may_enter(*next_port, direction);
170                    if !can_enter {
171                        continue;
172                    }
173                    let Some(&next) = self.owner.get(next_port) else {
174                        continue;
175                    };
176                    if next == current {
177                        continue;
178                    }
179                    if seen_elements.insert(next) {
180                        if !exit_stated || !enter_stated {
181                            used_undirected = true;
182                        }
183                        queue.push_back(next);
184                    }
185                }
186            }
187        }
188
189        seen_elements.remove(&element);
190        FlowQuery {
191            elements: seen_elements.into_iter().collect(),
192            used_undirected,
193        }
194    }
195
196    /// What this element feeds.
197    pub fn downstream_of(&self, element: EntityId) -> FlowQuery {
198        self.query(element, Direction::Downstream)
199    }
200
201    /// What feeds this element.
202    pub fn upstream_of(&self, element: EntityId) -> FlowQuery {
203        self.query(element, Direction::Upstream)
204    }
205}