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::{HashMap, HashSet};
43
44use ifc_model::{EntityId, Model, Value};
45
46use crate::error::ScheduleReadError;
47use crate::release::ReadRelease;
48use crate::task::DurationType;
49
50const SEQUENCE: &str = "IFCRELSEQUENCE";
51const LAG_TIME: &str = "IFCLAGTIME";
52
53/// `IfcRelSequence` slots in the layout IFC4 and IFC4X3 share, which the
54/// modelless `create_sequence` writes. The reader goes by name.
55pub(crate) mod slot {
56 /// `RelatingProcess`, the predecessor.
57 pub const RELATING: usize = 4;
58 /// `RelatedProcess`, the successor.
59 pub const RELATED: usize = 5;
60 /// `TimeLag`, an `IfcLagTime` reference.
61 pub const TIME_LAG: usize = 6;
62 /// `SequenceType`.
63 pub const SEQUENCE_TYPE: usize = 7;
64}
65
66/// `IfcLagTime` slots (IFC4 and IFC4X3; IFC2X3 has no `IfcLagTime`). The
67/// reader goes by name.
68pub mod lag_slot {
69 /// `LagValue`. Required by the schema.
70 pub const LAG_VALUE: usize = 3;
71 /// `DurationType`. Required by the schema.
72 pub const DURATION_TYPE: usize = 4;
73}
74
75/// The longest chain of processes a sequence walk follows.
76///
77/// A walk that would go deeper is refused with
78/// [`ScheduleReadError::SequenceDepthExceeded`] rather than returned
79/// truncated (#236).
80pub const MAX_SEQUENCE_DEPTH: usize = 4096;
81
82/// Which ends of two tasks a sequence links.
83///
84/// `IfcSequenceEnum`, verified against IFC4 EXPRESS.
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86#[non_exhaustive]
87pub enum SequenceType {
88 /// Successor starts after predecessor starts.
89 StartStart,
90 /// Successor finishes after predecessor starts.
91 StartFinish,
92 /// Successor starts after predecessor finishes. The common case.
93 FinishStart,
94 /// Successor finishes after predecessor finishes.
95 FinishFinish,
96 /// `.USERDEFINED.`
97 UserDefined,
98 /// `.NOTDEFINED.`
99 NotDefined,
100}
101
102impl SequenceType {
103 fn parse(token: &str) -> Option<Self> {
104 Some(match token {
105 "START_START" => Self::StartStart,
106 "START_FINISH" => Self::StartFinish,
107 "FINISH_START" => Self::FinishStart,
108 "FINISH_FINISH" => Self::FinishFinish,
109 "USERDEFINED" => Self::UserDefined,
110 "NOTDEFINED" => Self::NotDefined,
111 _ => return None,
112 })
113 }
114}
115
116/// The lag between two sequenced tasks.
117#[derive(Debug, Clone, PartialEq)]
118#[non_exhaustive]
119pub struct Lag {
120 /// The `IfcLagTime` entity.
121 pub id: EntityId,
122 /// The lag as an authored ISO 8601 duration, when stated as a duration.
123 pub duration: Option<String>,
124 /// The lag as a ratio, when stated as one.
125 ///
126 /// `IfcTimeOrRatioSelect` admits both. A ratio lag means "start when the
127 /// predecessor is 50% done" and cannot be converted to a duration without
128 /// knowing that task's own duration.
129 pub ratio: Option<f64>,
130 /// `IfcLagTime.DurationType`: whether the lag counts working time or
131 /// elapsed time (#236). Required by the schema; `None` when the record
132 /// leaves it unset or states a token outside `IfcTaskDurationEnum`.
133 pub duration_type: Option<DurationType>,
134 /// `IfcSchedulingTime.Name`, as authored (#236).
135 pub name: Option<String>,
136}
137
138/// One directed sequence link.
139#[derive(Debug, Clone, PartialEq)]
140#[non_exhaustive]
141pub struct Sequence {
142 /// The `IfcRelSequence` entity.
143 pub id: EntityId,
144 /// The predecessor task.
145 pub predecessor: EntityId,
146 /// The successor task.
147 pub successor: EntityId,
148 /// Which ends are linked, if stated.
149 pub sequence_type: Option<SequenceType>,
150 /// The `IfcLagTime` lag, if stated (IFC4 and IFC4X3).
151 pub lag: Option<Lag>,
152 /// IFC2X3's `TimeLag`, an `IfcTimeMeasure` in the project's time unit
153 /// stated on the relationship itself; `None` in IFC4 and IFC4X3, whose
154 /// lag is [`Self::lag`].
155 pub time_lag_measure: Option<f64>,
156}
157
158/// A cycle in the sequence graph.
159///
160/// A schedule whose tasks depend on each other in a loop has no valid
161/// ordering. This is data to report, not a condition to crash on.
162#[derive(Debug, Clone, PartialEq, Eq)]
163#[non_exhaustive]
164pub struct SequenceCycle {
165 /// The task the walk returned to.
166 pub repeated: EntityId,
167 /// The path taken, ending at the repeat.
168 pub path: Vec<EntityId>,
169}
170
171/// Every sequence link in the model, in file order, read against the
172/// model's declared release.
173///
174/// # Errors
175///
176/// [`ScheduleReadError::UnsupportedSchema`] or
177/// [`ScheduleReadError::MultipleSchemas`] for a header the readers cannot
178/// bind; a header with no schema reads as IFC4.
179pub fn sequences(model: &Model) -> Result<Vec<Sequence>, ScheduleReadError> {
180 let release = ReadRelease::of(model)?;
181 let mut out = Vec::new();
182 for (id, entity) in model.of_type(SEQUENCE) {
183 let (Some(predecessor), Some(successor)) = (
184 release.reference(SEQUENCE, entity, "RelatingProcess"),
185 release.reference(SEQUENCE, entity, "RelatedProcess"),
186 ) else {
187 continue;
188 };
189 let sequence_type = release
190 .token(SEQUENCE, entity, "SequenceType")
191 .and_then(SequenceType::parse);
192 let (lag, time_lag_measure) = match release.value(SEQUENCE, entity, "TimeLag") {
193 Some(Value::Ref(lag_id)) => (read_lag(model, release, *lag_id), None),
194 Some(value) => (None, value.unwrap_typed().as_f64()),
195 None => (None, None),
196 };
197 out.push(Sequence {
198 id,
199 predecessor,
200 successor,
201 sequence_type,
202 lag,
203 time_lag_measure,
204 });
205 }
206 Ok(out)
207}
208
209fn read_lag(model: &Model, release: ReadRelease, id: EntityId) -> Option<Lag> {
210 let entity = model.get(id)?;
211 if !entity.type_name.eq_ignore_ascii_case(LAG_TIME) {
212 return None;
213 }
214 let value = release.value(LAG_TIME, entity, "LagValue");
215 // IfcTimeOrRatioSelect: IfcDuration is a string, IfcRatioMeasure a real.
216 // The wrapper distinguishes them, so read both rather than guessing.
217 let duration = value
218 .and_then(|v| v.unwrap_typed().as_text())
219 .map(str::to_string);
220 let ratio = if duration.is_some() {
221 None
222 } else {
223 value.and_then(|v| v.unwrap_typed().as_f64())
224 };
225 Some(Lag {
226 id,
227 duration,
228 ratio,
229 duration_type: release
230 .token(LAG_TIME, entity, "DurationType")
231 .and_then(DurationType::parse),
232 name: release.text(LAG_TIME, entity, "Name").map(str::to_string),
233 })
234}
235
236/// Processes that must finish (or start) before `task`, in file order.
237///
238/// # Errors
239///
240/// The binding refusals of [`sequences`].
241pub fn predecessors_of(model: &Model, task: EntityId) -> Result<Vec<EntityId>, ScheduleReadError> {
242 Ok(sequences(model)?
243 .into_iter()
244 .filter(|s| s.successor == task)
245 .map(|s| s.predecessor)
246 .collect())
247}
248
249/// Processes that follow `task`, in file order.
250///
251/// # Errors
252///
253/// The binding refusals of [`sequences`].
254pub fn successors_of(model: &Model, task: EntityId) -> Result<Vec<EntityId>, ScheduleReadError> {
255 Ok(sequences(model)?
256 .into_iter()
257 .filter(|s| s.predecessor == task)
258 .map(|s| s.successor)
259 .collect())
260}
261
262/// Every process reachable downstream of `task`, depth-first.
263///
264/// Excludes the start. Returns `Err` with the offending path if the graph
265/// cycles: a schedule that loops has no valid ordering, and the loop is the
266/// answer the caller needs. Every `IfcRelSequence` is followed, whatever
267/// `IfcProcess` it relates.
268///
269/// # Errors
270///
271/// [`ScheduleReadError::Cycle`] when a process is reachable from itself,
272/// [`ScheduleReadError::SequenceDepthExceeded`] when a chain from `task` is
273/// longer than [`MAX_SEQUENCE_DEPTH`], and the binding refusals of
274/// [`sequences`].
275pub fn downstream_of(model: &Model, task: EntityId) -> Result<Vec<EntityId>, ScheduleReadError> {
276 let all = sequences(model)?;
277 downstream_in(&successor_map(&all), task)
278}
279
280/// Successors of each predecessor, in file order of the links.
281fn successor_map(all: &[Sequence]) -> HashMap<EntityId, Vec<EntityId>> {
282 let mut map: HashMap<EntityId, Vec<EntityId>> = HashMap::new();
283 for link in all {
284 map.entry(link.predecessor)
285 .or_default()
286 .push(link.successor);
287 }
288 map
289}
290
291/// [`downstream_of`] over already read links.
292///
293/// An explicit stack rather than recursion, so the depth budget, not the
294/// thread's stack, bounds the walk.
295fn downstream_in(
296 successors: &HashMap<EntityId, Vec<EntityId>>,
297 start: EntityId,
298) -> Result<Vec<EntityId>, ScheduleReadError> {
299 let mut out = Vec::new();
300 let mut seen = HashSet::new();
301 // The current path, each node with the index of its next successor.
302 let mut path: Vec<(EntityId, usize)> = vec![(start, 0)];
303 let mut on_path = HashSet::from([start]);
304 while let Some((node, next)) = path.last_mut() {
305 let node = *node;
306 let Some(&successor) = successors.get(&node).and_then(|all| all.get(*next)) else {
307 path.pop();
308 on_path.remove(&node);
309 continue;
310 };
311 *next += 1;
312 if on_path.contains(&successor) {
313 let mut cycle: Vec<EntityId> = path.iter().map(|(id, _)| *id).collect();
314 cycle.push(successor);
315 return Err(SequenceCycle {
316 repeated: successor,
317 path: cycle,
318 }
319 .into());
320 }
321 // A diamond reconverges on the same process by two routes; report it
322 // once, but still descend the first time it is seen.
323 if seen.insert(successor) {
324 out.push(successor);
325 if path.len() >= MAX_SEQUENCE_DEPTH {
326 return Err(ScheduleReadError::SequenceDepthExceeded {
327 start,
328 limit: MAX_SEQUENCE_DEPTH,
329 });
330 }
331 path.push((successor, 0));
332 on_path.insert(successor);
333 }
334 }
335 Ok(out)
336}
337
338/// Every `IfcProcess` in the model, in file order, per its declared release.
339pub(crate) fn processes(model: &Model) -> Result<Vec<EntityId>, ScheduleReadError> {
340 Ok(ReadRelease::of(model)?.instances_of(model, "IFCPROCESS"))
341}
342
343/// The first cycle in the whole sequence graph, if any.
344///
345/// Walks from every `IfcProcess` (tasks, procedures and events alike), so a
346/// cycle through a non-task process, or in a disconnected component, is
347/// still found (#236).
348///
349/// # Errors
350///
351/// [`ScheduleReadError::SequenceDepthExceeded`] when a chain is longer than
352/// [`MAX_SEQUENCE_DEPTH`], since a truncated search cannot say there is no
353/// cycle, and the binding refusals of [`sequences`].
354pub fn find_cycle(model: &Model) -> Result<Option<SequenceCycle>, ScheduleReadError> {
355 let all = sequences(model)?;
356 let successors = successor_map(&all);
357 // A process already reached from an earlier start had its whole
358 // downstream walked without a cycle, so walking from it again finds none.
359 let mut covered = HashSet::new();
360 for id in processes(model)? {
361 if covered.contains(&id) {
362 continue;
363 }
364 match downstream_in(&successors, id) {
365 Ok(reached) => {
366 covered.insert(id);
367 covered.extend(reached);
368 }
369 Err(ScheduleReadError::Cycle(cycle)) => return Ok(Some(cycle)),
370 Err(other) => return Err(other),
371 }
372 }
373 Ok(None)
374}