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)]
20pub struct EntityRef {
21 pub type_name: String,
23 pub display: String,
25 entity: Arc<dyn Any + Send + Sync>,
27}
28
29impl EntityRef {
30 pub fn new<T: Clone + Debug + Send + Sync + 'static>(entity: &T) -> Self {
32 Self {
33 type_name: std::any::type_name::<T>().to_string(),
34 display: format!("{:?}", entity),
35 entity: Arc::new(entity.clone()),
36 }
37 }
38
39 pub fn with_display<T: Clone + Send + Sync + 'static>(entity: &T, display: String) -> Self {
41 Self {
42 type_name: std::any::type_name::<T>().to_string(),
43 display,
44 entity: Arc::new(entity.clone()),
45 }
46 }
47
48 pub fn as_entity<T: 'static>(&self) -> Option<&T> {
50 self.entity.downcast_ref::<T>()
51 }
52
53 pub fn short_type_name(&self) -> &str {
55 self.type_name
56 .rsplit("::")
57 .next()
58 .unwrap_or(&self.type_name)
59 }
60}
61
62impl Debug for EntityRef {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 f.debug_struct("EntityRef")
65 .field("type", &self.short_type_name())
66 .field("display", &self.display)
67 .finish()
68 }
69}
70
71impl PartialEq for EntityRef {
72 fn eq(&self, other: &Self) -> bool {
73 self.type_name == other.type_name && self.display == other.display
74 }
75}
76
77impl Eq for EntityRef {}
78
79impl Hash for EntityRef {
80 fn hash<H: Hasher>(&self, state: &mut H) {
81 self.type_name.hash(state);
82 self.display.hash(state);
83 }
84}
85
86#[derive(Debug, Clone)]
88pub struct ConstraintJustification {
89 pub entities: Vec<EntityRef>,
91 pub description: String,
93}
94
95impl ConstraintJustification {
96 pub fn new(entities: Vec<EntityRef>) -> Self {
98 let description = if entities.is_empty() {
99 "No entities".to_string()
100 } else {
101 entities
102 .iter()
103 .map(|e| e.display.as_str())
104 .collect::<Vec<_>>()
105 .join(", ")
106 };
107 Self {
108 entities,
109 description,
110 }
111 }
112
113 pub fn with_description(entities: Vec<EntityRef>, description: String) -> Self {
115 Self {
116 entities,
117 description,
118 }
119 }
120}
121
122#[derive(Debug, Clone)]
124pub struct DetailedConstraintMatch<Sc: Score> {
125 pub constraint_ref: ConstraintRef,
127 pub score: Sc,
129 pub justification: ConstraintJustification,
131}
132
133impl<Sc: Score> DetailedConstraintMatch<Sc> {
134 pub fn new(
136 constraint_ref: ConstraintRef,
137 score: Sc,
138 justification: ConstraintJustification,
139 ) -> Self {
140 Self {
141 constraint_ref,
142 score,
143 justification,
144 }
145 }
146}
147
148#[derive(Debug, Clone)]
150pub struct DetailedConstraintEvaluation<Sc: Score> {
151 pub total_score: Sc,
153 pub match_count: usize,
155 pub matches: Vec<DetailedConstraintMatch<Sc>>,
157}
158
159impl<Sc: Score> DetailedConstraintEvaluation<Sc> {
160 pub fn new(total_score: Sc, matches: Vec<DetailedConstraintMatch<Sc>>) -> Self {
162 let match_count = matches.len();
163 Self {
164 total_score,
165 match_count,
166 matches,
167 }
168 }
169
170 pub fn empty() -> Self {
172 Self {
173 total_score: Sc::zero(),
174 match_count: 0,
175 matches: Vec::new(),
176 }
177 }
178}
179
180#[derive(Debug, Clone)]
182pub struct ConstraintAnalysis<Sc: Score> {
183 pub constraint_ref: ConstraintRef,
185 pub weight: Sc,
187 pub score: Sc,
189 pub matches: Vec<DetailedConstraintMatch<Sc>>,
191 pub is_hard: bool,
193}
194
195impl<Sc: Score> ConstraintAnalysis<Sc> {
196 pub fn new(
198 constraint_ref: ConstraintRef,
199 weight: Sc,
200 score: Sc,
201 matches: Vec<DetailedConstraintMatch<Sc>>,
202 is_hard: bool,
203 ) -> Self {
204 Self {
205 constraint_ref,
206 weight,
207 score,
208 matches,
209 is_hard,
210 }
211 }
212
213 pub fn match_count(&self) -> usize {
215 self.matches.len()
216 }
217
218 pub fn name(&self) -> &str {
220 &self.constraint_ref.name
221 }
222}
223
224#[derive(Debug, Clone)]
226pub struct ScoreExplanation<Sc: Score> {
227 pub score: Sc,
229 pub constraint_analyses: Vec<ConstraintAnalysis<Sc>>,
231}
232
233impl<Sc: Score> ScoreExplanation<Sc> {
234 pub fn new(score: Sc, constraint_analyses: Vec<ConstraintAnalysis<Sc>>) -> Self {
236 Self {
237 score,
238 constraint_analyses,
239 }
240 }
241
242 pub fn total_match_count(&self) -> usize {
244 self.constraint_analyses
245 .iter()
246 .map(|a| a.match_count())
247 .sum()
248 }
249
250 pub fn non_zero_constraints(&self) -> Vec<&ConstraintAnalysis<Sc>> {
252 self.constraint_analyses
253 .iter()
254 .filter(|a| a.score != Sc::zero())
255 .collect()
256 }
257
258 pub fn all_matches(&self) -> Vec<&DetailedConstraintMatch<Sc>> {
260 self.constraint_analyses
261 .iter()
262 .flat_map(|a| &a.matches)
263 .collect()
264 }
265}
266
267#[derive(Debug, Clone)]
269pub struct Indictment<Sc: Score> {
270 pub entity: EntityRef,
272 pub score: Sc,
274 pub constraint_matches: HashMap<ConstraintRef, Vec<DetailedConstraintMatch<Sc>>>,
276}
277
278impl<Sc: Score> Indictment<Sc> {
279 pub fn new(entity: EntityRef) -> Self {
281 Self {
282 entity,
283 score: Sc::zero(),
284 constraint_matches: HashMap::new(),
285 }
286 }
287
288 pub fn add_match(&mut self, constraint_match: DetailedConstraintMatch<Sc>) {
290 self.score = self.score + constraint_match.score;
291 self.constraint_matches
292 .entry(constraint_match.constraint_ref.clone())
293 .or_default()
294 .push(constraint_match);
295 }
296
297 pub fn match_count(&self) -> usize {
299 self.constraint_matches
300 .values()
301 .map(|v| v.len())
302 .sum::<usize>()
303 }
304
305 pub fn violated_constraints(&self) -> Vec<&ConstraintRef> {
307 self.constraint_matches.keys().collect()
308 }
309
310 pub fn constraint_count(&self) -> usize {
312 self.constraint_matches.len()
313 }
314}
315
316#[derive(Debug, Clone)]
318pub struct IndictmentMap<Sc: Score> {
319 pub indictments: HashMap<EntityRef, Indictment<Sc>>,
321}
322
323impl<Sc: Score> IndictmentMap<Sc> {
324 pub fn new() -> Self {
326 Self {
327 indictments: HashMap::new(),
328 }
329 }
330
331 pub fn from_matches(matches: Vec<DetailedConstraintMatch<Sc>>) -> Self {
333 let mut map = Self::new();
334 for m in matches {
335 for entity in &m.justification.entities {
336 map.indictments
337 .entry(entity.clone())
338 .or_insert_with(|| Indictment::new(entity.clone()))
339 .add_match(m.clone());
340 }
341 }
342 map
343 }
344
345 pub fn get(&self, entity: &EntityRef) -> Option<&Indictment<Sc>> {
347 self.indictments.get(entity)
348 }
349
350 pub fn entities(&self) -> impl Iterator<Item = &EntityRef> {
352 self.indictments.keys()
353 }
354
355 pub fn worst_entities(&self) -> Vec<&EntityRef> {
357 let mut entities: Vec<_> = self.indictments.keys().collect();
358 entities.sort_by(|a, b| {
359 let score_a = &self.indictments[*a].score;
360 let score_b = &self.indictments[*b].score;
361 score_a.cmp(score_b)
362 });
363 entities
364 }
365
366 pub fn len(&self) -> usize {
368 self.indictments.len()
369 }
370
371 pub fn is_empty(&self) -> bool {
373 self.indictments.is_empty()
374 }
375}
376
377impl<Sc: Score> Default for IndictmentMap<Sc> {
378 fn default() -> Self {
379 Self::new()
380 }
381}