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;