Skip to main content

ifc_systems/connectivity/
relation.rs

1//! `IfcRelConnectsPorts` and the port connection graph.
2//!
3//! # Slots
4//!
5//! ```text
6//! IfcRelConnectsPorts   4 = RelatingPort   5 = RelatedPort   6 = RealizingElement
7//! ```
8//!
9//! # Direction is not connectivity
10//!
11//! `RelatingPort` and `RelatedPort` record which port the exporter wrote
12//! first, NOT which way anything flows. Flow is stated separately by each
13//! port's `FlowDirection`. Treating the relationship as directed would invent
14//! a direction: a pipe connecting an outlet to an inlet is the same physical
15//! connection whichever end the exporter happened to name first.
16//!
17//! The graph is therefore UNDIRECTED, and direction is applied on top from
18//! flow directions when a caller asks for it.
19
20use std::collections::{BTreeMap, BTreeSet};
21
22use ifc_model::{EntityId, Model, Value};
23
24use crate::error::SystemAnomaly;
25use crate::release;
26
27pub(crate) mod slot {
28    /// `IfcRelConnectsPorts.RelatingPort`.
29    pub const RELATING: usize = 4;
30    /// `IfcRelConnectsPorts.RelatedPort`.
31    pub const RELATED: usize = 5;
32    /// `IfcRelConnectsPorts.RealizingElement` -- the pipe/duct that realises
33    /// the connection, when the file names one.
34    pub const REALIZING: usize = 6;
35}
36
37/// One stated port-to-port connection.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct Connection {
40    /// The `IfcRelConnectsPorts` entity.
41    pub id: EntityId,
42    /// The port named first. NOT an upstream/downstream claim.
43    pub relating: EntityId,
44    /// The port named second.
45    pub related: EntityId,
46    /// The element realising this connection, when stated.
47    pub realizing: Option<EntityId>,
48}
49
50/// An undirected port connection graph.
51///
52/// Built once and queried many times: a traversal that re-scanned the model
53/// for each step would be quadratic on files with thousands of ports.
54#[derive(Debug, Default, Clone)]
55pub struct ConnectionGraph {
56    connections: Vec<Connection>,
57    adjacency: BTreeMap<EntityId, BTreeSet<EntityId>>,
58}
59
60impl ConnectionGraph {
61    /// Read every `IfcRelConnectsPorts` in the file.
62    ///
63    /// A connection naming a non-port, or an entity not in the file, is
64    /// reported and skipped: one malformed relationship must not cost the
65    /// caller the rest of the network.
66    ///
67    /// Port ancestry is read against the release the model declares.
68    pub fn build(model: &Model) -> (Self, Vec<SystemAnomaly>) {
69        let schema = release::resolve_or_ifc4(model);
70        let mut anomalies = Vec::new();
71        let mut graph = Self::default();
72
73        for &id in model.ids_of_type("IFCRELCONNECTSPORTS") {
74            let Some(entity) = model.get(id) else {
75                continue;
76            };
77            let relating = match entity.attributes.get(slot::RELATING) {
78                Some(Value::Ref(port)) => *port,
79                _ => continue,
80            };
81            let related = match entity.attributes.get(slot::RELATED) {
82                Some(Value::Ref(port)) => *port,
83                _ => continue,
84            };
85
86            let mut ok = true;
87            for port in [relating, related] {
88                match model.get(port) {
89                    None => {
90                        anomalies.push(SystemAnomaly::Dangling {
91                            relation: id,
92                            missing: port,
93                        });
94                        ok = false;
95                    }
96                    Some(e) if !schema.is_a(&e.type_name.to_ascii_uppercase(), "IFCPORT") => {
97                        anomalies.push(SystemAnomaly::NotAPort {
98                            relation: id,
99                            entity: port,
100                            type_name: e.type_name.to_ascii_uppercase(),
101                        });
102                        ok = false;
103                    }
104                    Some(_) => {}
105                }
106            }
107            if !ok {
108                continue;
109            }
110
111            let realizing = match entity.attributes.get(slot::REALIZING) {
112                Some(Value::Ref(element)) => Some(*element),
113                _ => None,
114            };
115            graph.connections.push(Connection {
116                id,
117                relating,
118                related,
119                realizing,
120            });
121            // Undirected: both directions are inserted, because the schema
122            // order records authoring order and not flow.
123            graph.adjacency.entry(relating).or_default().insert(related);
124            graph.adjacency.entry(related).or_default().insert(relating);
125        }
126        (graph, anomalies)
127    }
128
129    /// Every stated connection, in file order.
130    pub fn connections(&self) -> &[Connection] {
131        &self.connections
132    }
133
134    /// Ports directly connected to `port`, ascending by id.
135    pub fn neighbours(&self, port: EntityId) -> Vec<EntityId> {
136        self.adjacency
137            .get(&port)
138            .map(|set| set.iter().copied().collect())
139            .unwrap_or_default()
140    }
141
142    /// Every port reachable from `start`, including `start` itself.
143    ///
144    /// Breadth-first with a visited set, so a network containing a LOOP -- a
145    /// ring main, a recirculating circuit -- terminates instead of running
146    /// forever. Loops are normal in real distribution systems, not corrupt
147    /// data, so this must not be a refusal.
148    pub fn reachable_from(&self, start: EntityId) -> Vec<EntityId> {
149        let mut seen = BTreeSet::new();
150        let mut queue = std::collections::VecDeque::new();
151        seen.insert(start);
152        queue.push_back(start);
153        while let Some(port) = queue.pop_front() {
154            for next in self.adjacency.get(&port).into_iter().flatten() {
155                if seen.insert(*next) {
156                    queue.push_back(*next);
157                }
158            }
159        }
160        seen.into_iter().collect()
161    }
162
163    /// Connected components of the graph, each sorted, components ascending.
164    ///
165    /// A distribution system that splits into two components is usually an
166    /// authoring error -- a missing connection -- and is worth surfacing.
167    pub fn components(&self) -> Vec<Vec<EntityId>> {
168        let mut seen = BTreeSet::new();
169        let mut out = Vec::new();
170        for &port in self.adjacency.keys() {
171            if seen.contains(&port) {
172                continue;
173            }
174            let component = self.reachable_from(port);
175            seen.extend(component.iter().copied());
176            out.push(component);
177        }
178        out
179    }
180}
181
182/// A network view that also steps THROUGH elements, not just between them.
183///
184/// `IfcRelConnectsPorts` joins one element's port to another's. It never
185/// joins an element's OWN ports to each other: the fact that fluid entering
186/// a pipe's inlet leaves by its outlet is implied by the element, not stated
187/// by any relationship.
188///
189/// So the raw connection graph of a real chain
190///
191/// ```text
192/// [seg0] out --- in [seg1] out --- in [fitting]
193/// ```
194///
195/// has NO path from seg0's inlet to seg1 at all: every connection is an
196/// isolated pair. Answering "what is downstream of this pipe" needs both
197/// kinds of edge -- across connections AND through elements.
198///
199/// This is the distinction between the two, made explicit rather than
200/// silently folded into [`ConnectionGraph`].
201#[derive(Debug, Default, Clone)]
202pub struct NetworkGraph {
203    adjacency: BTreeMap<EntityId, BTreeSet<EntityId>>,
204}
205
206impl NetworkGraph {
207    /// Combine stated connections with through-element port pairing.
208    ///
209    /// `ports` supplies each port's owning element, which is what makes the
210    /// through-element edges knowable.
211    pub fn build(graph: &ConnectionGraph, ports: &[crate::port::Port]) -> Self {
212        let mut adjacency: BTreeMap<EntityId, BTreeSet<EntityId>> = BTreeMap::new();
213
214        // Stated port-to-port connections.
215        for connection in &graph.connections {
216            adjacency
217                .entry(connection.relating)
218                .or_default()
219                .insert(connection.related);
220            adjacency
221                .entry(connection.related)
222                .or_default()
223                .insert(connection.relating);
224        }
225
226        // Through-element edges: every pair of ports on the same element.
227        let mut by_element: BTreeMap<EntityId, Vec<EntityId>> = BTreeMap::new();
228        for port in ports {
229            if let Some(element) = port.element {
230                by_element.entry(element).or_default().push(port.id);
231            }
232        }
233        for members in by_element.values() {
234            for (i, &a) in members.iter().enumerate() {
235                for &b in &members[i + 1..] {
236                    adjacency.entry(a).or_default().insert(b);
237                    adjacency.entry(b).or_default().insert(a);
238                }
239            }
240        }
241
242        Self { adjacency }
243    }
244
245    /// Every port reachable from `start`, including `start`.
246    ///
247    /// Cycle-safe: ring mains are normal, so a visited set is required, not
248    /// an optimisation.
249    pub fn reachable_from(&self, start: EntityId) -> Vec<EntityId> {
250        let mut seen = BTreeSet::new();
251        let mut queue = std::collections::VecDeque::new();
252        seen.insert(start);
253        queue.push_back(start);
254        while let Some(port) = queue.pop_front() {
255            for next in self.adjacency.get(&port).into_iter().flatten() {
256                if seen.insert(*next) {
257                    queue.push_back(*next);
258                }
259            }
260        }
261        seen.into_iter().collect()
262    }
263
264    /// Connected components, each sorted, components ascending.
265    pub fn components(&self) -> Vec<Vec<EntityId>> {
266        let mut seen = BTreeSet::new();
267        let mut out = Vec::new();
268        for &port in self.adjacency.keys() {
269            if seen.contains(&port) {
270                continue;
271            }
272            let component = self.reachable_from(port);
273            seen.extend(component.iter().copied());
274            out.push(component);
275        }
276        out
277    }
278}