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