solverforge_core/analysis/
manager.rs1use crate::analysis::ScoreExplanation;
2use crate::bridge::LanguageBridge;
3use crate::constraints::ConstraintSet;
4use crate::domain::DomainModel;
5use crate::error::{SolverForgeError, SolverForgeResult};
6use crate::solver::{
7 ListAccessorDto, ScoreDto, SolveRequest, SolveResponse, Solver, SolverConfig, SolverService,
8};
9use crate::ObjectHandle;
10use std::marker::PhantomData;
11use std::sync::Arc;
12
13pub struct SolutionManager<B: LanguageBridge> {
14 config: SolverConfig,
15 service: Arc<dyn SolverService>,
16 domain_model: DomainModel,
17 constraints: ConstraintSet,
18 wasm_module: String,
19 _bridge: PhantomData<B>,
20}
21
22impl<B: LanguageBridge> SolutionManager<B> {
23 pub fn create(solver: &Solver<B>) -> Self {
24 Self {
25 config: solver.config().clone(),
26 service: solver.service().clone(),
27 domain_model: solver.domain_model().clone(),
28 constraints: solver.constraints().clone(),
29 wasm_module: solver.wasm_module().to_string(),
30 _bridge: PhantomData,
31 }
32 }
33
34 pub fn new(
35 config: SolverConfig,
36 service: Arc<dyn SolverService>,
37 domain_model: DomainModel,
38 constraints: ConstraintSet,
39 wasm_module: String,
40 ) -> Self {
41 Self {
42 config,
43 service,
44 domain_model,
45 constraints,
46 wasm_module,
47 _bridge: PhantomData,
48 }
49 }
50
51 pub fn explain(
52 &self,
53 bridge: &B,
54 solution: ObjectHandle,
55 ) -> SolverForgeResult<ScoreExplanation> {
56 let request = self.build_explain_request(bridge, solution)?;
57 let response = self.service.solve(&request)?;
58 self.parse_explanation(response)
59 }
60
61 pub fn update(&self, bridge: &B, solution: ObjectHandle) -> SolverForgeResult<ScoreDto> {
62 let request = self.build_update_request(bridge, solution)?;
63 let response = self.service.solve(&request)?;
64 Ok(parse_score_string(&response.score))
65 }
66
67 fn build_explain_request(
68 &self,
69 bridge: &B,
70 solution: ObjectHandle,
71 ) -> SolverForgeResult<SolveRequest> {
72 let solution_json = bridge.serialize_object(solution).map_err(|e| {
73 SolverForgeError::Bridge(format!("Failed to serialize solution: {}", e))
74 })?;
75
76 let domain_dto = self.domain_model.to_dto();
77 let constraints_dto = self.constraints.to_dto();
78
79 let list_accessor = ListAccessorDto::new(
80 "create_list",
81 "get_item",
82 "set_item",
83 "get_size",
84 "append",
85 "insert",
86 "remove",
87 "deallocate_list",
88 );
89
90 let mut request = SolveRequest::new(
91 domain_dto,
92 constraints_dto,
93 self.wasm_module.clone(),
94 "allocate".to_string(),
95 "deallocate".to_string(),
96 list_accessor,
97 solution_json,
98 );
99
100 if let Some(termination) = self.config.termination.as_ref() {
102 request = request.with_termination(termination.clone());
103 }
104
105 if let Some(mode) = &self.config.environment_mode {
106 request = request.with_environment_mode(format!("{:?}", mode).to_uppercase());
107 }
108
109 Ok(request)
110 }
111
112 fn build_update_request(
113 &self,
114 bridge: &B,
115 solution: ObjectHandle,
116 ) -> SolverForgeResult<SolveRequest> {
117 self.build_explain_request(bridge, solution)
118 }
119
120 fn parse_explanation(&self, response: SolveResponse) -> SolverForgeResult<ScoreExplanation> {
121 let score = parse_score_string(&response.score);
124 Ok(ScoreExplanation::new(score))
125 }
126
127 pub fn config(&self) -> &SolverConfig {
128 &self.config
129 }
130
131 pub fn domain_model(&self) -> &DomainModel {
132 &self.domain_model
133 }
134
135 pub fn constraints(&self) -> &ConstraintSet {
136 &self.constraints
137 }
138}
139
140fn parse_score_string(s: &str) -> ScoreDto {
142 if let Some((hard_str, rest)) = s.split_once("hard/") {
144 if let Some((soft_str, _)) = rest.split_once("soft") {
145 if let (Ok(hard), Ok(soft)) = (hard_str.parse(), soft_str.parse()) {
146 return ScoreDto::hard_soft(hard, soft);
147 }
148 }
149 if let Some((medium_str, rest2)) = rest.split_once("medium/") {
151 if let Some((soft_str, _)) = rest2.split_once("soft") {
152 if let (Ok(hard), Ok(medium), Ok(soft)) =
153 (hard_str.parse(), medium_str.parse(), soft_str.parse())
154 {
155 return ScoreDto::hard_medium_soft(hard, medium, soft);
156 }
157 }
158 }
159 }
160 if let Ok(score) = s.parse() {
162 return ScoreDto::simple(score);
163 }
164 ScoreDto::simple(0)
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use crate::analysis::{ConstraintMatch, Indictment};
172 use crate::bridge::tests::MockBridge;
173 use crate::constraints::Constraint;
174 use crate::domain::{DomainModelBuilder, FieldDescriptor, FieldType, PrimitiveType, ScoreType};
175 use crate::solver::HttpSolverService;
176
177 fn create_test_config() -> SolverConfig {
178 SolverConfig::new().with_solution_class("Timetable")
179 }
180
181 fn create_test_domain() -> DomainModel {
182 use crate::domain::{DomainClass, PlanningAnnotation};
183
184 DomainModelBuilder::new()
185 .add_class(
186 DomainClass::new("Timetable")
187 .with_annotation(PlanningAnnotation::PlanningSolution)
188 .with_field(
189 FieldDescriptor::new("score", FieldType::Score(ScoreType::HardSoft))
190 .with_planning_annotation(PlanningAnnotation::planning_score()),
191 ),
192 )
193 .add_class(
194 DomainClass::new("Lesson")
195 .with_annotation(PlanningAnnotation::PlanningEntity)
196 .with_field(
197 FieldDescriptor::new("id", FieldType::Primitive(PrimitiveType::String))
198 .with_planning_annotation(PlanningAnnotation::PlanningId),
199 )
200 .with_field(
201 FieldDescriptor::new("room", FieldType::object("Room"))
202 .with_planning_annotation(PlanningAnnotation::planning_variable(vec![
203 "rooms".to_string(),
204 ])),
205 ),
206 )
207 .build()
208 }
209
210 fn create_test_constraints() -> ConstraintSet {
211 ConstraintSet::new().with_constraint(Constraint::new("testConstraint"))
212 }
213
214 #[test]
215 fn test_solution_manager_new() {
216 let service = Arc::new(HttpSolverService::new("http://localhost:8080"));
217
218 let manager = SolutionManager::<MockBridge>::new(
219 create_test_config(),
220 service,
221 create_test_domain(),
222 create_test_constraints(),
223 "AGFzbQ==".to_string(),
224 );
225
226 assert_eq!(
227 manager.config().solution_class,
228 Some("Timetable".to_string())
229 );
230 assert_eq!(manager.constraints().len(), 1);
231 }
232
233 #[test]
234 fn test_solution_manager_accessors() {
235 let service = Arc::new(HttpSolverService::new("http://localhost:8080"));
236
237 let manager = SolutionManager::<MockBridge>::new(
238 create_test_config(),
239 service,
240 create_test_domain(),
241 create_test_constraints(),
242 "AGFzbQ==".to_string(),
243 );
244
245 assert!(manager.domain_model().classes.contains_key("Timetable"));
246 assert!(manager.domain_model().classes.contains_key("Lesson"));
247 }
248
249 #[test]
250 fn test_parse_explanation() {
251 let service = Arc::new(HttpSolverService::new("http://localhost:8080"));
252
253 let manager = SolutionManager::<MockBridge>::new(
254 create_test_config(),
255 service,
256 create_test_domain(),
257 create_test_constraints(),
258 "AGFzbQ==".to_string(),
259 );
260
261 let response = SolveResponse::new("{}".to_string(), "-2hard/-15soft");
262
263 let explanation = manager.parse_explanation(response).unwrap();
264
265 assert_eq!(explanation.hard_score(), -2);
266 assert_eq!(explanation.soft_score(), -15);
267 assert!(!explanation.is_feasible());
268 }
269
270 #[test]
271 fn test_parse_feasible_explanation() {
272 let service = Arc::new(HttpSolverService::new("http://localhost:8080"));
273
274 let manager = SolutionManager::<MockBridge>::new(
275 create_test_config(),
276 service,
277 create_test_domain(),
278 create_test_constraints(),
279 "AGFzbQ==".to_string(),
280 );
281
282 let response = SolveResponse::new("{}".to_string(), "0hard/-5soft");
283
284 let explanation = manager.parse_explanation(response).unwrap();
285
286 assert!(explanation.is_feasible());
287 }
288
289 #[test]
290 fn test_constraint_match_creation() {
291 let cm = ConstraintMatch::new("roomConflict", ScoreDto::hard_soft(-1, 0))
292 .with_package("com.example")
293 .with_indicted_object(ObjectHandle::new(1));
294
295 assert_eq!(cm.full_constraint_name(), "com.example.roomConflict");
296 assert_eq!(cm.indicted_objects.len(), 1);
297 }
298
299 #[test]
300 fn test_indictment_creation() {
301 let obj = ObjectHandle::new(42);
302 let cm = ConstraintMatch::new("roomConflict", ScoreDto::hard_soft(-1, 0));
303
304 let indictment = Indictment::new(obj, ScoreDto::hard_soft(-1, 0)).with_constraint_match(cm);
305
306 assert_eq!(indictment.indicted_object, obj);
307 assert_eq!(indictment.constraint_count(), 1);
308 }
309
310 #[test]
311 fn test_score_explanation_builder() {
312 let explanation = ScoreExplanation::new(ScoreDto::hard_soft(-3, -20))
313 .with_constraint_match(ConstraintMatch::new(
314 "conflict1",
315 ScoreDto::hard_soft(-2, 0),
316 ))
317 .with_constraint_match(ConstraintMatch::new(
318 "conflict2",
319 ScoreDto::hard_soft(-1, -20),
320 ))
321 .with_indictment(Indictment::new(
322 ObjectHandle::new(1),
323 ScoreDto::hard_soft(-2, 0),
324 ));
325
326 assert_eq!(explanation.constraint_count(), 2);
327 assert_eq!(explanation.indictments.len(), 1);
328 }
329}