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