Skip to main content

dpm_schedule/
placement.rs

1//! Calendars of one remaining projection, compiled once for every pass over the network.
2//!
3//! Each activity executes on the calendar its work resolves to, and a task still awaiting
4//! verification finishes only when the verifier kind's calendar is open. Calendars are compiled
5//! over a window of hours after the clock reading; a pass that leaves the window is repeated on a
6//! wider one, so results never depend on the window chosen.
7
8use crate::network::Network;
9use crate::{RemainingOptions, ScheduleError};
10use dpm_model::{
11    BeyondCalendar, Calendars, MAX_WINDOW_HOURS, Plan, ResolvedCalendar, Timeline, WorkItemId,
12    WorkingTime,
13};
14use std::collections::{BTreeMap, BTreeSet};
15
16/// Wider windows tried before a projection gives up; each doubles the span, and no window
17/// exceeds the calendar's maximum.
18const WIDENINGS: usize = 24;
19
20/// Compiled calendars and each activity's execution and review calendars.
21#[derive(Debug)]
22pub(crate) struct Placement {
23    calendars: Calendars,
24    origin: chrono::DateTime<chrono::Utc>,
25    window: (f64, f64),
26    names: Vec<String>,
27    compiled: Vec<WorkingTime>,
28    /// Execution calendar index by topological position.
29    execution: Vec<usize>,
30    /// Verifier calendar index by topological position, for tasks that still await verification.
31    review: Vec<Option<usize>>,
32    /// Resolved calendar of each executable task, for reports.
33    pub(crate) resolved: BTreeMap<WorkItemId, ResolvedCalendar>,
34    /// Elapsed hours a review waits after the work ends, before the verifier's calendar applies.
35    review_delay: f64,
36}
37
38impl Placement {
39    /// Placement of a remaining projection, or `None` for a plan without calendars that needs
40    /// no review delay.
41    ///
42    /// `awaiting` lists the tasks whose verification the projection still waits for. A review
43    /// delay in a plan without calendars places every activity on the `always` calendar, which
44    /// reproduces the elapsed projection apart from the delay.
45    pub(crate) fn new(
46        plan: &Plan,
47        network: &Network,
48        timeline: &Timeline,
49        awaiting: &BTreeSet<WorkItemId>,
50        horizon: f64,
51        options: RemainingOptions,
52    ) -> Result<Option<Self>, ScheduleError> {
53        let (calendars, reported) = match (&plan.calendars, options.delays_review()) {
54            (Some(calendars), _) => (calendars.clone(), true),
55            (None, true) => (elapsed_calendars(), false),
56            (None, false) => return Ok(None),
57        };
58        let mut names: Vec<String> = Vec::new();
59        let mut index = |name: &str| match names.iter().position(|n| n == name) {
60            Some(found) => found,
61            None => {
62                names.push(name.to_owned());
63                names.len() - 1
64            }
65        };
66        let verifier = index(calendars.verifier_calendar());
67        let mut execution = Vec::with_capacity(network.order().len());
68        let mut review = Vec::with_capacity(network.order().len());
69        let mut resolved = BTreeMap::new();
70        for id in network.order() {
71            let work = plan
72                .work_items
73                .get(id)
74                .ok_or(ScheduleError::MissingWorkItem(*id))?;
75            let calendar = calendars.resolve(work);
76            execution.push(index(&calendar.calendar));
77            review.push(awaiting.contains(id).then_some(verifier));
78            if work.is_executable() && reported {
79                resolved.insert(*id, calendar);
80            }
81        }
82        let mut placement = Self {
83            calendars,
84            origin: timeline.now(),
85            window: (
86                -24.0 * 14.0,
87                horizon.clamp(24.0 * 7.0 * 8.0, MAX_WINDOW_HOURS / 2.0),
88            ),
89            names,
90            compiled: Vec::new(),
91            execution,
92            review,
93            resolved,
94            review_delay: options.review_delay_hours,
95        };
96        placement.compile()?;
97        Ok(Some(placement))
98    }
99
100    fn compile(&mut self) -> Result<(), ScheduleError> {
101        let zone = self
102            .calendars
103            .zone()
104            .map_err(|_| ScheduleError::CalendarRange)?;
105        self.compiled = self
106            .names
107            .iter()
108            .map(|name| {
109                let rules = self
110                    .calendars
111                    .rules(name)
112                    .ok_or(ScheduleError::CalendarRange)?;
113                WorkingTime::compile(rules, zone, self.origin, self.window.0, self.window.1)
114                    .ok_or(ScheduleError::CalendarRange)
115            })
116            .collect::<Result<_, _>>()?;
117        Ok(())
118    }
119
120    /// Widen the window, mostly forwards where projections grow, and recompile; a window at the
121    /// maximum is out of range.
122    fn widen(&mut self) -> Result<(), ScheduleError> {
123        let span = self.window.1 - self.window.0;
124        if span >= MAX_WINDOW_HOURS {
125            return Err(ScheduleError::CalendarRange);
126        }
127        let grown = (span * 2.0).min(MAX_WINDOW_HOURS);
128        let before = (grown - span) / 4.0;
129        self.window = (
130            self.window.0 - before,
131            self.window.1 + (grown - span - before),
132        );
133        self.compile()
134    }
135
136    /// Run a pass, widening the window until the pass fits in it.
137    pub(crate) fn fit<T>(
138        &mut self,
139        mut pass: impl FnMut(&Self) -> Result<T, Placed>,
140    ) -> Result<T, ScheduleError> {
141        for _ in 0..WIDENINGS {
142            match pass(self) {
143                Ok(value) => return Ok(value),
144                Err(Placed::Beyond) => self.widen()?,
145                Err(Placed::Schedule(error)) => return Err(error),
146            }
147        }
148        Err(ScheduleError::CalendarRange)
149    }
150
151    fn calendar(&self, index: usize) -> Result<&WorkingTime, Placed> {
152        self.compiled
153            .get(index)
154            .ok_or(Placed::Schedule(ScheduleError::CalendarRange))
155    }
156
157    /// Execution calendar of the activity at a position.
158    pub(crate) fn execution(&self, position: usize) -> Result<&WorkingTime, Placed> {
159        let index = self
160            .execution
161            .get(position)
162            .ok_or(Placed::Schedule(ScheduleError::UnknownPosition(position)))?;
163        self.calendar(*index)
164    }
165
166    /// Elapsed hours a review waits after the work ends.
167    pub(crate) fn review_delay(&self) -> f64 {
168        self.review_delay
169    }
170
171    /// Whether the task at a position is reviewed on its own execution calendar with no delay,
172    /// so work ending at a working moment is already aligned for review.
173    pub(crate) fn reviewed_on_own_calendar(&self, position: usize) -> bool {
174        self.review_delay == 0.0
175            && matches!(
176                (self.review.get(position), self.execution.get(position)),
177                (Some(Some(review)), Some(execution)) if review == execution
178            )
179    }
180
181    /// Verifier calendar of a task at a position that still awaits verification.
182    pub(crate) fn review(&self, position: usize) -> Result<Option<&WorkingTime>, Placed> {
183        match self.review.get(position) {
184            Some(Some(index)) => self.calendar(*index).map(Some),
185            Some(None) => Ok(None),
186            None => Err(Placed::Schedule(ScheduleError::UnknownPosition(position))),
187        }
188    }
189}
190
191/// Failure of one pass: the window was too narrow, or the projection itself is invalid.
192#[derive(Debug)]
193pub(crate) enum Placed {
194    Beyond,
195    Schedule(ScheduleError),
196}
197
198impl From<BeyondCalendar> for Placed {
199    fn from(_: BeyondCalendar) -> Self {
200        Self::Beyond
201    }
202}
203
204impl From<ScheduleError> for Placed {
205    fn from(error: ScheduleError) -> Self {
206        Self::Schedule(error)
207    }
208}
209
210/// Calendars in which every kind works every hour, so placement adds nothing but a review delay.
211fn elapsed_calendars() -> Calendars {
212    let always = || dpm_model::ALWAYS.to_owned();
213    Calendars {
214        kinds: dpm_model::KindCalendars {
215            human: always(),
216            agent: always(),
217            service: always(),
218        },
219        ..Calendars::in_zone("UTC")
220    }
221}
222
223/// Hours between two instants of a task's work, such as its start and the clock reading: working
224/// hours of the task's calendar when the plan has calendars, elapsed hours otherwise; zero when
225/// `until` precedes `from`.
226///
227/// Remaining forecasts measure started work with it, and calibration measures recorded work with
228/// it, so both count the same hours.
229pub fn worked_between(
230    plan: &Plan,
231    work: &dpm_model::WorkItem,
232    started: chrono::DateTime<chrono::Utc>,
233    now: chrono::DateTime<chrono::Utc>,
234) -> Result<f64, ScheduleError> {
235    let hours = ((now - started).num_milliseconds() as f64 / 3_600_000.0).max(0.0);
236    let Some(calendars) = plan.calendars.as_ref() else {
237        return Ok(hours);
238    };
239    let rules = calendars
240        .rules(&calendars.resolve(work).calendar)
241        .ok_or(ScheduleError::CalendarRange)?;
242    let zone = calendars.zone().map_err(|_| ScheduleError::CalendarRange)?;
243    WorkingTime::compile(rules, zone, started, -24.0, hours + 24.0)
244        .ok_or(ScheduleError::CalendarRange)?
245        .between(0.0, hours)
246        .map_err(|_| ScheduleError::CalendarRange)
247}