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(¤t) 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}