ifc_schedule/sequence/relation.rs
1//! `IfcRelSequence`: predecessor/successor links and their lag.
2//!
3//! # Read by name in the declared release (#212)
4//!
5//! ```text
6//! IfcRelSequence (IfcRelConnects -> IfcRelationship -> IfcRoot)
7//! IFC4, IFC4X3 0 GlobalId 1 OwnerHistory 2 Name 3 Description
8//! 4 RelatingProcess 5 RelatedProcess
9//! 6 TimeLag (OPTIONAL IfcLagTime) 7 SequenceType
10//! 8 UserDefinedSequenceType
11//! IFC2X3 the same first six, 6 TimeLag (IfcTimeMeasure, required)
12//! 7 SequenceType (required)
13//!
14//! IfcLagTime (IfcSchedulingTime; IFC4 and IFC4X3 only)
15//! 0 Name 1 DataOrigin 2 UserDefinedDataOrigin
16//! 3 LagValue 4 DurationType
17//! ```
18//!
19//! Every attribute is found by name in the model's declared release. An
20//! IFC2X3 lag is a time measure on the relationship itself, reported as
21//! [`Sequence::time_lag_measure`], never as an `IfcLagTime`.
22//!
23//! # Direction is stated, not inferred
24//!
25//! Unlike `IfcRelConnectsPorts`, where authoring order carries no physical
26//! meaning, `IfcRelSequence` IS directed by definition: `RelatingProcess` is
27//! the predecessor and `RelatedProcess` is the successor. The schema's own
28//! inverse names confirm it -- `IsPredecessorTo` is `FOR RelatingProcess`.
29//!
30//! # Sequence type says WHICH ends are linked
31//!
32//! `IfcSequenceEnum` is not decoration: `FINISH_START` means the successor
33//! starts after the predecessor finishes, while `START_START` means they start
34//! together. A tool that treats every link as finish-to-start will compute a
35//! schedule that the file does not state.
36//!
37//! # Lag is signed
38//!
39//! `IfcLagTime.LagValue` may be negative: a negative lag is a lead, meaning
40//! the linked ends overlap. It is a stated fact, not a defect to refuse.
41
42use std::collections::HashSet;
43
44use ifc_model::{EntityId, Model, Value};
45
46use crate::error::ScheduleReadError;
47use crate::release::ReadRelease;
48
49const SEQUENCE: &str = "IFCRELSEQUENCE";
50const LAG_TIME: &str = "IFCLAGTIME";
51
52/// `IfcRelSequence` slots in the layout IFC4 and IFC4X3 share, which the
53/// modelless `create_sequence` writes. The reader goes by name.
54pub(crate) mod slot {
55 /// `RelatingProcess`, the predecessor.
56 pub const RELATING: usize = 4;
57 /// `RelatedProcess`, the successor.
58 pub const RELATED: usize = 5;
59 /// `TimeLag`, an `IfcLagTime` reference.
60 pub const TIME_LAG: usize = 6;
61 /// `SequenceType`.
62 pub const SEQUENCE_TYPE: usize = 7;
63}
64
65/// `IfcLagTime` slots (IFC4 and IFC4X3; IFC2X3 has no `IfcLagTime`). The
66/// reader goes by name.
67pub mod lag_slot {
68 /// `LagValue`. Required by the schema.
69 pub const LAG_VALUE: usize = 3;
70 /// `DurationType`. Required by the schema.
71 pub const DURATION_TYPE: usize = 4;
72}
73
74/// The maximum sequence-graph depth walked before reporting a runaway chain.
75pub const MAX_SEQUENCE_DEPTH: usize = 4096;
76
77/// Which ends of two tasks a sequence links.
78///
79/// `IfcSequenceEnum`, verified against IFC4 EXPRESS.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81#[non_exhaustive]
82pub enum SequenceType {
83 /// Successor starts after predecessor starts.
84 StartStart,
85 /// Successor finishes after predecessor starts.
86 StartFinish,
87 /// Successor starts after predecessor finishes. The common case.
88 FinishStart,
89 /// Successor finishes after predecessor finishes.
90 FinishFinish,
91 /// `.USERDEFINED.`
92 UserDefined,
93 /// `.NOTDEFINED.`
94 NotDefined,
95}
96
97impl SequenceType {
98 fn parse(token: &str) -> Option<Self> {
99 Some(match token {
100 "START_START" => Self::StartStart,
101 "START_FINISH" => Self::StartFinish,
102 "FINISH_START" => Self::FinishStart,
103 "FINISH_FINISH" => Self::FinishFinish,
104 "USERDEFINED" => Self::UserDefined,
105 "NOTDEFINED" => Self::NotDefined,
106 _ => return None,
107 })
108 }
109}
110
111/// The lag between two sequenced tasks.
112#[derive(Debug, Clone, PartialEq)]
113#[non_exhaustive]
114pub struct Lag {
115 /// The `IfcLagTime` entity.
116 pub id: EntityId,
117 /// The lag as an authored ISO 8601 duration, when stated as a duration.
118 pub duration: Option<String>,
119 /// The lag as a ratio, when stated as one.
120 ///
121 /// `IfcTimeOrRatioSelect` admits both. A ratio lag means "start when the
122 /// predecessor is 50% done" and cannot be converted to a duration without
123 /// knowing that task's own duration.
124 pub ratio: Option<f64>,
125}
126
127/// One directed sequence link.
128#[derive(Debug, Clone, PartialEq)]
129#[non_exhaustive]
130pub struct Sequence {
131 /// The `IfcRelSequence` entity.
132 pub id: EntityId,
133 /// The predecessor task.
134 pub predecessor: EntityId,
135 /// The successor task.
136 pub successor: EntityId,
137 /// Which ends are linked, if stated.
138 pub sequence_type: Option<SequenceType>,
139 /// The `IfcLagTime` lag, if stated (IFC4 and IFC4X3).
140 pub lag: Option<Lag>,
141 /// IFC2X3's `TimeLag`, an `IfcTimeMeasure` in the project's time unit
142 /// stated on the relationship itself; `None` in IFC4 and IFC4X3, whose
143 /// lag is [`Self::lag`].
144 pub time_lag_measure: Option<f64>,
145}
146
147/// A cycle in the sequence graph.
148///
149/// A schedule whose tasks depend on each other in a loop has no valid
150/// ordering. This is data to report, not a condition to crash on.
151#[derive(Debug, Clone, PartialEq, Eq)]
152#[non_exhaustive]
153pub struct SequenceCycle {
154 /// The task the walk returned to.
155 pub repeated: EntityId,
156 /// The path taken, ending at the repeat.
157 pub path: Vec<EntityId>,
158}
159
160/// Every sequence link in the model, in file order, read against the
161/// model's declared release.
162///
163/// # Errors
164///
165/// [`ScheduleReadError::UnsupportedSchema`] or
166/// [`ScheduleReadError::MultipleSchemas`] for a header the readers cannot
167/// bind; a header with no schema reads as IFC4.
168pub fn sequences(model: &Model) -> Result<Vec<Sequence>, ScheduleReadError> {
169 let release = ReadRelease::of(model)?;
170 let mut out = Vec::new();
171 for (id, entity) in model.of_type(SEQUENCE) {
172 let (Some(predecessor), Some(successor)) = (
173 release.reference(SEQUENCE, entity, "RelatingProcess"),
174 release.reference(SEQUENCE, entity, "RelatedProcess"),
175 ) else {
176 continue;
177 };
178 let sequence_type = release
179 .token(SEQUENCE, entity, "SequenceType")
180 .and_then(SequenceType::parse);
181 let (lag, time_lag_measure) = match release.value(SEQUENCE, entity, "TimeLag") {
182 Some(Value::Ref(lag_id)) => (read_lag(model, release, *lag_id), None),
183 Some(value) => (None, value.unwrap_typed().as_f64()),
184 None => (None, None),
185 };
186 out.push(Sequence {
187 id,
188 predecessor,
189 successor,
190 sequence_type,
191 lag,
192 time_lag_measure,
193 });
194 }
195 Ok(out)
196}
197
198fn read_lag(model: &Model, release: ReadRelease, id: EntityId) -> Option<Lag> {
199 let entity = model.get(id)?;
200 if !entity.type_name.eq_ignore_ascii_case(LAG_TIME) {
201 return None;
202 }
203 let value = release.value(LAG_TIME, entity, "LagValue");
204 // IfcTimeOrRatioSelect: IfcDuration is a string, IfcRatioMeasure a real.
205 // The wrapper distinguishes them, so read both rather than guessing.
206 let duration = value
207 .and_then(|v| v.unwrap_typed().as_text())
208 .map(str::to_string);
209 let ratio = if duration.is_some() {
210 None
211 } else {
212 value.and_then(|v| v.unwrap_typed().as_f64())
213 };
214 Some(Lag {
215 id,
216 duration,
217 ratio,
218 })
219}
220
221/// Tasks that must finish (or start) before `task`, in file order.
222///
223/// # Errors
224///
225/// The binding refusals of [`sequences`].
226pub fn predecessors_of(model: &Model, task: EntityId) -> Result<Vec<EntityId>, ScheduleReadError> {
227 Ok(sequences(model)?
228 .into_iter()
229 .filter(|s| s.successor == task)
230 .map(|s| s.predecessor)
231 .collect())
232}
233
234/// Tasks that follow `task`, in file order.
235///
236/// # Errors
237///
238/// The binding refusals of [`sequences`].
239pub fn successors_of(model: &Model, task: EntityId) -> Result<Vec<EntityId>, ScheduleReadError> {
240 Ok(sequences(model)?
241 .into_iter()
242 .filter(|s| s.predecessor == task)
243 .map(|s| s.successor)
244 .collect())
245}
246
247/// Every task reachable downstream of `task`, depth-first.
248///
249/// Excludes the start. Returns `Err` with the offending path if the graph
250/// cycles: a schedule that loops has no valid ordering, and the loop is the
251/// answer the caller needs.
252///
253/// # Errors
254///
255/// [`ScheduleReadError::Cycle`] when a task is reachable from itself, and
256/// the binding refusals of [`sequences`].
257pub fn downstream_of(model: &Model, task: EntityId) -> Result<Vec<EntityId>, ScheduleReadError> {
258 let all = sequences(model)?;
259 Ok(downstream_in(&all, task)?)
260}
261
262/// [`downstream_of`] over already read links.
263fn downstream_in(all: &[Sequence], task: EntityId) -> Result<Vec<EntityId>, SequenceCycle> {
264 let mut out = Vec::new();
265 let mut path = Vec::new();
266 let mut on_path = HashSet::new();
267 let mut seen = HashSet::new();
268 walk(all, task, &mut out, &mut path, &mut on_path, &mut seen)?;
269 Ok(out)
270}
271
272fn walk(
273 all: &[Sequence],
274 node: EntityId,
275 out: &mut Vec<EntityId>,
276 path: &mut Vec<EntityId>,
277 on_path: &mut HashSet<EntityId>,
278 seen: &mut HashSet<EntityId>,
279) -> Result<(), SequenceCycle> {
280 if path.len() >= MAX_SEQUENCE_DEPTH {
281 return Ok(());
282 }
283 path.push(node);
284 on_path.insert(node);
285
286 for successor in all
287 .iter()
288 .filter(|s| s.predecessor == node)
289 .map(|s| s.successor)
290 {
291 if on_path.contains(&successor) {
292 let mut cycle = path.clone();
293 cycle.push(successor);
294 return Err(SequenceCycle {
295 repeated: successor,
296 path: cycle,
297 });
298 }
299 // A diamond reconverges on the same task by two routes; report it
300 // once, but still recurse the first time it is seen.
301 if seen.insert(successor) {
302 out.push(successor);
303 walk(all, successor, out, path, on_path, seen)?;
304 }
305 }
306
307 path.pop();
308 on_path.remove(&node);
309 Ok(())
310}
311
312/// The first cycle in the whole sequence graph, if any.
313///
314/// Checks every task, so a cycle in a disconnected component is still found.
315///
316/// # Errors
317///
318/// The binding refusals of [`sequences`].
319pub fn find_cycle(model: &Model) -> Result<Option<SequenceCycle>, ScheduleReadError> {
320 let all = sequences(model)?;
321 for (id, _) in model.of_type("IFCTASK") {
322 if let Err(cycle) = downstream_in(&all, id) {
323 return Ok(Some(cycle));
324 }
325 }
326 Ok(None)
327}