converge_optimization/gate/
constraints.rs1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct ObjectiveSpec {
8 pub direction: ObjectiveDirection,
10 pub metric: String,
12 pub weight: f64,
14}
15
16impl ObjectiveSpec {
17 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 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 pub fn with_weight(mut self, weight: f64) -> Self {
37 self.weight = weight;
38 self
39 }
40
41 pub fn is_minimize(&self) -> bool {
43 self.direction == ObjectiveDirection::Minimize
44 }
45
46 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "snake_case")]
59pub enum ObjectiveDirection {
60 Minimize,
62 Maximize,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct ConstraintSpec {
69 pub name: String,
71 pub constraint_type: ConstraintType,
73 pub hardness: ConstraintHardness,
75 pub penalty_weight: f64,
77}
78
79impl ConstraintSpec {
80 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 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 pub fn is_hard(&self) -> bool {
102 self.hardness == ConstraintHardness::Hard
103 }
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108#[serde(tag = "type", rename_all = "snake_case")]
109pub enum ConstraintType {
110 Capacity {
112 resource: String,
114 limit: f64,
116 },
117 TimeWindow {
119 start: i64,
121 end: i64,
123 },
124 Precedence {
126 before: String,
128 after: String,
130 },
131 Exclusion {
133 items: Vec<String>,
135 },
136 Minimum {
138 resource: String,
140 value: f64,
142 },
143 Maximum {
145 resource: String,
147 value: f64,
149 },
150 Custom {
152 key: String,
154 value: serde_json::Value,
156 },
157}
158
159impl ConstraintType {
160 pub fn capacity(resource: impl Into<String>, limit: f64) -> Self {
162 Self::Capacity {
163 resource: resource.into(),
164 limit,
165 }
166 }
167
168 pub fn time_window(start: i64, end: i64) -> Self {
170 Self::TimeWindow { start, end }
171 }
172
173 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 pub fn exclusion(items: Vec<String>) -> Self {
183 Self::Exclusion { items }
184 }
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
189#[serde(rename_all = "snake_case")]
190pub enum ConstraintHardness {
191 Hard,
193 Soft,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct Violation {
200 pub constraint_name: String,
202 pub severity: f64,
204 pub explanation: String,
206 pub affected_entities: Vec<String>,
208}
209
210impl Violation {
211 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 pub fn with_affected(mut self, entity: impl Into<String>) -> Self {
227 self.affected_entities.push(entity.into());
228 self
229 }
230
231 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 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)); }
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)); }
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}