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///
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    */
81    fn apply(&self, score_director: &mut dyn Director<S>);
82}
83
84/// A boxed problem change for type-erased storage.
85pub type BoxedProblemChange<S> = Box<dyn ProblemChange<S>>;
86
87/// A problem change implemented as a closure.
88///
89/// This is a convenience wrapper for simple changes that don't need
90/// a dedicated struct.
91///
92/// # Example
93///
94/// ```
95/// use solverforge_solver::realtime::ClosureProblemChange;
96/// use solverforge_scoring::Director;
97/// use solverforge_core::domain::PlanningSolution;
98/// use solverforge_core::score::SoftScore;
99///
100/// #[derive(Clone, Debug)]
101/// struct Task { id: usize, done: bool }
102///
103/// #[derive(Clone, Debug)]
104/// struct Solution {
105///     tasks: Vec<Task>,
106///     score: Option<SoftScore>,
107/// }
108///
109/// impl PlanningSolution for Solution {
110///     type Score = SoftScore;
111///     fn score(&self) -> Option<Self::Score> { self.score }
112///     fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
113/// }
114///
115/// // Mark task 0 as done
116/// let change = ClosureProblemChange::<Solution, _>::new("mark_task_done", |sd| {
117///     if let Some(task) = sd.working_solution_mut().tasks.get_mut(0) {
118///         task.done = true;
119///     }
120/// });
121/// ```
122pub struct ClosureProblemChange<S: PlanningSolution, F>
123where
124    F: Fn(&mut dyn Director<S>) + Send,
125{
126    name: &'static str,
127    change_fn: F,
128    _phantom: std::marker::PhantomData<fn() -> S>,
129}
130
131impl<S, F> ClosureProblemChange<S, F>
132where
133    S: PlanningSolution,
134    F: Fn(&mut dyn Director<S>) + Send,
135{
136    /// Creates a new closure-based problem change.
137    ///
138    /// # Arguments
139    /// * `name` - A descriptive name for debugging
140    /// * `change_fn` - The closure that applies the change
141    pub fn new(name: &'static str, change_fn: F) -> Self {
142        Self {
143            name,
144            change_fn,
145            _phantom: std::marker::PhantomData,
146        }
147    }
148}
149
150impl<S, F> Debug for ClosureProblemChange<S, F>
151where
152    S: PlanningSolution,
153    F: Fn(&mut dyn Director<S>) + Send,
154{
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        f.debug_struct("ClosureProblemChange")
157            .field("name", &self.name)
158            .finish()
159    }
160}
161
162impl<S, F> ProblemChange<S> for ClosureProblemChange<S, F>
163where
164    S: PlanningSolution,
165    F: Fn(&mut dyn Director<S>) + Send,
166{
167    fn apply(&self, score_director: &mut dyn Director<S>) {
168        (self.change_fn)(score_director);
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use solverforge_core::domain::{
176        EntityCollectionExtractor, EntityDescriptor, SolutionDescriptor,
177    };
178    use solverforge_core::score::SoftScore;
179    use solverforge_scoring::ScoreDirector;
180    use std::any::TypeId;
181
182    #[derive(Clone, Debug)]
183    struct Task {
184        id: usize,
185    }
186
187    #[derive(Clone, Debug)]
188    struct TaskSchedule {
189        tasks: Vec<Task>,
190        score: Option<SoftScore>,
191    }
192
193    impl PlanningSolution for TaskSchedule {
194        type Score = SoftScore;
195        fn score(&self) -> Option<Self::Score> {
196            self.score
197        }
198        fn set_score(&mut self, score: Option<Self::Score>) {
199            self.score = score;
200        }
201    }
202
203    fn get_tasks(s: &TaskSchedule) -> &Vec<Task> {
204        &s.tasks
205    }
206    fn get_tasks_mut(s: &mut TaskSchedule) -> &mut Vec<Task> {
207        &mut s.tasks
208    }
209
210    fn create_director(tasks: Vec<Task>) -> ScoreDirector<TaskSchedule, ()> {
211        let solution = TaskSchedule { tasks, score: None };
212        let extractor = Box::new(EntityCollectionExtractor::new(
213            "Task",
214            "tasks",
215            get_tasks,
216            get_tasks_mut,
217        ));
218        let entity_desc =
219            EntityDescriptor::new("Task", TypeId::of::<Task>(), "tasks").with_extractor(extractor);
220        let descriptor = SolutionDescriptor::new("TaskSchedule", TypeId::of::<TaskSchedule>())
221            .with_entity(entity_desc);
222        ScoreDirector::simple(solution, descriptor, |s, _| s.tasks.len())
223    }
224
225    #[derive(Debug)]
226    struct AddTask {
227        id: usize,
228    }
229
230    impl ProblemChange<TaskSchedule> for AddTask {
231        fn apply(&self, score_director: &mut dyn Director<TaskSchedule>) {
232            score_director
233                .working_solution_mut()
234                .tasks
235                .push(Task { id: self.id });
236        }
237    }
238
239    #[test]
240    fn struct_problem_change() {
241        let mut director = create_director(vec![Task { id: 0 }]);
242
243        let change = AddTask { id: 1 };
244        change.apply(&mut director);
245
246        assert_eq!(director.working_solution().tasks.len(), 2);
247        assert_eq!(director.working_solution().tasks[1].id, 1);
248    }
249
250    #[test]
251    fn closure_problem_change() {
252        let mut director = create_director(vec![Task { id: 0 }]);
253
254        let change = ClosureProblemChange::<TaskSchedule, _>::new("remove_all", |sd| {
255            sd.working_solution_mut().tasks.clear();
256        });
257
258        change.apply(&mut director);
259
260        assert!(director.working_solution().tasks.is_empty());
261    }
262}