Skip to main content

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}