use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ObjectiveSpec {
pub direction: ObjectiveDirection,
pub metric: String,
pub weight: f64,
}
impl ObjectiveSpec {
pub fn minimize(metric: impl Into<String>) -> Self {
Self {
direction: ObjectiveDirection::Minimize,
metric: metric.into(),
weight: 1.0,
}
}
pub fn maximize(metric: impl Into<String>) -> Self {
Self {
direction: ObjectiveDirection::Maximize,
metric: metric.into(),
weight: 1.0,
}
}
pub fn with_weight(mut self, weight: f64) -> Self {
self.weight = weight;
self
}
pub fn is_minimize(&self) -> bool {
self.direction == ObjectiveDirection::Minimize
}
pub fn is_better(&self, a: f64, b: f64) -> bool {
match self.direction {
ObjectiveDirection::Minimize => a < b,
ObjectiveDirection::Maximize => a > b,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ObjectiveDirection {
Minimize,
Maximize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConstraintSpec {
pub name: String,
pub constraint_type: ConstraintType,
pub hardness: ConstraintHardness,
pub penalty_weight: f64,
}
impl ConstraintSpec {
pub fn hard(name: impl Into<String>, constraint_type: ConstraintType) -> Self {
Self {
name: name.into(),
constraint_type,
hardness: ConstraintHardness::Hard,
penalty_weight: 0.0,
}
}
pub fn soft(name: impl Into<String>, constraint_type: ConstraintType, penalty: f64) -> Self {
Self {
name: name.into(),
constraint_type,
hardness: ConstraintHardness::Soft,
penalty_weight: penalty,
}
}
pub fn is_hard(&self) -> bool {
self.hardness == ConstraintHardness::Hard
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ConstraintType {
Capacity {
resource: String,
limit: f64,
},
TimeWindow {
start: i64,
end: i64,
},
Precedence {
before: String,
after: String,
},
Exclusion {
items: Vec<String>,
},
Minimum {
resource: String,
value: f64,
},
Maximum {
resource: String,
value: f64,
},
Custom {
key: String,
value: serde_json::Value,
},
}
impl ConstraintType {
pub fn capacity(resource: impl Into<String>, limit: f64) -> Self {
Self::Capacity {
resource: resource.into(),
limit,
}
}
pub fn time_window(start: i64, end: i64) -> Self {
Self::TimeWindow { start, end }
}
pub fn precedence(before: impl Into<String>, after: impl Into<String>) -> Self {
Self::Precedence {
before: before.into(),
after: after.into(),
}
}
pub fn exclusion(items: Vec<String>) -> Self {
Self::Exclusion { items }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConstraintHardness {
Hard,
Soft,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Violation {
pub constraint_name: String,
pub severity: f64,
pub explanation: String,
pub affected_entities: Vec<String>,
}
impl Violation {
pub fn new(
constraint_name: impl Into<String>,
severity: f64,
explanation: impl Into<String>,
) -> Self {
Self {
constraint_name: constraint_name.into(),
severity: severity.clamp(0.0, 1.0),
explanation: explanation.into(),
affected_entities: Vec::new(),
}
}
pub fn with_affected(mut self, entity: impl Into<String>) -> Self {
self.affected_entities.push(entity.into());
self
}
pub fn with_affected_all(mut self, entities: impl IntoIterator<Item = impl Into<String>>) -> Self {
for e in entities {
self.affected_entities.push(e.into());
}
self
}
pub fn is_severe(&self) -> bool {
self.severity >= 0.8
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_objective_minimize() {
let obj = ObjectiveSpec::minimize("cost");
assert!(obj.is_minimize());
assert!(obj.is_better(10.0, 20.0)); }
#[test]
fn test_objective_maximize() {
let obj = ObjectiveSpec::maximize("profit");
assert!(!obj.is_minimize());
assert!(obj.is_better(20.0, 10.0)); }
#[test]
fn test_constraint_hard() {
let c = ConstraintSpec::hard("capacity", ConstraintType::capacity("memory", 1024.0));
assert!(c.is_hard());
assert_eq!(c.penalty_weight, 0.0);
}
#[test]
fn test_constraint_soft() {
let c = ConstraintSpec::soft("preference", ConstraintType::time_window(0, 100), 0.5);
assert!(!c.is_hard());
assert_eq!(c.penalty_weight, 0.5);
}
#[test]
fn test_violation() {
let v = Violation::new("capacity", 0.9, "exceeded by 10%")
.with_affected("node-1")
.with_affected("node-2");
assert!(v.is_severe());
assert_eq!(v.affected_entities.len(), 2);
}
#[test]
fn test_severity_clamped() {
let v = Violation::new("test", 1.5, "over max");
assert_eq!(v.severity, 1.0);
let v2 = Violation::new("test", -0.5, "under min");
assert_eq!(v2.severity, 0.0);
}
#[test]
fn test_constraint_serde() {
let c = ConstraintSpec::hard("cap", ConstraintType::capacity("cpu", 100.0));
let json = serde_json::to_string(&c).unwrap();
let restored: ConstraintSpec = serde_json::from_str(&json).unwrap();
assert_eq!(restored.name, "cap");
}
}