Skip to main content

rustsim_core/
scheduler.rs

1//! Scheduler trait and built-in implementations.
2//!
3//! Schedulers control the order in which agents are activated each step.
4//! The built-in schedulers mirror Julia Agents.jl `Schedulers.*`:
5//!
6//! | Rust type | Julia equivalent | Behavior |
7//! |-----------|-----------------|----------|
8//! | [`Fastest`] | `Schedulers.fastest` | Iteration order (no sorting) |
9//! | [`ById`] | `Schedulers.ByID` | Sorted ascending by agent ID |
10//! | [`Randomly`] | `Schedulers.Randomly` | Shuffled randomly each step |
11//! | [`ByProperty`] | `Schedulers.ByProperty` | Sorted by a user-defined key (greatest first) |
12//! | [`PartiallyRandom`] | `Schedulers.Partially` | Random subset with a given probability |
13//!
14//! # Determinism notes
15//!
16//! Deterministic replay depends on both the scheduler and the order of agent IDs
17//! collected from the store.
18//!
19//! - [`ById`] imposes an explicit deterministic order.
20//! - [`Fastest`] is deterministic only if the underlying store iteration order is deterministic.
21//! - [`Randomly`] and [`PartiallyRandom`] are reproducible for a fixed RNG seed only when the
22//!   pre-randomization ID collection order is itself deterministic.
23//! - [`ByProperty`] is reproducible when the selected key is deterministic and either ties do not
24//!   occur or the source ID order is deterministic.
25
26use crate::types::AgentId;
27use rand::seq::SliceRandom;
28use rand::Rng;
29
30use crate::{model::Model, standard::HasAgentIds};
31
32/// Trait for agent activation schedulers.
33///
34/// Each step, the model calls [`schedule_into`] to populate a buffer with
35/// the agent IDs to activate, in the desired order. The buffer is cleared
36/// by the caller before being passed in.
37///
38/// Schedulers are stateless - the buffer is owned by the model and reused
39/// across steps to avoid per-step allocations.
40///
41/// [`schedule_into`]: Scheduler::schedule_into
42pub trait Scheduler<M> {
43    /// Fill `buf` with the agent IDs to activate this step, in the desired order.
44    ///
45    /// The buffer is cleared by the caller before this method is invoked.
46    fn schedule_into(&mut self, model: &M, buf: &mut Vec<AgentId>);
47}
48
49/// Fastest scheduler - returns agents in store iteration order.
50///
51/// No sorting or shuffling is performed. This is the cheapest scheduler
52/// and is appropriate when activation order does not matter.
53///
54/// Reproducibility inherits whatever ordering guarantees the underlying store
55/// provides.
56#[derive(Debug, Default)]
57pub struct Fastest;
58
59impl Fastest {
60    pub const fn new() -> Self {
61        Self
62    }
63}
64
65impl<M> Scheduler<M> for Fastest
66where
67    M: HasAgentIds,
68{
69    fn schedule_into(&mut self, model: &M, buf: &mut Vec<AgentId>) {
70        model.agent_ids_into(buf);
71    }
72}
73
74/// ID-ordered scheduler - returns agents sorted ascending by [`AgentId`].
75///
76/// Produces a deterministic, reproducible activation order regardless of
77/// store internals.
78#[derive(Debug, Default)]
79pub struct ById;
80
81impl ById {
82    pub fn new() -> Self {
83        Self
84    }
85}
86
87impl<M> Scheduler<M> for ById
88where
89    M: HasAgentIds,
90{
91    fn schedule_into(&mut self, model: &M, buf: &mut Vec<AgentId>) {
92        model.agent_ids_into(buf);
93        buf.sort_unstable();
94    }
95}
96
97/// Random scheduler - shuffles agent IDs each step using the model's RNG.
98///
99/// Produces a different activation order every step while remaining
100/// reproducible for a given RNG seed **when the collected source ID order is
101/// deterministic**.
102#[derive(Debug, Default)]
103pub struct Randomly;
104
105impl Randomly {
106    pub fn new() -> Self {
107        Self
108    }
109}
110
111impl<M> Scheduler<M> for Randomly
112where
113    M: HasAgentIds + Model,
114{
115    fn schedule_into(&mut self, model: &M, buf: &mut Vec<AgentId>) {
116        model.agent_ids_into(buf);
117        buf.shuffle(&mut *model.rng_mut());
118    }
119}
120
121/// Partial random scheduler - activates a random subset of agents each step.
122///
123/// Each agent is independently included with probability `p`. Useful for
124/// modelling intermittent activity or sampling.
125///
126/// Reproducibility for a fixed RNG seed still depends on deterministic source
127/// ID enumeration before filtering.
128#[derive(Debug)]
129pub struct PartiallyRandom {
130    probability: f64,
131}
132
133impl PartiallyRandom {
134    /// Create a new `PartiallyRandom` scheduler with the given inclusion probability.
135    ///
136    /// # Panics
137    ///
138    /// Does not validate `probability` - values outside `[0, 1]` will produce
139    /// unexpected results from [`rand::Rng::gen_bool`].
140    pub fn new(probability: f64) -> Self {
141        Self { probability }
142    }
143}
144
145impl<M> Scheduler<M> for PartiallyRandom
146where
147    M: HasAgentIds + Model,
148{
149    fn schedule_into(&mut self, model: &M, buf: &mut Vec<AgentId>) {
150        model.agent_ids_into(buf);
151        let mut rng = model.rng_mut();
152        buf.retain(|_| rng.gen_bool(self.probability));
153    }
154}
155
156/// Property-sorted scheduler - activates agents sorted by a user-defined key.
157///
158/// The `selector` closure extracts an [`Ord`]-comparable key from each agent.
159/// Agents with the **greatest** key value are activated first (descending order).
160///
161/// When multiple agents produce the same key, reproducibility falls back to the
162/// relative order already present in the collected ID buffer.
163///
164/// # Example
165///
166/// ```ignore
167/// // Activate highest-energy agents first
168/// let sched = ByProperty::new(|a: &MyAgent| a.energy);
169/// ```
170#[derive(Debug)]
171pub struct ByProperty<F> {
172    selector: F,
173}
174
175impl<F> ByProperty<F> {
176    /// Create a new `ByProperty` scheduler with the given key selector.
177    pub fn new(selector: F) -> Self {
178        Self { selector }
179    }
180}
181
182impl<M, F, K> Scheduler<M> for ByProperty<F>
183where
184    M: HasAgentIds + Model,
185    F: Fn(&M::Agent) -> K,
186    K: Ord,
187{
188    fn schedule_into(&mut self, model: &M, buf: &mut Vec<AgentId>) {
189        model.agent_ids_into(buf);
190
191        // Drop any IDs that vanished since collection.
192        buf.retain(|id| model.agent(*id).is_some());
193
194        let selector = &self.selector;
195        buf.sort_by(|a, b| {
196            let agent_a = model.agent(*a);
197            let agent_b = model.agent(*b);
198            match (agent_a, agent_b) {
199                (Some(agent_a), Some(agent_b)) => {
200                    let ka = selector(&agent_a);
201                    let kb = selector(&agent_b);
202                    kb.cmp(&ka)
203                }
204                (Some(_), None) => std::cmp::Ordering::Less,
205                (None, Some(_)) => std::cmp::Ordering::Greater,
206                (None, None) => std::cmp::Ordering::Equal,
207            }
208        });
209    }
210}