solverforge_core/score/traits.rs
1// Core Score trait definition
2
3use std::cmp::Ordering;
4use std::fmt::{Debug, Display};
5use std::ops::{Add, Neg, Sub};
6
7use super::ScoreLevel;
8
9/// Core trait for all score types in SolverForge.
10///
11/// Scores represent the quality of a planning solution. They are used to:
12/// - Compare solutions (better/worse/equal)
13/// - Guide the optimization process
14/// - Determine feasibility
15///
16/// All score implementations must be:
17/// - Immutable (operations return new instances)
18/// - Thread-safe (Send + Sync)
19/// - Comparable (total ordering)
20///
21/// # Score Levels
22///
23/// Scores can have multiple levels (e.g., hard/soft constraints):
24/// - Hard constraints: Must be satisfied for a solution to be feasible
25/// - Soft constraints: Optimization objectives to maximize/minimize
26///
27/// When comparing scores, higher-priority levels are compared first.
28pub trait Score:
29 Copy
30 + Debug
31 + Display
32 + Default
33 + Send
34 + Sync
35 + PartialEq
36 + Eq
37 + PartialOrd
38 + Ord
39 + Add<Output = Self>
40 + Sub<Output = Self>
41 + Neg<Output = Self>
42 + 'static
43{
44 /* Returns true if this score represents a feasible solution.
45
46 A solution is feasible when all hard constraints are satisfied
47 (i.e., the hard score is >= 0).
48 */
49 fn is_feasible(&self) -> bool;
50
51 fn zero() -> Self;
52
53 /* Returns the number of score levels.
54
55 For example:
56 - SoftScore: 1 level
57 - HardSoftScore: 2 levels
58 - HardMediumSoftScore: 3 levels
59 */
60 fn levels_count() -> usize;
61
62 /* Returns the score values as a vector of i64.
63
64 The order is from highest priority to lowest priority.
65 For HardSoftScore: [hard, soft]
66 */
67 fn to_level_numbers(&self) -> Vec<i64>;
68
69 /* Creates a score from level numbers.
70
71 # Panics
72 Panics if the number of levels doesn't match `levels_count()`.
73 */
74 fn from_level_numbers(levels: &[i64]) -> Self;
75
76 // Multiplies this score by a scalar.
77 fn multiply(&self, multiplicand: f64) -> Self;
78
79 // Divides this score by a scalar.
80 fn divide(&self, divisor: f64) -> Self;
81
82 fn abs(&self) -> Self;
83
84 /* Converts this score to a single f64 scalar value.
85
86 Higher-priority levels are weighted with larger multipliers to preserve
87 their dominance. Used for simulated annealing temperature calculations.
88 */
89 fn to_scalar(&self) -> f64;
90
91 /* Returns the semantic label for the score level at the given index.
92
93 Level indices follow the same order as `to_level_numbers()`:
94 highest priority first.
95
96 # Panics
97 Panics if `index >= levels_count()`.
98 */
99 fn level_label(index: usize) -> ScoreLevel;
100
101 /* Compares two scores, returning the ordering.
102
103 Default implementation uses the Ord trait.
104 */
105 fn compare(&self, other: &Self) -> Ordering {
106 self.cmp(other)
107 }
108
109 /* Returns true if this score is better than the other score.
110
111 In optimization, "better" typically means higher score.
112 */
113 fn is_better_than(&self, other: &Self) -> bool {
114 self > other
115 }
116
117 // Returns true if this score is worse than the other score.
118 fn is_worse_than(&self, other: &Self) -> bool {
119 self < other
120 }
121
122 // Returns true if this score is equal to the other score.
123 fn is_equal_to(&self, other: &Self) -> bool {
124 self == other
125 }
126
127 // Returns a score with 1 at the first Hard-labeled level and 0 elsewhere.
128 fn one_hard() -> Self {
129 let mut levels = vec![0i64; Self::levels_count()];
130 if let Some(i) =
131 (0..Self::levels_count()).find(|&i| Self::level_label(i) == ScoreLevel::Hard)
132 {
133 levels[i] = 1;
134 }
135 Self::from_level_numbers(&levels)
136 }
137
138 // Returns a score with 1 at the last Soft-labeled level and 0 elsewhere.
139 fn one_soft() -> Self {
140 let mut levels = vec![0i64; Self::levels_count()];
141 if let Some(i) = (0..Self::levels_count())
142 .rev()
143 .find(|&i| Self::level_label(i) == ScoreLevel::Soft)
144 {
145 levels[i] = 1;
146 }
147 Self::from_level_numbers(&levels)
148 }
149
150 // Returns a score with 1 at the first Medium-labeled level and 0 elsewhere.
151 fn one_medium() -> Self {
152 let mut levels = vec![0i64; Self::levels_count()];
153 if let Some(i) =
154 (0..Self::levels_count()).find(|&i| Self::level_label(i) == ScoreLevel::Medium)
155 {
156 levels[i] = 1;
157 }
158 Self::from_level_numbers(&levels)
159 }
160}
161
162/// Marker trait for scores that can be parsed from a string.
163pub trait ParseableScore: Score {
164 /* Parses a score from a string representation.
165
166 # Format
167 - SoftScore: "42" or "42init"
168 - HardSoftScore: "0hard/-100soft" or "-1hard/0soft"
169 - HardMediumSoftScore: "0hard/0medium/-100soft"
170 */
171 fn parse(s: &str) -> Result<Self, ScoreParseError>;
172
173 fn to_string_repr(&self) -> String;
174}
175
176// Error when parsing a score from string
177#[derive(Debug, Clone, PartialEq, Eq)]
178pub struct ScoreParseError {
179 pub message: String,
180}
181
182impl std::fmt::Display for ScoreParseError {
183 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184 write!(f, "Score parse error: {}", self.message)
185 }
186}
187
188impl std::error::Error for ScoreParseError {}