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::ScoreDirector;
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///
13/// # Implementation Notes
14///
15/// When implementing `ProblemChange`:
16/// - Use `score_director.working_solution_mut()` to access and modify the solution
17/// - Call `score_director.trigger_variable_listeners()` after modifications
18/// - Changes should be idempotent when possible
19/// - Avoid holding references to entities across changes
20///
21/// # Example
22///
23/// ```
24/// use solverforge_solver::realtime::ProblemChange;
25/// use solverforge_scoring::ScoreDirector;
26/// use solverforge_core::domain::PlanningSolution;
27/// use solverforge_core::score::SimpleScore;
28///
29/// #[derive(Clone, Debug)]
30/// struct Employee { id: usize, shift: Option<i32> }
31///
32/// #[derive(Clone, Debug)]
33/// struct Schedule {
34/// employees: Vec<Employee>,
35/// score: Option<SimpleScore>,
36/// }
37///
38/// impl PlanningSolution for Schedule {
39/// type Score = SimpleScore;
40/// fn score(&self) -> Option<Self::Score> { self.score }
41/// fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
42/// }
43///
44/// /// Adds a new employee to the schedule.
45/// #[derive(Debug)]
46/// struct AddEmployee {
47/// employee_id: usize,
48/// }
49///
50/// impl ProblemChange<Schedule> for AddEmployee {
51/// fn apply(&self, score_director: &mut dyn ScoreDirector<Schedule>) {
52/// // Add the new employee
53/// score_director.working_solution_mut().employees.push(Employee {
54/// id: self.employee_id,
55/// shift: None,
56/// });
57///
58/// // Notify the solver of the change
59/// score_director.trigger_variable_listeners();
60/// }
61/// }
62///
63/// /// Removes an employee from the schedule.
64/// #[derive(Debug)]
65/// struct RemoveEmployee {
66/// employee_id: usize,
67/// }
68///
69/// impl ProblemChange<Schedule> for RemoveEmployee {
70/// fn apply(&self, score_director: &mut dyn ScoreDirector<Schedule>) {
71/// // Remove the employee
72/// let id = self.employee_id;
73/// score_director.working_solution_mut().employees.retain(|e| e.id != id);
74///
75/// // Notify the solver of the change
76/// score_director.trigger_variable_listeners();
77/// }
78/// }
79/// ```
80pub trait ProblemChange<S: PlanningSolution>: Send + Debug {
81 /// Applies this change to the working solution.
82 ///
83 /// This method is called by the solver at a safe point (between steps).
84 /// Access the working solution via `score_director.working_solution_mut()`.
85 ///
86 /// After making changes, call `score_director.trigger_variable_listeners()`
87 /// to ensure shadow variables and constraints are updated.
88 fn apply(&self, score_director: &mut dyn ScoreDirector<S>);
89}
90
91/// A boxed problem change for type-erased storage.
92pub type BoxedProblemChange<S> = Box<dyn ProblemChange<S>>;
93
94/// A problem change implemented as a closure.
95///
96/// This is a convenience wrapper for simple changes that don't need
97/// a dedicated struct.
98///
99/// # Example
100///
101/// ```
102/// use solverforge_solver::realtime::ClosureProblemChange;
103/// use solverforge_scoring::ScoreDirector;
104/// use solverforge_core::domain::PlanningSolution;
105/// use solverforge_core::score::SimpleScore;
106///
107/// #[derive(Clone, Debug)]
108/// struct Task { id: usize, done: bool }
109///
110/// #[derive(Clone, Debug)]
111/// struct Solution {
112/// tasks: Vec<Task>,
113/// score: Option<SimpleScore>,
114/// }
115///
116/// impl PlanningSolution for Solution {
117/// type Score = SimpleScore;
118/// fn score(&self) -> Option<Self::Score> { self.score }
119/// fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
120/// }
121///
122/// // Mark task 0 as done
123/// let change = ClosureProblemChange::<Solution, _>::new("mark_task_done", |sd| {
124/// if let Some(task) = sd.working_solution_mut().tasks.get_mut(0) {
125/// task.done = true;
126/// }
127/// sd.trigger_variable_listeners();
128/// });
129/// ```
130pub struct ClosureProblemChange<S: PlanningSolution, F>
131where
132 F: Fn(&mut dyn ScoreDirector<S>) + Send,
133{
134 name: &'static str,
135 change_fn: F,
136 _phantom: std::marker::PhantomData<S>,
137}
138
139impl<S, F> ClosureProblemChange<S, F>
140where
141 S: PlanningSolution,
142 F: Fn(&mut dyn ScoreDirector<S>) + Send,
143{
144 /// Creates a new closure-based problem change.
145 ///
146 /// # Arguments
147 /// * `name` - A descriptive name for debugging
148 /// * `change_fn` - The closure that applies the change
149 pub fn new(name: &'static str, change_fn: F) -> Self {
150 Self {
151 name,
152 change_fn,
153 _phantom: std::marker::PhantomData,
154 }
155 }
156}
157
158impl<S, F> Debug for ClosureProblemChange<S, F>
159where
160 S: PlanningSolution,
161 F: Fn(&mut dyn ScoreDirector<S>) + Send,
162{
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 f.debug_struct("ClosureProblemChange")
165 .field("name", &self.name)
166 .finish()
167 }
168}
169
170impl<S, F> ProblemChange<S> for ClosureProblemChange<S, F>
171where
172 S: PlanningSolution,
173 F: Fn(&mut dyn ScoreDirector<S>) + Send,
174{
175 fn apply(&self, score_director: &mut dyn ScoreDirector<S>) {
176 (self.change_fn)(score_director);
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
184 use solverforge_core::score::SimpleScore;
185 use solverforge_scoring::SimpleScoreDirector;
186 use std::any::TypeId;
187
188 #[derive(Clone, Debug)]
189 struct Task {
190 id: usize,
191 }
192
193 #[derive(Clone, Debug)]
194 struct TaskSchedule {
195 tasks: Vec<Task>,
196 score: Option<SimpleScore>,
197 }
198
199 impl PlanningSolution for TaskSchedule {
200 type Score = SimpleScore;
201 fn score(&self) -> Option<Self::Score> {
202 self.score
203 }
204 fn set_score(&mut self, score: Option<Self::Score>) {
205 self.score = score;
206 }
207 }
208
209 fn get_tasks(s: &TaskSchedule) -> &Vec<Task> {
210 &s.tasks
211 }
212 fn get_tasks_mut(s: &mut TaskSchedule) -> &mut Vec<Task> {
213 &mut s.tasks
214 }
215
216 fn create_director(
217 tasks: Vec<Task>,
218 ) -> SimpleScoreDirector<TaskSchedule, impl Fn(&TaskSchedule) -> SimpleScore> {
219 let solution = TaskSchedule { tasks, score: None };
220 let extractor = Box::new(TypedEntityExtractor::new(
221 "Task",
222 "tasks",
223 get_tasks,
224 get_tasks_mut,
225 ));
226 let entity_desc =
227 EntityDescriptor::new("Task", TypeId::of::<Task>(), "tasks").with_extractor(extractor);
228 let descriptor = SolutionDescriptor::new("TaskSchedule", TypeId::of::<TaskSchedule>())
229 .with_entity(entity_desc);
230 SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
231 }
232
233 #[derive(Debug)]
234 struct AddTask {
235 id: usize,
236 }
237
238 impl ProblemChange<TaskSchedule> for AddTask {
239 fn apply(&self, score_director: &mut dyn ScoreDirector<TaskSchedule>) {
240 score_director
241 .working_solution_mut()
242 .tasks
243 .push(Task { id: self.id });
244 score_director.trigger_variable_listeners();
245 }
246 }
247
248 #[test]
249 fn struct_problem_change() {
250 let mut director = create_director(vec![Task { id: 0 }]);
251
252 let change = AddTask { id: 1 };
253 change.apply(&mut director);
254
255 assert_eq!(director.working_solution().tasks.len(), 2);
256 assert_eq!(director.working_solution().tasks[1].id, 1);
257 }
258
259 #[test]
260 fn closure_problem_change() {
261 let mut director = create_director(vec![Task { id: 0 }]);
262
263 let change = ClosureProblemChange::<TaskSchedule, _>::new("remove_all", |sd| {
264 sd.working_solution_mut().tasks.clear();
265 sd.trigger_variable_listeners();
266 });
267
268 change.apply(&mut director);
269
270 assert!(director.working_solution().tasks.is_empty());
271 }
272}