Skip to main content

zincite_check/
scheduling.rs

1//! Scheduling relations over concrete candidate values.
2use super::*;
3
4#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5pub(crate) enum SchedulingName {
6    Strict,
7    Unary,
8    OptionalUnary,
9    OptionalCumulative,
10    Cumulative,
11    Cumulatives,
12    UnaryFlex,
13    OptionalUnaryFlex,
14    CumulativeFlex,
15    OptionalCumulativeFlex,
16    CumulativesFlex,
17}
18impl SchedulingName {
19    pub fn from_name(name: &str, arity: usize) -> Option<Self> {
20        if name == "fzn_disjunctive_strict" {
21            return (arity == 2).then_some(Self::Strict);
22        }
23        for base in [
24            name,
25            name.strip_prefix("gecode_").unwrap_or(""),
26            name.strip_prefix("fzn_").unwrap_or(""),
27        ] {
28            let kind = match base {
29                "gecode_schedule_unary" => Self::Unary,
30                "gecode_schedule_unary_optional" => Self::OptionalUnary,
31                "gecode_schedule_cumulative_optional" => Self::OptionalCumulative,
32                "gecode_unary_flex" => Self::UnaryFlex,
33                "gecode_unary_flex_optional" => Self::OptionalUnaryFlex,
34                "gecode_cumulative_flex" => Self::CumulativeFlex,
35                "gecode_cumulative_flex_optional" => Self::OptionalCumulativeFlex,
36                "gecode_cumulatives_flex" => Self::CumulativesFlex,
37                "cumulatives" => match arity {
38                    4 => Self::Cumulative,
39                    6 => Self::Cumulatives,
40                    7 => Self::CumulativesFlex,
41                    _ => return None,
42                },
43                _ => continue,
44            };
45            return (arity == kind.arity()).then_some(kind);
46        }
47        None
48    }
49    fn arity(self) -> usize {
50        match self {
51            Self::Strict | Self::Unary => 2,
52            Self::OptionalUnary => 3,
53            Self::OptionalCumulative => 5,
54            Self::Cumulative => 4,
55            Self::Cumulatives => 6,
56            Self::UnaryFlex => 3,
57            Self::OptionalUnaryFlex => 4,
58            Self::CumulativeFlex => 5,
59            Self::OptionalCumulativeFlex => 6,
60            Self::CumulativesFlex => 7,
61        }
62    }
63    #[cfg(feature = "prepare")]
64    pub fn uses_explicit_ends(self) -> bool {
65        matches!(
66            self,
67            Self::UnaryFlex
68                | Self::OptionalUnaryFlex
69                | Self::CumulativeFlex
70                | Self::OptionalCumulativeFlex
71                | Self::CumulativesFlex
72        )
73    }
74    #[cfg(feature = "prepare")]
75    pub fn signature(self) -> Vec<prepare::ArgumentType> {
76        let argument = |kind, is_array, is_var| prepare::ArgumentType {
77            kind,
78            is_array,
79            is_var,
80        };
81        let integer_array = |is_var| argument(ValueType::Integer, true, is_var);
82        if self.uses_explicit_ends() {
83            let mut signature = vec![integer_array(true); 3];
84            match self {
85                Self::UnaryFlex => {}
86                Self::OptionalUnaryFlex => signature.push(argument(ValueType::Boolean, true, true)),
87                Self::CumulativeFlex | Self::OptionalCumulativeFlex => {
88                    signature.extend([
89                        integer_array(false),
90                        argument(ValueType::Integer, false, true),
91                    ]);
92                    if self == Self::OptionalCumulativeFlex {
93                        signature.push(argument(ValueType::Boolean, true, true));
94                    }
95                }
96                Self::CumulativesFlex => signature.extend([
97                    integer_array(true),
98                    integer_array(true),
99                    integer_array(false),
100                    argument(ValueType::Boolean, false, false),
101                ]),
102                _ => unreachable!(),
103            }
104            return signature;
105        }
106        let mut signature = vec![
107            integer_array(true),
108            integer_array(matches!(
109                self,
110                Self::Strict | Self::Cumulative | Self::Cumulatives
111            )),
112        ];
113        match self {
114            Self::Strict | Self::Unary => {}
115            Self::OptionalUnary => signature.push(argument(ValueType::Boolean, true, true)),
116            Self::OptionalCumulative => signature.extend([
117                integer_array(false),
118                argument(ValueType::Boolean, true, true),
119                argument(ValueType::Integer, false, true),
120            ]),
121            Self::Cumulative => signature.extend([
122                integer_array(true),
123                argument(ValueType::Integer, false, true),
124            ]),
125            Self::Cumulatives => signature.extend([
126                integer_array(true),
127                integer_array(true),
128                integer_array(false),
129                argument(ValueType::Boolean, false, false),
130            ]),
131            _ => unreachable!(),
132        }
133        signature
134    }
135}
136
137#[derive(Debug, Serialize, Deserialize)]
138pub(crate) struct ScheduleTasks {
139    pub starts: Vec<Operand>,
140    pub durations: Vec<Operand>,
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub ends: Option<Vec<Operand>>,
143}
144#[derive(Debug, Serialize, Deserialize)]
145pub(crate) enum Scheduling {
146    Strict(ScheduleTasks),
147    Unary {
148        tasks: ScheduleTasks,
149        present: Option<Vec<Operand>>,
150    },
151    OptionalCumulative {
152        tasks: ScheduleTasks,
153        usages: Vec<Operand>,
154        present: Vec<Operand>,
155        capacity: Operand,
156    },
157    Cumulative {
158        tasks: ScheduleTasks,
159        usages: Vec<Operand>,
160        capacity: Operand,
161    },
162    Cumulatives {
163        tasks: ScheduleTasks,
164        usages: Vec<Operand>,
165        machines: Vec<Operand>,
166        capacities: Vec<Operand>,
167        upper: bool,
168    },
169}
170
171#[derive(Clone, Debug, Serialize)]
172pub struct SchedulingTask {
173    pub position: usize,
174    pub start: Option<ArgumentValue>,
175    pub duration: Option<ArgumentValue>,
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub end: Option<ArgumentValue>,
178    pub usage: Option<ArgumentValue>,
179    pub present: Option<ArgumentValue>,
180    pub machine: Option<ArgumentValue>,
181}
182#[derive(Clone, Debug, Serialize)]
183#[serde(tag = "kind", rename_all = "snake_case")]
184pub enum SchedulingFault {
185    Duration {
186        position: usize,
187        #[serde(serialize_with = "serialize_integer")]
188        value: i64,
189    },
190    Machine {
191        position: usize,
192        #[serde(serialize_with = "serialize_integer")]
193        value: i64,
194    },
195    Capacity {
196        #[serde(serialize_with = "serialize_integer")]
197        value: i64,
198    },
199    Conflict {
200        first: usize,
201        second: usize,
202    },
203    TaskUsage {
204        position: usize,
205        #[serde(serialize_with = "serialize_integer")]
206        usage: i64,
207        #[serde(serialize_with = "serialize_integer")]
208        bound: i64,
209    },
210    Resource {
211        #[serde(serialize_with = "serialize_optional_integer")]
212        machine: Option<i64>,
213        #[serde(serialize_with = "serialize_integer")]
214        time: i64,
215        #[serde(serialize_with = "serialize_integer")]
216        usage: i64,
217        #[serde(serialize_with = "serialize_integer")]
218        bound: i64,
219        upper: bool,
220        tasks: Vec<usize>,
221    },
222}
223#[derive(Clone, Debug, Serialize)]
224pub struct SchedulingFinding {
225    pub tasks: Vec<SchedulingTask>,
226    pub capacities: Vec<ArgumentValue>,
227    pub faults: Vec<SchedulingFault>,
228}
229
230impl Scheduling {
231    pub fn name(&self) -> SchedulingName {
232        if self.tasks().ends.is_some() {
233            return match self {
234                Self::Unary { present: None, .. } => SchedulingName::UnaryFlex,
235                Self::Unary {
236                    present: Some(_), ..
237                } => SchedulingName::OptionalUnaryFlex,
238                Self::Cumulative { .. } => SchedulingName::CumulativeFlex,
239                Self::OptionalCumulative { .. } => SchedulingName::OptionalCumulativeFlex,
240                Self::Cumulatives { .. } => SchedulingName::CumulativesFlex,
241                Self::Strict(_) => SchedulingName::Strict,
242            };
243        }
244        match self {
245            Self::Strict(_) => SchedulingName::Strict,
246            Self::Unary { present: None, .. } => SchedulingName::Unary,
247            Self::Unary {
248                present: Some(_), ..
249            } => SchedulingName::OptionalUnary,
250            Self::OptionalCumulative { .. } => SchedulingName::OptionalCumulative,
251            Self::Cumulative { .. } => SchedulingName::Cumulative,
252            Self::Cumulatives { .. } => SchedulingName::Cumulatives,
253        }
254    }
255    pub fn arity(&self) -> usize {
256        self.name().arity()
257    }
258    fn tasks(&self) -> &ScheduleTasks {
259        match self {
260            Self::Strict(tasks)
261            | Self::Unary { tasks, .. }
262            | Self::OptionalCumulative { tasks, .. }
263            | Self::Cumulative { tasks, .. }
264            | Self::Cumulatives { tasks, .. } => tasks,
265        }
266    }
267    pub fn presence(&self) -> Option<&[Operand]> {
268        match self {
269            Self::Unary { present, .. } => present.as_deref(),
270            Self::OptionalCumulative { present, .. } => Some(present),
271            _ => None,
272        }
273    }
274    fn usages(&self) -> Option<&[Operand]> {
275        match self {
276            Self::OptionalCumulative { usages, .. }
277            | Self::Cumulative { usages, .. }
278            | Self::Cumulatives { usages, .. } => Some(usages),
279            _ => None,
280        }
281    }
282    fn capacities(&self) -> &[Operand] {
283        match self {
284            Self::OptionalCumulative { capacity, .. } | Self::Cumulative { capacity, .. } => {
285                std::slice::from_ref(capacity)
286            }
287            Self::Cumulatives { capacities, .. } => capacities,
288            _ => &[],
289        }
290    }
291    fn machines(&self) -> Option<&[Operand]> {
292        match self {
293            Self::Cumulatives { machines, .. } => Some(machines),
294            _ => None,
295        }
296    }
297    pub fn integer_operands(&self) -> Vec<&Operand> {
298        let tasks = self.tasks();
299        tasks
300            .starts
301            .iter()
302            .chain(&tasks.durations)
303            .chain(tasks.ends.iter().flatten())
304            .chain(self.usages().into_iter().flatten())
305            .chain(self.machines().into_iter().flatten())
306            .chain(self.capacities())
307            .collect()
308    }
309    pub fn operands(&self) -> Vec<&Operand> {
310        let mut operands = self.integer_operands();
311        operands.extend(self.presence().into_iter().flatten());
312        operands
313    }
314    pub fn validate_shape(&self) -> Result<(), &'static str> {
315        let tasks = self.tasks();
316        let n = tasks.starts.len();
317        if tasks.durations.len() != n
318            || tasks.ends.as_ref().is_some_and(|ends| ends.len() != n)
319            || self.usages().is_some_and(|entries| entries.len() != n)
320            || self.presence().is_some_and(|entries| entries.len() != n)
321            || self.machines().is_some_and(|entries| entries.len() != n)
322        {
323            return Err("scheduling task arrays must have equal lengths");
324        }
325        if matches!(self, Self::Strict(_)) && tasks.ends.is_some() {
326            return Err("strict disjunctive does not accept explicit ends");
327        }
328        let fixed_nonnegative = |entries: &[Operand]| {
329            entries.iter().all(
330                |operand| matches!(operand, Operand::Literal(Value::Integer(value)) if *value >= 0),
331            )
332        };
333        if matches!(self, Self::Unary { .. } | Self::OptionalCumulative { .. })
334            && tasks.ends.is_none()
335            && !fixed_nonnegative(&tasks.durations)
336        {
337            return Err(
338                "native unary and optional cumulative durations must be fixed nonnegative integers",
339            );
340        }
341        if (matches!(self, Self::OptionalCumulative { .. })
342            || self.name() == SchedulingName::CumulativeFlex)
343            && let Some(usages) = self.usages()
344            && !fixed_nonnegative(usages)
345        {
346            return Err("native cumulative usages must be fixed nonnegative integers");
347        }
348        if let Self::Cumulatives { capacities, .. } = self
349            && !capacities
350                .iter()
351                .all(|operand| matches!(operand, Operand::Literal(Value::Integer(_))))
352        {
353            return Err("multi-machine capacities must be fixed integers");
354        }
355        Ok(())
356    }
357    pub fn required_operands(&self, values: &[Option<Value>]) -> Vec<&Operand> {
358        if self.precondition_faults(values).is_empty() {
359            self.relation_operands(values)
360        } else {
361            Vec::new()
362        }
363    }
364    fn precondition_faults(&self, values: &[Option<Value>]) -> Vec<SchedulingFault> {
365        let mut faults = Vec::new();
366        if (matches!(self, Self::OptionalCumulative { .. })
367            || self.name() == SchedulingName::CumulativeFlex)
368            && let Some(Value::Integer(value)) = self.capacities()[0].value(values)
369            && *value < 0
370        {
371            faults.push(SchedulingFault::Capacity { value: *value });
372        }
373        if self.name() != SchedulingName::Cumulative
374            && self.name() != SchedulingName::CumulativesFlex
375        {
376            for (i, duration) in self.tasks().durations.iter().enumerate() {
377                if let Some(Value::Integer(value)) = duration.value(values)
378                    && *value < 0
379                {
380                    faults.push(SchedulingFault::Duration {
381                        position: i + 1,
382                        value: *value,
383                    });
384                }
385            }
386        }
387        if self.tasks().ends.is_none()
388            && let Some(machines) = self.machines()
389        {
390            for (i, machine) in machines.iter().enumerate() {
391                if let Some(Value::Integer(value)) = machine.value(values)
392                    && usize::try_from(*value)
393                        .ok()
394                        .is_none_or(|index| index >= self.capacities().len())
395                {
396                    faults.push(SchedulingFault::Machine {
397                        position: i + 1,
398                        value: *value,
399                    });
400                }
401            }
402        }
403        faults
404    }
405    fn relation_operands(&self, values: &[Option<Value>]) -> Vec<&Operand> {
406        let tasks = self.tasks();
407        if let Some(ends) = &tasks.ends {
408            return self.explicit_operands(ends, values);
409        }
410        if matches!(self, Self::Cumulative { .. }) {
411            return match tasks.starts.len() {
412                0 => Vec::new(),
413                1 => {
414                    let mut required = vec![&tasks.durations[0]];
415                    if tasks.durations[0]
416                        .value(values)
417                        .is_some_and(|value| value.integer() > 0)
418                    {
419                        required.extend(self.usages().into_iter().flatten());
420                        required.extend(self.capacities());
421                    }
422                    required
423                }
424                _ => self.operands(),
425            };
426        }
427        let mut required = Vec::new();
428        required.extend(self.capacities());
429        required.extend(self.presence().into_iter().flatten());
430        for i in 0..tasks.starts.len() {
431            if self
432                .presence()
433                .is_none_or(|present| present[i].value(values).is_some_and(Value::boolean))
434            {
435                required.extend([&tasks.starts[i], &tasks.durations[i]]);
436                if let Some(usages) = self.usages() {
437                    required.push(&usages[i]);
438                }
439                if let Some(machines) = self.machines() {
440                    required.push(&machines[i]);
441                }
442            }
443        }
444        required
445    }
446    fn explicit_operands<'a>(
447        &'a self,
448        ends: &'a [Operand],
449        values: &[Option<Value>],
450    ) -> Vec<&'a Operand> {
451        let tasks = self.tasks();
452        let mut required = Vec::new();
453        required.extend(self.capacities());
454        required.extend(self.presence().into_iter().flatten());
455        if self.machines().is_none() {
456            // Native unary/cumulative require nonnegative durations even for absent tasks.
457            required.extend(&tasks.durations);
458        }
459        for (i, end) in ends.iter().enumerate() {
460            if self
461                .presence()
462                .is_some_and(|present| !present[i].value(values).is_some_and(Value::boolean))
463            {
464                continue;
465            }
466            if let Some(machines) = self.machines() {
467                required.push(&machines[i]);
468                if machines[i]
469                    .value(values)
470                    .is_some_and(|value| !self.valid_machine(value.integer()))
471                {
472                    continue;
473                }
474            }
475            required.extend([&tasks.starts[i], end]);
476            if self.machines().is_some()
477                && tasks.starts[i]
478                    .value(values)
479                    .zip(end.value(values))
480                    .is_some_and(|(start, end)| start.integer() >= end.integer())
481            {
482                continue;
483            }
484            if let Some(usages) = self.usages() {
485                required.push(&usages[i]);
486            }
487        }
488        required
489    }
490    fn valid_machine(&self, machine: i64) -> bool {
491        usize::try_from(machine).is_ok_and(|index| index < self.capacities().len())
492    }
493}
494
495struct ConcreteTask {
496    position: usize,
497    start: i64,
498    end: i64,
499    zero_duration: bool,
500    usage: i64,
501    machine: Option<i64>,
502}
503impl Scheduling {
504    fn concrete_tasks(
505        &self,
506        values: &[Option<Value>],
507        constraint: &Constraint,
508    ) -> Result<Vec<ConcreteTask>, CheckError> {
509        let tasks = self.tasks();
510        let integer = |operand: &Operand| {
511            operand
512                .value(values)
513                .expect("available schedule operand")
514                .integer()
515        };
516        let mut concrete = Vec::new();
517        for i in 0..tasks.starts.len() {
518            if self.presence().is_some_and(|present| {
519                !present[i]
520                    .value(values)
521                    .expect("available presence")
522                    .boolean()
523            }) {
524                continue;
525            }
526            let machine = self.machines().map(|machines| integer(&machines[i]));
527            if let Some(machine) = machine
528                && !self.valid_machine(machine)
529            {
530                continue;
531            }
532            let duration = tasks.durations[i].value(values).map(Value::integer);
533            if duration.is_some_and(|duration| duration < 0)
534                && !matches!(
535                    self.name(),
536                    SchedulingName::Cumulative | SchedulingName::CumulativesFlex
537                )
538            {
539                continue;
540            }
541            let start = integer(&tasks.starts[i]);
542            let end = if let Some(ends) = &tasks.ends {
543                integer(&ends[i])
544            } else {
545                start
546                    .checked_add(duration.expect("available legacy duration"))
547                    .ok_or_else(|| {
548                        constraint.execution_error("checked i64 overflow in scheduling endpoint")
549                    })?
550            };
551            if machine.is_some() && tasks.ends.is_some() && start >= end {
552                continue;
553            }
554            concrete.push(ConcreteTask {
555                position: i + 1,
556                start,
557                end,
558                zero_duration: duration == Some(0),
559                usage: self.usages().map_or(1, |usages| integer(&usages[i])),
560                machine,
561            });
562        }
563        Ok(concrete)
564    }
565    fn check_relation(
566        &self,
567        values: &[Option<Value>],
568        constraint: &Constraint,
569        faults: &mut Vec<SchedulingFault>,
570    ) -> Result<(), CheckError> {
571        let tasks = self.tasks();
572        let integer = |operand: &Operand| {
573            operand
574                .value(values)
575                .expect("available schedule operand")
576                .integer()
577        };
578        if self.name() == SchedulingName::Cumulative && tasks.starts.len() <= 1 {
579            if tasks.starts.len() == 1 && integer(&tasks.durations[0]) > 0 {
580                let usage = integer(&self.usages().expect("cumulative usages")[0]);
581                let bound = integer(&self.capacities()[0]);
582                if usage > bound {
583                    faults.push(SchedulingFault::TaskUsage {
584                        position: 1,
585                        usage,
586                        bound,
587                    });
588                }
589            }
590        } else {
591            let concrete = self.concrete_tasks(values, constraint)?;
592            match self {
593                Self::Strict(_) | Self::Unary { .. } => {
594                    for (i, first) in concrete.iter().enumerate() {
595                        for second in &concrete[i + 1..] {
596                            if first.end > second.start && second.end > first.start {
597                                faults.push(SchedulingFault::Conflict {
598                                    first: first.position,
599                                    second: second.position,
600                                });
601                            }
602                        }
603                    }
604                }
605                Self::OptionalCumulative { capacity, .. } => {
606                    let bound = integer(capacity);
607                    if bound >= 0 {
608                        check_resources(
609                            &concrete,
610                            ResourceLimit {
611                                machine: None,
612                                bound,
613                                upper: true,
614                            },
615                            if tasks.ends.is_some() {
616                                ResourceChecks::TaskStarts
617                            } else {
618                                ResourceChecks::ZeroDurationStarts
619                            },
620                            constraint,
621                            faults,
622                        )?;
623                    }
624                }
625                Self::Cumulative { capacity, .. } => {
626                    let bound = integer(capacity);
627                    if tasks.ends.is_none() || bound >= 0 {
628                        check_resources(
629                            &concrete,
630                            ResourceLimit {
631                                machine: None,
632                                bound,
633                                upper: true,
634                            },
635                            if tasks.ends.is_some() {
636                                ResourceChecks::TaskStarts
637                            } else {
638                                ResourceChecks::ConcreteHorizon
639                            },
640                            constraint,
641                            faults,
642                        )?;
643                    }
644                }
645                Self::Cumulatives {
646                    capacities, upper, ..
647                } => {
648                    for (machine, capacity) in capacities.iter().enumerate() {
649                        let machine = i64::try_from(machine)
650                            .map_err(|_| constraint.execution_error("machine index exceeds i64"))?;
651                        check_resources(
652                            &concrete,
653                            ResourceLimit {
654                                machine: Some(machine),
655                                bound: integer(capacity),
656                                upper: *upper,
657                            },
658                            ResourceChecks::ActiveIntervals,
659                            constraint,
660                            faults,
661                        )?;
662                    }
663                }
664            }
665        }
666        Ok(())
667    }
668}
669impl PreparedModel {
670    pub(crate) fn evaluate_schedule(
671        &self,
672        constraint: &Constraint,
673        schedule: &Scheduling,
674        values: &[Option<Value>],
675    ) -> Result<Option<ViolationKind>, CheckError> {
676        let mut faults = schedule.precondition_faults(values);
677        if schedule
678            .relation_operands(values)
679            .iter()
680            .all(|operand| operand.value(values).is_some())
681        {
682            schedule.check_relation(values, constraint, &mut faults)?;
683        }
684        let tasks = schedule.tasks();
685        if faults.is_empty() {
686            return Ok(None);
687        }
688        let argument = |operand: &Operand| self.argument(operand, values);
689        let tasks = (0..tasks.starts.len())
690            .map(|i| SchedulingTask {
691                position: i + 1,
692                start: argument(&tasks.starts[i]),
693                duration: argument(&tasks.durations[i]),
694                end: tasks.ends.as_ref().and_then(|ends| argument(&ends[i])),
695                usage: schedule.usages().and_then(|usages| argument(&usages[i])),
696                present: schedule
697                    .presence()
698                    .and_then(|present| argument(&present[i])),
699                machine: schedule
700                    .machines()
701                    .and_then(|machines| argument(&machines[i])),
702            })
703            .collect();
704        Ok(Some(ViolationKind::Scheduling {
705            finding: SchedulingFinding {
706                tasks,
707                capacities: schedule.capacities().iter().filter_map(argument).collect(),
708                faults,
709            },
710        }))
711    }
712}
713
714#[derive(Clone, Copy)]
715struct ResourceLimit {
716    machine: Option<i64>,
717    bound: i64,
718    upper: bool,
719}
720#[derive(Clone, Copy, PartialEq, Eq)]
721enum ResourceChecks {
722    ActiveIntervals,
723    ConcreteHorizon,
724    ZeroDurationStarts,
725    TaskStarts,
726}
727
728// Values are constant between boundaries, so even huge horizons need only task events.
729fn check_resources(
730    tasks: &[ConcreteTask],
731    limit: ResourceLimit,
732    checks: ResourceChecks,
733    constraint: &Constraint,
734    faults: &mut Vec<SchedulingFault>,
735) -> Result<(), CheckError> {
736    let tasks: Vec<_> = tasks
737        .iter()
738        .filter(|task| task.machine == limit.machine)
739        .collect();
740    let mut boundaries: Vec<_> = tasks
741        .iter()
742        .flat_map(|task| [task.start, task.end])
743        .collect();
744    boundaries.sort_unstable();
745    boundaries.dedup();
746    let horizon_end = tasks.iter().map(|task| task.end).max();
747    let horizon_start = tasks.iter().map(|task| task.start).min();
748    for time in boundaries {
749        let active: Vec<_> = tasks
750            .iter()
751            .copied()
752            .filter(|task| task.start <= time && time < task.end)
753            .collect();
754        if !active.is_empty()
755            || (checks == ResourceChecks::ConcreteHorizon
756                && horizon_start.is_some_and(|start| start <= time)
757                && horizon_end.is_some_and(|end| time < end))
758        {
759            check_resource(&active, None, limit, time, constraint, faults)?;
760        }
761        if matches!(
762            checks,
763            ResourceChecks::ZeroDurationStarts | ResourceChecks::TaskStarts
764        ) {
765            let spanning: Vec<_> = tasks
766                .iter()
767                .copied()
768                .filter(|task| task.start < time && time < task.end)
769                .collect();
770            for starting in tasks.iter().copied().filter(|task| {
771                task.start == time && (checks == ResourceChecks::TaskStarts || task.zero_duration)
772            }) {
773                check_resource(&spanning, Some(starting), limit, time, constraint, faults)?;
774            }
775        }
776    }
777    Ok(())
778}
779fn check_resource(
780    active: &[&ConcreteTask],
781    starting: Option<&ConcreteTask>,
782    limit: ResourceLimit,
783    time: i64,
784    constraint: &Constraint,
785    faults: &mut Vec<SchedulingFault>,
786) -> Result<(), CheckError> {
787    let usage = checked_sum(
788        active
789            .iter()
790            .copied()
791            .chain(starting)
792            .map(|task| (1, task.usage)),
793    )
794    .ok_or_else(|| constraint.execution_error("checked i64 overflow in scheduling resource sum"))?;
795    let ResourceLimit {
796        machine,
797        bound,
798        upper,
799    } = limit;
800    if (upper && usage > bound) || (!upper && usage < bound) {
801        faults.push(SchedulingFault::Resource {
802            machine,
803            time,
804            usage,
805            bound,
806            upper,
807            tasks: active
808                .iter()
809                .copied()
810                .chain(starting)
811                .map(|task| task.position)
812                .collect(),
813        });
814    }
815    Ok(())
816}