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}