Skip to main content

converge_optimization/gate/
constraints.rs

1//! Constraint and objective specifications
2
3use serde::{Deserialize, Serialize};
4
5/// Objective specification
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ObjectiveSpec {
8    /// Objective direction
9    pub direction: ObjectiveDirection,
10    /// Name of the metric to optimize
11    pub metric: String,
12    /// Weight for multi-objective (1.0 for single)
13    pub weight: f64,
14}
15
16impl ObjectiveSpec {
17    /// Create minimize objective
18    pub fn minimize(metric: impl Into<String>) -> Self {
19        Self {
20            direction: ObjectiveDirection::Minimize,
21            metric: metric.into(),
22            weight: 1.0,
23        }
24    }
25
26    /// Create maximize objective
27    pub fn maximize(metric: impl Into<String>) -> Self {
28        Self {
29            direction: ObjectiveDirection::Maximize,
30            metric: metric.into(),
31            weight: 1.0,
32        }
33    }
34
35    /// Set weight for multi-objective optimization
36    pub fn with_weight(mut self, weight: f64) -> Self {
37        self.weight = weight;
38        self
39    }
40
41    /// Check if this is a minimization objective
42    pub fn is_minimize(&self) -> bool {
43        self.direction == ObjectiveDirection::Minimize
44    }
45
46    /// Compare two values according to objective direction
47    /// Returns true if `a` is better than `b`
48    pub fn is_better(&self, a: f64, b: f64) -> bool {
49        match self.direction {
50            ObjectiveDirection::Minimize => a < b,
51            ObjectiveDirection::Maximize => a > b,
52        }
53    }
54}
55
56/// Objective direction
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "snake_case")]
59pub enum ObjectiveDirection {
60    /// Minimize the objective
61    Minimize,
62    /// Maximize the objective
63    Maximize,
64}
65
66/// Constraint specification
67#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct ConstraintSpec {
69    /// Constraint name
70    pub name: String,
71    /// Constraint type
72    pub constraint_type: ConstraintType,
73    /// Whether this is a hard (must satisfy) or soft constraint
74    pub hardness: ConstraintHardness,
75    /// Penalty weight for soft constraints
76    pub penalty_weight: f64,
77}
78
79impl ConstraintSpec {
80    /// Create a hard constraint
81    pub fn hard(name: impl Into<String>, constraint_type: ConstraintType) -> Self {
82        Self {
83            name: name.into(),
84            constraint_type,
85            hardness: ConstraintHardness::Hard,
86            penalty_weight: 0.0,
87        }
88    }
89
90    /// Create a soft constraint with penalty
91    pub fn soft(name: impl Into<String>, constraint_type: ConstraintType, penalty: f64) -> Self {
92        Self {
93            name: name.into(),
94            constraint_type,
95            hardness: ConstraintHardness::Soft,
96            penalty_weight: penalty,
97        }
98    }
99
100    /// Check if this is a hard constraint
101    pub fn is_hard(&self) -> bool {
102        self.hardness == ConstraintHardness::Hard
103    }
104}
105
106/// Constraint type (pack-specific interpretation)
107#[derive(Debug, Clone, Serialize, Deserialize)]
108#[serde(tag = "type", rename_all = "snake_case")]
109pub enum ConstraintType {
110    /// Capacity constraint
111    Capacity {
112        /// Resource being constrained
113        resource: String,
114        /// Maximum limit
115        limit: f64,
116    },
117    /// Time window constraint
118    TimeWindow {
119        /// Start time (unix timestamp)
120        start: i64,
121        /// End time (unix timestamp)
122        end: i64,
123    },
124    /// Precedence constraint
125    Precedence {
126        /// Item that must come before
127        before: String,
128        /// Item that must come after
129        after: String,
130    },
131    /// Exclusion constraint (mutual exclusivity)
132    Exclusion {
133        /// Items that cannot be selected together
134        items: Vec<String>,
135    },
136    /// Minimum requirement
137    Minimum {
138        /// Resource or metric
139        resource: String,
140        /// Minimum value required
141        value: f64,
142    },
143    /// Maximum limit
144    Maximum {
145        /// Resource or metric
146        resource: String,
147        /// Maximum value allowed
148        value: f64,
149    },
150    /// Custom constraint (pack interprets)
151    Custom {
152        /// Constraint key
153        key: String,
154        /// Constraint value
155        value: serde_json::Value,
156    },
157}
158
159impl ConstraintType {
160    /// Create a capacity constraint
161    pub fn capacity(resource: impl Into<String>, limit: f64) -> Self {
162        Self::Capacity {
163            resource: resource.into(),
164            limit,
165        }
166    }
167
168    /// Create a time window constraint
169    pub fn time_window(start: i64, end: i64) -> Self {
170        Self::TimeWindow { start, end }
171    }
172
173    /// Create a precedence constraint
174    pub fn precedence(before: impl Into<String>, after: impl Into<String>) -> Self {
175        Self::Precedence {
176            before: before.into(),
177            after: after.into(),
178        }
179    }
180
181    /// Create an exclusion constraint
182    pub fn exclusion(items: Vec<String>) -> Self {
183        Self::Exclusion { items }
184    }
185}
186
187/// Constraint hardness
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
189#[serde(rename_all = "snake_case")]
190pub enum ConstraintHardness {
191    /// Must be satisfied
192    Hard,
193    /// Can be violated with penalty
194    Soft,
195}
196
197/// A constraint violation in solution
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct Violation {
200    /// Which constraint was violated
201    pub constraint_name: String,
202    /// Severity (0.0 = marginal, 1.0 = complete violation)
203    pub severity: f64,
204    /// Human-readable explanation
205    pub explanation: String,
206    /// Affected entities
207    pub affected_entities: Vec<String>,
208}
209
210impl Violation {
211    /// Create a new violation
212    pub fn new(
213        constraint_name: impl Into<String>,
214        severity: f64,
215        explanation: impl Into<String>,
216    ) -> Self {
217        Self {
218            constraint_name: constraint_name.into(),
219            severity: severity.clamp(0.0, 1.0),
220            explanation: explanation.into(),
221            affected_entities: Vec::new(),
222        }
223    }
224
225    /// Add affected entity
226    pub fn with_affected(mut self, entity: impl Into<String>) -> Self {
227        self.affected_entities.push(entity.into());
228        self
229    }
230
231    /// Add multiple affected entities
232    pub fn with_affected_all(
233        mut self,
234        entities: impl IntoIterator<Item = impl Into<String>>,
235    ) -> Self {
236        for e in entities {
237            self.affected_entities.push(e.into());
238        }
239        self
240    }
241
242    /// Check if this is a severe violation
243    pub fn is_severe(&self) -> bool {
244        self.severity >= 0.8
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_objective_minimize() {
254        let obj = ObjectiveSpec::minimize("cost");
255        assert!(obj.is_minimize());
256        assert!(obj.is_better(10.0, 20.0)); // 10 < 20, so 10 is better
257    }
258
259    #[test]
260    fn test_objective_maximize() {
261        let obj = ObjectiveSpec::maximize("profit");
262        assert!(!obj.is_minimize());
263        assert!(obj.is_better(20.0, 10.0)); // 20 > 10, so 20 is better
264    }
265
266    #[test]
267    fn test_constraint_hard() {
268        let c = ConstraintSpec::hard("capacity", ConstraintType::capacity("memory", 1024.0));
269        assert!(c.is_hard());
270        assert_eq!(c.penalty_weight, 0.0);
271    }
272
273    #[test]
274    fn test_constraint_soft() {
275        let c = ConstraintSpec::soft("preference", ConstraintType::time_window(0, 100), 0.5);
276        assert!(!c.is_hard());
277        assert_eq!(c.penalty_weight, 0.5);
278    }
279
280    #[test]
281    fn test_violation() {
282        let v = Violation::new("capacity", 0.9, "exceeded by 10%")
283            .with_affected("node-1")
284            .with_affected("node-2");
285
286        assert!(v.is_severe());
287        assert_eq!(v.affected_entities.len(), 2);
288    }
289
290    #[test]
291    fn test_severity_clamped() {
292        let v = Violation::new("test", 1.5, "over max");
293        assert_eq!(v.severity, 1.0);
294
295        let v2 = Violation::new("test", -0.5, "under min");
296        assert_eq!(v2.severity, 0.0);
297    }
298
299    #[test]
300    fn test_constraint_serde() {
301        let c = ConstraintSpec::hard("cap", ConstraintType::capacity("cpu", 100.0));
302        let json = serde_json::to_string(&c).unwrap();
303        let restored: ConstraintSpec = serde_json::from_str(&json).unwrap();
304        assert_eq!(restored.name, "cap");
305    }
306}