solverforge_scoring/constraint/
balance.rs1use std::collections::HashMap;
11use std::hash::Hash;
12use std::marker::PhantomData;
13
14use solverforge_core::score::Score;
15use solverforge_core::{ConstraintRef, ImpactType};
16
17use crate::api::constraint_set::IncrementalConstraint;
18use crate::stream::collection_extract::ChangeSource;
19use crate::stream::filter::UniFilter;
20
21pub struct BalanceConstraint<S, A, K, E, F, KF, Sc>
85where
86 Sc: Score,
87{
88 constraint_ref: ConstraintRef,
89 impact_type: ImpactType,
90 extractor: E,
91 filter: F,
92 key_fn: KF,
93 change_source: ChangeSource,
94 base_score: Sc,
96 is_hard: bool,
97 counts: HashMap<K, i64>,
99 entity_keys: HashMap<usize, K>,
101 group_count: i64,
104 total_count: i64,
106 sum_squared: i64,
108 _phantom: PhantomData<(fn() -> S, fn() -> A)>,
109}
110
111impl<S, A, K, E, F, KF, Sc> BalanceConstraint<S, A, K, E, F, KF, Sc>
112where
113 S: Send + Sync + 'static,
114 A: Clone + Send + Sync + 'static,
115 K: Clone + Eq + Hash + Send + Sync + 'static,
116 E: crate::stream::collection_extract::CollectionExtract<S, Item = A>,
117 F: UniFilter<S, A>,
118 KF: Fn(&A) -> Option<K> + Send + Sync,
119 Sc: Score + 'static,
120{
121 pub fn new(
134 constraint_ref: ConstraintRef,
135 impact_type: ImpactType,
136 extractor: E,
137 filter: F,
138 key_fn: KF,
139 base_score: Sc,
140 is_hard: bool,
141 ) -> Self {
142 let change_source = extractor.change_source();
143 Self {
144 constraint_ref,
145 impact_type,
146 extractor,
147 filter,
148 key_fn,
149 change_source,
150 base_score,
151 is_hard,
152 counts: HashMap::new(),
153 entity_keys: HashMap::new(),
154 group_count: 0,
155 total_count: 0,
156 sum_squared: 0,
157 _phantom: PhantomData,
158 }
159 }
160
161 fn compute_std_dev(&self) -> f64 {
163 if self.group_count == 0 {
164 return 0.0;
165 }
166 let n = self.group_count as f64;
167 let mean = self.total_count as f64 / n;
168 let variance = (self.sum_squared as f64 / n) - (mean * mean);
169 if variance <= 0.0 {
170 return 0.0;
171 }
172 variance.sqrt()
173 }
174
175 fn compute_score(&self) -> Sc {
177 let std_dev = self.compute_std_dev();
178 let base = self.base_score.multiply(std_dev);
179 match self.impact_type {
180 ImpactType::Penalty => -base,
181 ImpactType::Reward => base,
182 }
183 }
184
185 fn compute_std_dev_from_counts(counts: &HashMap<K, i64>) -> f64 {
187 if counts.is_empty() {
188 return 0.0;
189 }
190 let n = counts.len() as f64;
191 let total: i64 = counts.values().sum();
192 let sum_sq: i64 = counts.values().map(|&c| c * c).sum();
193 let mean = total as f64 / n;
194 let variance = (sum_sq as f64 / n) - (mean * mean);
195 if variance > 0.0 {
196 variance.sqrt()
197 } else {
198 0.0
199 }
200 }
201}
202
203impl<S, A, K, E, F, KF, Sc> IncrementalConstraint<S, Sc>
204 for BalanceConstraint<S, A, K, E, F, KF, Sc>
205where
206 S: Send + Sync + 'static,
207 A: Clone + Send + Sync + 'static,
208 K: Clone + Eq + Hash + Send + Sync + 'static,
209 E: crate::stream::collection_extract::CollectionExtract<S, Item = A>,
210 F: UniFilter<S, A>,
211 KF: Fn(&A) -> Option<K> + Send + Sync,
212 Sc: Score + 'static,
213{
214 fn evaluate(&self, solution: &S) -> Sc {
215 let entities = self.extractor.extract(solution);
216
217 let mut counts: HashMap<K, i64> = HashMap::new();
219 for entity in entities {
220 if !self.filter.test(solution, entity) {
221 continue;
222 }
223 if let Some(key) = (self.key_fn)(entity) {
224 *counts.entry(key).or_insert(0) += 1;
225 }
226 }
227
228 if counts.is_empty() {
229 return Sc::zero();
230 }
231
232 let std_dev = Self::compute_std_dev_from_counts(&counts);
233 let base = self.base_score.multiply(std_dev);
234 match self.impact_type {
235 ImpactType::Penalty => -base,
236 ImpactType::Reward => base,
237 }
238 }
239
240 fn match_count(&self, solution: &S) -> usize {
241 let entities = self.extractor.extract(solution);
242
243 let mut counts: HashMap<K, i64> = HashMap::new();
245 for entity in entities {
246 if !self.filter.test(solution, entity) {
247 continue;
248 }
249 if let Some(key) = (self.key_fn)(entity) {
250 *counts.entry(key).or_insert(0) += 1;
251 }
252 }
253
254 if counts.is_empty() {
255 return 0;
256 }
257
258 let total: i64 = counts.values().sum();
259 let mean = total as f64 / counts.len() as f64;
260
261 counts
263 .values()
264 .filter(|&&c| (c as f64 - mean).abs() > 0.5)
265 .count()
266 }
267
268 fn initialize(&mut self, solution: &S) -> Sc {
269 self.reset();
270
271 let entities = self.extractor.extract(solution);
272
273 for (idx, entity) in entities.iter().enumerate() {
274 if !self.filter.test(solution, entity) {
275 continue;
276 }
277 if let Some(key) = (self.key_fn)(entity) {
278 let old_count = *self.counts.get(&key).unwrap_or(&0);
279 let new_count = old_count + 1;
280 match self.counts.get_mut(&key) {
281 Some(slot) => *slot = new_count,
282 None => {
283 self.counts.insert(key.clone(), new_count);
284 }
285 }
286 self.entity_keys.insert(idx, key);
287
288 if old_count == 0 {
289 self.group_count += 1;
290 }
291 self.total_count += 1;
292 self.sum_squared += new_count * new_count - old_count * old_count;
293 }
294 }
295
296 self.compute_score()
297 }
298
299 fn on_insert(&mut self, solution: &S, entity_index: usize, _descriptor_index: usize) -> Sc {
300 if !self
301 .change_source
302 .assert_localizes(_descriptor_index, &self.constraint_ref.name)
303 {
304 return Sc::zero();
305 }
306 let entities = self.extractor.extract(solution);
307 if entity_index >= entities.len() {
308 return Sc::zero();
309 }
310
311 let entity = &entities[entity_index];
312 if !self.filter.test(solution, entity) {
313 return Sc::zero();
314 }
315
316 let Some(key) = (self.key_fn)(entity) else {
317 return Sc::zero();
318 };
319
320 let old_score = self.compute_score();
321
322 let old_count = *self.counts.get(&key).unwrap_or(&0);
323 let new_count = old_count + 1;
324 match self.counts.get_mut(&key) {
325 Some(slot) => *slot = new_count,
326 None => {
327 self.counts.insert(key.clone(), new_count);
328 }
329 }
330 self.entity_keys.insert(entity_index, key);
331
332 if old_count == 0 {
333 self.group_count += 1;
334 }
335 self.total_count += 1;
336 self.sum_squared += new_count * new_count - old_count * old_count;
337
338 let new_score = self.compute_score();
339 new_score - old_score
340 }
341
342 fn on_retract(&mut self, solution: &S, entity_index: usize, _descriptor_index: usize) -> Sc {
343 if !self
344 .change_source
345 .assert_localizes(_descriptor_index, &self.constraint_ref.name)
346 {
347 return Sc::zero();
348 }
349 let entities = self.extractor.extract(solution);
350 if entity_index >= entities.len() {
351 return Sc::zero();
352 }
353
354 let Some(key) = self.entity_keys.remove(&entity_index) else {
356 return Sc::zero();
357 };
358
359 let old_score = self.compute_score();
360
361 let old_count = *self.counts.get(&key).unwrap_or(&0);
362 if old_count == 0 {
363 return Sc::zero();
364 }
365
366 let new_count = old_count - 1;
367 if new_count == 0 {
368 self.counts.remove(&key);
369 self.group_count -= 1;
370 } else {
371 self.counts.insert(key, new_count);
372 }
373 self.total_count -= 1;
374 self.sum_squared += new_count * new_count - old_count * old_count;
375
376 let new_score = self.compute_score();
377 new_score - old_score
378 }
379
380 fn reset(&mut self) {
381 self.counts.clear();
382 self.entity_keys.clear();
383 self.group_count = 0;
384 self.total_count = 0;
385 self.sum_squared = 0;
386 }
387
388 fn name(&self) -> &str {
389 &self.constraint_ref.name
390 }
391
392 fn is_hard(&self) -> bool {
393 self.is_hard
394 }
395
396 fn constraint_ref(&self) -> &ConstraintRef {
397 &self.constraint_ref
398 }
399}
400
401impl<S, A, K, E, F, KF, Sc> std::fmt::Debug for BalanceConstraint<S, A, K, E, F, KF, Sc>
402where
403 Sc: Score,
404{
405 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
406 f.debug_struct("BalanceConstraint")
407 .field("name", &self.constraint_ref.name)
408 .field("impact_type", &self.impact_type)
409 .field("groups", &self.counts.len())
410 .finish()
411 }
412}