Skip to main content

ifc_schedule/sequence/
relation.rs

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