solverforge_scoring/api/
analysis.rs1use std::any::Any;
8use std::collections::HashMap;
9use std::fmt::Debug;
10use std::hash::{Hash, Hasher};
11use std::sync::Arc;
12
13use solverforge_core::score::Score;
14use solverforge_core::ConstraintRef;
15
16#[derive(Clone)]
21pub struct EntityRef {
22 pub type_name: String,
24 pub display: String,
26 entity: Arc<dyn Any + Send + Sync>,
28}
29
30impl EntityRef {
31 pub fn new<T: Clone + Debug + Send + Sync + 'static>(entity: &T) -> Self {
33 Self {
34 type_name: std::any::type_name::<T>().to_string(),
35 display: format!("{:?}", entity),
36 entity: Arc::new(entity.clone()),
37 }
38 }
39
40 pub fn with_display<T: Clone + Send + Sync + 'static>(entity: &T, display: String) -> Self {
42 Self {
43 type_name: std::any::type_name::<T>().to_string(),
44 display,
45 entity: Arc::new(entity.clone()),
46 }
47 }
48
49 pub fn as_entity<T: 'static>(&self) -> Option<&T> {
51 self.entity.downcast_ref::<T>()
52 }
53
54 pub fn short_type_name(&self) -> &str {
56 self.type_name
57 .rsplit("::")
58 .next()
59 .unwrap_or(&self.type_name)
60 }
61}
62
63impl Debug for EntityRef {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 f.debug_struct("EntityRef")
66 .field("type", &self.short_type_name())
67 .field("display", &self.display)
68 .finish()
69 }
70}
71
72impl PartialEq for EntityRef {
73 fn eq(&self, other: &Self) -> bool {
74 self.type_name == other.type_name && self.display == other.display
75 }
76}
77
78impl Eq for EntityRef {}
79
80impl Hash for EntityRef {
81 fn hash<H: Hasher>(&self, state: &mut H) {
82 self.type_name.hash(state);
83 self.display.hash(state);
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct ConstraintJustification {
90 pub entities: Vec<EntityRef>,
92 pub description: String,
94}
95
96impl ConstraintJustification {
97 pub fn new(entities: Vec<EntityRef>) -> Self {
99 let description = if entities.is_empty() {
100 "No entities".to_string()
101 } else {
102 entities
103 .iter()
104 .map(|e| e.display.as_str())
105 .collect::<Vec<_>>()
106 .join(", ")
107 };
108 Self {
109 entities,
110 description,
111 }
112 }
113
114 pub fn with_description(entities: Vec<EntityRef>, description: String) -> Self {
116 Self {
117 entities,
118 description,
119 }
120 }
121}
122
123#[derive(Debug, Clone)]
125pub struct DetailedConstraintMatch<Sc: Score> {
126 pub constraint_ref: ConstraintRef,
128 pub score: Sc,
130 pub justification: ConstraintJustification,
132}
133
134impl<Sc: Score> DetailedConstraintMatch<Sc> {
135 pub fn new(
137 constraint_ref: ConstraintRef,
138 score: Sc,
139 justification: ConstraintJustification,
140 ) -> Self {
141 Self {
142 constraint_ref,
143 score,
144 justification,
145 }
146 }
147}
148
149#[derive(Debug, Clone)]
151pub struct DetailedConstraintEvaluation<Sc: Score> {
152 pub total_score: Sc,
154 pub match_count: usize,
156 pub matches: Vec<DetailedConstraintMatch<Sc>>,
158}
159
160impl<Sc: Score> DetailedConstraintEvaluation<Sc> {
161 pub fn new(total_score: Sc, matches: Vec<DetailedConstraintMatch<Sc>>) -> Self {
163 let match_count = matches.len();
164 Self {
165 total_score,
166 match_count,
167 matches,
168 }
169 }
170
171 pub fn empty() -> Self {
173 Self {
174 total_score: Sc::zero(),
175 match_count: 0,
176 matches: Vec::new(),
177 }
178 }
179}
180
181#[derive(Debug, Clone)]
183pub struct ConstraintAnalysis<Sc: Score> {
184 pub constraint_ref: ConstraintRef,
186 pub weight: Sc,
188 pub score: Sc,
190 pub matches: Vec<DetailedConstraintMatch<Sc>>,
192 pub is_hard: bool,
194}
195
196impl<Sc: Score> ConstraintAnalysis<Sc> {
197 pub fn new(
199 constraint_ref: ConstraintRef,
200 weight: Sc,
201 score: Sc,
202 matches: Vec<DetailedConstraintMatch<Sc>>,
203 is_hard: bool,
204 ) -> Self {
205 Self {
206 constraint_ref,
207 weight,
208 score,
209 matches,
210 is_hard,
211 }
212 }
213
214 pub fn match_count(&self) -> usize {
216 self.matches.len()
217 }
218
219 pub fn name(&self) -> &str {
221 &self.constraint_ref.name
222 }
223}
224
225#[derive(Debug, Clone)]
227pub struct ScoreExplanation<Sc: Score> {
228 pub score: Sc,
230 pub constraint_analyses: Vec<ConstraintAnalysis<Sc>>,
232}
233
234impl<Sc: Score> ScoreExplanation<Sc> {
235 pub fn new(score: Sc, constraint_analyses: Vec<ConstraintAnalysis<Sc>>) -> Self {
237 Self {
238 score,
239 constraint_analyses,
240 }
241 }
242
243 pub fn total_match_count(&self) -> usize {
245 self.constraint_analyses
246 .iter()
247 .map(|a| a.match_count())
248 .sum()
249 }
250
251 pub fn non_zero_constraints(&self) -> Vec<&ConstraintAnalysis<Sc>> {
253 self.constraint_analyses
254 .iter()
255 .filter(|a| a.score != Sc::zero())
256 .collect()
257 }
258
259 pub fn all_matches(&self) -> Vec<&DetailedConstraintMatch<Sc>> {
261 self.constraint_analyses
262 .iter()
263 .flat_map(|a| &a.matches)
264 .collect()
265 }
266}
267
268#[derive(Debug, Clone)]
270pub struct Indictment<Sc: Score> {
271 pub entity: EntityRef,
273 pub score: Sc,
275 pub constraint_matches: HashMap<ConstraintRef, Vec<DetailedConstraintMatch<Sc>>>,
277}
278
279impl<Sc: Score> Indictment<Sc> {
280 pub fn new(entity: EntityRef) -> Self {
282 Self {
283 entity,
284 score: Sc::zero(),
285 constraint_matches: HashMap::new(),
286 }
287 }
288
289 pub fn add_match(&mut self, constraint_match: DetailedConstraintMatch<Sc>) {
291 self.score = self.score + constraint_match.score;
292 self.constraint_matches
293 .entry(constraint_match.constraint_ref.clone())
294 .or_default()
295 .push(constraint_match);
296 }
297
298 pub fn match_count(&self) -> usize {
300 self.constraint_matches
301 .values()
302 .map(|v| v.len())
303 .sum::<usize>()
304 }
305
306 pub fn violated_constraints(&self) -> Vec<&ConstraintRef> {
308 self.constraint_matches.keys().collect()
309 }
310
311 pub fn constraint_count(&self) -> usize {
313 self.constraint_matches.len()
314 }
315}
316
317#[derive(Debug, Clone)]
319pub struct IndictmentMap<Sc: Score> {
320 pub indictments: HashMap<EntityRef, Indictment<Sc>>,
322}
323
324impl<Sc: Score> IndictmentMap<Sc> {
325 pub fn new() -> Self {
327 Self {
328 indictments: HashMap::new(),
329 }
330 }
331
332 pub fn from_matches(matches: Vec<DetailedConstraintMatch<Sc>>) -> Self {
334 let mut map = Self::new();
335 for m in matches {
336 for entity in &m.justification.entities {
337 map.indictments
338 .entry(entity.clone())
339 .or_insert_with(|| Indictment::new(entity.clone()))
340 .add_match(m.clone());
341 }
342 }
343 map
344 }
345
346 pub fn get(&self, entity: &EntityRef) -> Option<&Indictment<Sc>> {
348 self.indictments.get(entity)
349 }
350
351 pub fn entities(&self) -> impl Iterator<Item = &EntityRef> {
353 self.indictments.keys()
354 }
355
356 pub fn worst_entities(&self) -> Vec<&EntityRef> {
358 let mut entities: Vec<_> = self.indictments.keys().collect();
359 entities.sort_by(|a, b| {
360 let score_a = &self.indictments[*a].score;
361 let score_b = &self.indictments[*b].score;
362 score_a.cmp(score_b)
363 });
364 entities
365 }
366
367 pub fn len(&self) -> usize {
369 self.indictments.len()
370 }
371
372 pub fn is_empty(&self) -> bool {
374 self.indictments.is_empty()
375 }
376}
377
378impl<Sc: Score> Default for IndictmentMap<Sc> {
379 fn default() -> Self {
380 Self::new()
381 }
382}