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}