solverforge_scoring/api/
analysis.rs1use std::any::Any;
9use std::collections::HashMap;
10use std::fmt::Debug;
11use std::hash::{Hash, Hasher};
12use std::sync::Arc;
13
14use solverforge_core::score::Score;
15use solverforge_core::ConstraintRef;
16
17#[derive(Clone)]
22pub struct EntityRef {
23 pub type_name: String,
25 pub display: String,
27 entity: Arc<dyn Any + Send + Sync>,
29}
30
31impl EntityRef {
32 pub fn new<T: Clone + Debug + Send + Sync + 'static>(entity: &T) -> Self {
34 Self {
35 type_name: std::any::type_name::<T>().to_string(),
36 display: format!("{:?}", entity),
37 entity: Arc::new(entity.clone()),
38 }
39 }
40
41 pub fn with_display<T: Clone + Send + Sync + 'static>(entity: &T, display: String) -> Self {
43 Self {
44 type_name: std::any::type_name::<T>().to_string(),
45 display,
46 entity: Arc::new(entity.clone()),
47 }
48 }
49
50 pub fn as_entity<T: 'static>(&self) -> Option<&T> {
52 self.entity.downcast_ref::<T>()
53 }
54
55 pub fn short_type_name(&self) -> &str {
57 self.type_name
58 .rsplit("::")
59 .next()
60 .unwrap_or(&self.type_name)
61 }
62}
63
64impl Debug for EntityRef {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 f.debug_struct("EntityRef")
67 .field("type", &self.short_type_name())
68 .field("display", &self.display)
69 .finish()
70 }
71}
72
73impl PartialEq for EntityRef {
74 fn eq(&self, other: &Self) -> bool {
75 self.type_name == other.type_name && self.display == other.display
76 }
77}
78
79impl Eq for EntityRef {}
80
81impl Hash for EntityRef {
82 fn hash<H: Hasher>(&self, state: &mut H) {
83 self.type_name.hash(state);
84 self.display.hash(state);
85 }
86}
87
88#[derive(Debug, Clone)]
90pub struct ConstraintJustification {
91 pub entities: Vec<EntityRef>,
93 pub description: String,
95}
96
97impl ConstraintJustification {
98 pub fn new(entities: Vec<EntityRef>) -> Self {
100 let description = if entities.is_empty() {
101 "No entities".to_string()
102 } else {
103 entities
104 .iter()
105 .map(|e| e.display.as_str())
106 .collect::<Vec<_>>()
107 .join(", ")
108 };
109 Self {
110 entities,
111 description,
112 }
113 }
114
115 pub fn with_description(entities: Vec<EntityRef>, description: String) -> Self {
117 Self {
118 entities,
119 description,
120 }
121 }
122}
123
124#[derive(Debug, Clone)]
126pub struct DetailedConstraintMatch<Sc: Score> {
127 pub constraint_ref: ConstraintRef,
129 pub score: Sc,
131 pub justification: ConstraintJustification,
133}
134
135impl<Sc: Score> DetailedConstraintMatch<Sc> {
136 pub fn new(
138 constraint_ref: ConstraintRef,
139 score: Sc,
140 justification: ConstraintJustification,
141 ) -> Self {
142 Self {
143 constraint_ref,
144 score,
145 justification,
146 }
147 }
148}
149
150#[derive(Debug, Clone)]
152pub struct DetailedConstraintEvaluation<Sc: Score> {
153 pub total_score: Sc,
155 pub match_count: usize,
157 pub matches: Vec<DetailedConstraintMatch<Sc>>,
159}
160
161impl<Sc: Score> DetailedConstraintEvaluation<Sc> {
162 pub fn new(total_score: Sc, matches: Vec<DetailedConstraintMatch<Sc>>) -> Self {
164 let match_count = matches.len();
165 Self {
166 total_score,
167 match_count,
168 matches,
169 }
170 }
171
172 pub fn empty() -> Self {
174 Self {
175 total_score: Sc::zero(),
176 match_count: 0,
177 matches: Vec::new(),
178 }
179 }
180}
181
182#[derive(Debug, Clone)]
184pub struct ConstraintAnalysis<Sc: Score> {
185 pub constraint_ref: ConstraintRef,
187 pub weight: Sc,
189 pub score: Sc,
191 pub matches: Vec<DetailedConstraintMatch<Sc>>,
193 pub is_hard: bool,
195}
196
197impl<Sc: Score> ConstraintAnalysis<Sc> {
198 pub fn new(
200 constraint_ref: ConstraintRef,
201 weight: Sc,
202 score: Sc,
203 matches: Vec<DetailedConstraintMatch<Sc>>,
204 is_hard: bool,
205 ) -> Self {
206 Self {
207 constraint_ref,
208 weight,
209 score,
210 matches,
211 is_hard,
212 }
213 }
214
215 pub fn match_count(&self) -> usize {
217 self.matches.len()
218 }
219
220 pub fn name(&self) -> &str {
222 &self.constraint_ref.name
223 }
224}
225
226#[derive(Debug, Clone)]
228pub struct ScoreExplanation<Sc: Score> {
229 pub score: Sc,
231 pub constraint_analyses: Vec<ConstraintAnalysis<Sc>>,
233}
234
235impl<Sc: Score> ScoreExplanation<Sc> {
236 pub fn new(score: Sc, constraint_analyses: Vec<ConstraintAnalysis<Sc>>) -> Self {
238 Self {
239 score,
240 constraint_analyses,
241 }
242 }
243
244 pub fn total_match_count(&self) -> usize {
246 self.constraint_analyses
247 .iter()
248 .map(|a| a.match_count())
249 .sum()
250 }
251
252 pub fn non_zero_constraints(&self) -> Vec<&ConstraintAnalysis<Sc>> {
254 self.constraint_analyses
255 .iter()
256 .filter(|a| a.score != Sc::zero())
257 .collect()
258 }
259
260 pub fn all_matches(&self) -> Vec<&DetailedConstraintMatch<Sc>> {
262 self.constraint_analyses
263 .iter()
264 .flat_map(|a| &a.matches)
265 .collect()
266 }
267}
268
269#[derive(Debug, Clone)]
271pub struct Indictment<Sc: Score> {
272 pub entity: EntityRef,
274 pub score: Sc,
276 pub constraint_matches: HashMap<ConstraintRef, Vec<DetailedConstraintMatch<Sc>>>,
278}
279
280impl<Sc: Score> Indictment<Sc> {
281 pub fn new(entity: EntityRef) -> Self {
283 Self {
284 entity,
285 score: Sc::zero(),
286 constraint_matches: HashMap::new(),
287 }
288 }
289
290 pub fn add_match(&mut self, constraint_match: DetailedConstraintMatch<Sc>) {
292 self.score = self.score + constraint_match.score;
293 self.constraint_matches
294 .entry(constraint_match.constraint_ref.clone())
295 .or_default()
296 .push(constraint_match);
297 }
298
299 pub fn match_count(&self) -> usize {
301 self.constraint_matches
302 .values()
303 .map(|v| v.len())
304 .sum::<usize>()
305 }
306
307 pub fn violated_constraints(&self) -> Vec<&ConstraintRef> {
309 self.constraint_matches.keys().collect()
310 }
311
312 pub fn constraint_count(&self) -> usize {
314 self.constraint_matches.len()
315 }
316}
317
318#[derive(Debug, Clone)]
320pub struct IndictmentMap<Sc: Score> {
321 pub indictments: HashMap<EntityRef, Indictment<Sc>>,
323}
324
325impl<Sc: Score> IndictmentMap<Sc> {
326 pub fn new() -> Self {
328 Self {
329 indictments: HashMap::new(),
330 }
331 }
332
333 pub fn from_matches(matches: Vec<DetailedConstraintMatch<Sc>>) -> Self {
335 let mut map = Self::new();
336 for m in matches {
337 for entity in &m.justification.entities {
338 map.indictments
339 .entry(entity.clone())
340 .or_insert_with(|| Indictment::new(entity.clone()))
341 .add_match(m.clone());
342 }
343 }
344 map
345 }
346
347 pub fn get(&self, entity: &EntityRef) -> Option<&Indictment<Sc>> {
349 self.indictments.get(entity)
350 }
351
352 pub fn entities(&self) -> impl Iterator<Item = &EntityRef> {
354 self.indictments.keys()
355 }
356
357 pub fn worst_entities(&self) -> Vec<&EntityRef> {
359 let mut entities: Vec<_> = self.indictments.keys().collect();
360 entities.sort_by(|a, b| {
361 let score_a = &self.indictments[*a].score;
362 let score_b = &self.indictments[*b].score;
363 score_a.cmp(score_b)
364 });
365 entities
366 }
367
368 pub fn len(&self) -> usize {
370 self.indictments.len()
371 }
372
373 pub fn is_empty(&self) -> bool {
375 self.indictments.is_empty()
376 }
377}
378
379impl<Sc: Score> Default for IndictmentMap<Sc> {
380 fn default() -> Self {
381 Self::new()
382 }
383}