Skip to main content

solverforge_solver/realtime/
problem_change.rs

1// Problem change trait for real-time planning.
2
3use std::fmt::Debug;
4
5use solverforge_core::domain::PlanningSolution;
6use solverforge_scoring::Director;
7
8/// A change to the problem that can be applied during solving.
9///
10/// Problem changes allow modifying the solution while the solver is running.
11/// Changes are queued and processed at step boundaries to maintain consistency.
12/// The solver applies each change through its committed mutation boundary so
13/// optional-construction frontier revisions stay coherent.
14///
15/// # Implementation Notes
16///
17/// When implementing `ProblemChange`:
18/// - Use `score_director.working_solution_mut()` to access and modify the solution
19/// - Changes should be idempotent when possible
20/// - Avoid holding references to entities across changes
21///
22/// # Example
23///
24/// ```
25/// use solverforge_solver::realtime::ProblemChange;
26/// use solverforge_scoring::Director;
27/// use solverforge_core::domain::PlanningSolution;
28/// use solverforge_core::score::SoftScore;
29///
30/// #[derive(Clone, Debug)]
31/// struct Employee { id: usize, shift: Option<i32> }
32///
33/// #[derive(Clone, Debug)]
34/// struct Schedule {
35///     employees: Vec<Employee>,
36///     score: Option<SoftScore>,
37/// }
38///
39/// impl PlanningSolution for Schedule {
40///     type Score = SoftScore;
41///     fn score(&self) -> Option<Self::Score> { self.score }
42///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
43/// }
44///
45// /// Adds a new employee to the schedule.
46/// #[derive(Debug)]
47/// struct AddEmployee {
48///     employee_id: usize,
49/// }
50///
51/// impl ProblemChange<Schedule> for AddEmployee {
52///     fn apply(&self, score_director: &mut dyn Director<Schedule>) {
53///         // Add the new employee
54///         score_director.working_solution_mut().employees.push(Employee {
55///             id: self.employee_id,
56///             shift: None,
57///         });
58///
59///     }
60/// }
61///
62/// /// Removes an employee from the schedule.
63/// #[derive(Debug)]
64/// struct RemoveEmployee {
65///     employee_id: usize,
66/// }
67///
68/// impl ProblemChange<Schedule> for RemoveEmployee {
69///     fn apply(&self, score_director: &mut dyn Director<Schedule>) {
70///         // Remove the employee
71///         let id = self.employee_id;
72///         score_director.working_solution_mut().employees.retain(|e| e.id != id);
73///     }
74/// }
75/// ```
76pub trait ProblemChange<S: PlanningSolution>: Send + Debug {
77    /* Applies this change to the working solution.
78
79    This method is called by the solver at a safe point (between steps).
80    Access the working solution via `score_director.working_solution_mut()`.
81
82    */
83    fn apply(&self, score_director: &mut dyn Director<S>);
84}
85
86/// A boxed problem change for type-erased storage.
87pub type BoxedProblemChange<S> = Box<dyn ProblemChange<S>>;
88
89/// A problem change implemented as a closure.
90///
91/// This is a convenience wrapper for simple changes that don't need
92/// a dedicated struct.
93///
94/// # Example
95///
96/// ```
97/// use solverforge_solver::realtime::ClosureProblemChange;
98/// use solverforge_scoring::Director;
99/// use solverforge_core::domain::PlanningSolution;
100/// use solverforge_core::score::SoftScore;
101///
102/// #[derive(Clone, Debug)]
103/// struct Task { id: usize, done: bool }
104///
105/// #[derive(Clone, Debug)]
106/// struct Solution {
107///     tasks: Vec<Task>,
108///     score: Option<SoftScore>,
109/// }
110///
111/// impl PlanningSolution for Solution {
112///     type Score = SoftScore;
113///     fn score(&self) -> Option<Self::Score> { self.score }
114///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
115/// }
116///
117/// // Mark task 0 as done
118/// let change = ClosureProblemChange::<Solution, _>::new("mark_task_done", |sd| {
119///     if let Some(task) = sd.working_solution_mut().tasks.get_mut(0) {
120///         task.done = true;
121///     }
122/// });
123/// ```
124pub struct ClosureProblemChange<S: PlanningSolution, F>
125where
126    F: Fn(&mut dyn Director<S>) + Send,
127{
128    name: &'static str,
129    change_fn: F,
130    _phantom: std::marker::PhantomData<fn() -> S>,
131}
132
133impl<S, F> ClosureProblemChange<S, F>
134where
135    S: PlanningSolution,
136    F: Fn(&mut dyn Director<S>) + Send,
137{
138    /// Creates a new closure-based problem change.
139    ///
140    /// # Arguments
141    /// * `name` - A descriptive name for debugging
142    /// * `change_fn` - The closure that applies the change
143    pub fn new(name: &'static str, change_fn: F) -> Self {
144        Self {
145            name,
146            change_fn,
147            _phantom: std::marker::PhantomData,
148        }
149    }
150}
151
152impl<S, F> Debug for ClosureProblemChange<S, F>
153where
154    S: PlanningSolution,
155    F: Fn(&mut dyn Director<S>) + Send,
156{
157    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158        f.debug_struct("ClosureProblemChange")
159            .field("name", &self.name)
160            .finish()
161    }
162}
163
164impl<S, F> ProblemChange<S> for ClosureProblemChange<S, F>
165where
166    S: PlanningSolution,
167    F: Fn(&mut dyn Director<S>) + Send,
168{
169    fn apply(&self, score_director: &mut dyn Director<S>) {
170        (self.change_fn)(score_director);
171    }
172}
173
174#[cfg(test)]
175#[path = "problem_change_tests.rs"]
176mod tests;