Skip to main content

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 {}