1use crate::{
30 SolverParams,
31 assignment::{self, AssignmentProblem},
32 gate::{ProblemSpec, PromotionGate, ProposedPlan, SolverReport},
33 graph::dijkstra,
34 knapsack::{self, KnapsackProblem},
35 packs::{InvariantResult, PackRegistry},
36};
37use serde::{Deserialize, Serialize};
38use std::sync::Arc;
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum OptimizationType {
44 Assignment,
46 Knapsack,
48 ShortestPath,
50 MaxFlow,
52 MinCostFlow,
54 SetCover,
56 Scheduling,
58 VehicleRouting,
60 ConstraintProgramming,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(tag = "type", rename_all = "snake_case")]
67pub enum OptimizationRequest {
68 Assignment {
70 costs: Vec<Vec<i64>>,
72 },
73 Knapsack {
75 weights: Vec<i64>,
77 values: Vec<i64>,
79 capacity: i64,
81 },
82 ShortestPath {
84 edges: Vec<(usize, usize, i64)>,
86 source: usize,
88 target: usize,
90 },
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95#[serde(tag = "type", rename_all = "snake_case")]
96pub enum OptimizationResponse {
97 Assignment {
99 assignments: Vec<usize>,
101 total_cost: i64,
103 },
104 Knapsack {
106 selected: Vec<usize>,
108 total_value: i64,
110 total_weight: i64,
112 },
113 ShortestPath {
115 path: Vec<usize>,
117 cost: i64,
119 },
120 Error {
122 message: String,
124 },
125}
126
127#[derive(Debug, Clone)]
129pub struct OptimizationProvider {
130 pub optimization_type: OptimizationType,
132 pub params: SolverParams,
134}
135
136impl Default for OptimizationProvider {
137 fn default() -> Self {
138 Self::new(OptimizationType::Assignment)
139 }
140}
141
142impl OptimizationProvider {
143 pub fn new(optimization_type: OptimizationType) -> Self {
145 Self {
146 optimization_type,
147 params: SolverParams::default(),
148 }
149 }
150
151 pub fn with_params(mut self, params: SolverParams) -> Self {
153 self.params = params;
154 self
155 }
156
157 pub fn solve(&self, request: OptimizationRequest) -> OptimizationResponse {
159 match request {
160 OptimizationRequest::Assignment { costs } => self.solve_assignment(costs),
161 OptimizationRequest::Knapsack {
162 weights,
163 values,
164 capacity,
165 } => self.solve_knapsack(weights, values, capacity),
166 OptimizationRequest::ShortestPath {
167 edges,
168 source,
169 target,
170 } => self.solve_shortest_path(edges, source, target),
171 }
172 }
173
174 fn solve_assignment(&self, costs: Vec<Vec<i64>>) -> OptimizationResponse {
175 let problem = AssignmentProblem::from_costs(costs);
176 match assignment::solve(&problem) {
177 Ok(solution) => OptimizationResponse::Assignment {
178 assignments: solution.assignments,
179 total_cost: solution.total_cost,
180 },
181 Err(e) => OptimizationResponse::Error {
182 message: e.to_string(),
183 },
184 }
185 }
186
187 fn solve_knapsack(
188 &self,
189 weights: Vec<i64>,
190 values: Vec<i64>,
191 capacity: i64,
192 ) -> OptimizationResponse {
193 match KnapsackProblem::new(weights, values, capacity) {
194 Ok(problem) => match knapsack::solve(&problem) {
195 Ok(solution) => OptimizationResponse::Knapsack {
196 selected: solution.selected,
197 total_value: solution.total_value,
198 total_weight: solution.total_weight,
199 },
200 Err(e) => OptimizationResponse::Error {
201 message: e.to_string(),
202 },
203 },
204 Err(e) => OptimizationResponse::Error {
205 message: e.to_string(),
206 },
207 }
208 }
209
210 fn solve_shortest_path(
211 &self,
212 edges: Vec<(usize, usize, i64)>,
213 source: usize,
214 target: usize,
215 ) -> OptimizationResponse {
216 use petgraph::graph::DiGraph;
217
218 let mut graph: DiGraph<(), i64> = DiGraph::new();
220 let max_node = edges
221 .iter()
222 .flat_map(|(a, b, _)| [*a, *b])
223 .max()
224 .unwrap_or(0);
225
226 let nodes: Vec<_> = (0..=max_node).map(|_| graph.add_node(())).collect();
228
229 for (from, to, cost) in edges {
231 if from <= max_node && to <= max_node {
232 graph.add_edge(nodes[from], nodes[to], cost);
233 }
234 }
235
236 if source > max_node || target > max_node {
237 return OptimizationResponse::Error {
238 message: "source or target node out of range".to_string(),
239 };
240 }
241
242 match dijkstra::shortest_path(&graph, nodes[source], nodes[target], |&w| w) {
243 Ok(Some(path)) => OptimizationResponse::ShortestPath {
244 path: vec![source, target], cost: path.cost,
246 },
247 Ok(None) => OptimizationResponse::Error {
248 message: "no path exists".to_string(),
249 },
250 Err(e) => OptimizationResponse::Error {
251 message: e.to_string(),
252 },
253 }
254 }
255}
256
257#[derive(Clone)]
267pub struct GateProvider {
268 registry: Arc<PackRegistry>,
269}
270
271impl Default for GateProvider {
272 fn default() -> Self {
273 Self::new()
274 }
275}
276
277impl GateProvider {
278 pub fn new() -> Self {
280 Self {
281 registry: Arc::new(PackRegistry::with_builtins()),
282 }
283 }
284
285 pub fn with_registry(registry: Arc<PackRegistry>) -> Self {
287 Self { registry }
288 }
289
290 pub fn registry(&self) -> &PackRegistry {
292 &self.registry
293 }
294
295 pub fn list_packs(&self) -> Vec<&str> {
297 self.registry.list()
298 }
299
300 pub fn has_pack(&self, name: &str) -> bool {
302 self.registry.contains(name)
303 }
304
305 pub fn solve(&self, pack_name: &str, spec: &ProblemSpec) -> crate::Result<GateSolveResult> {
315 let pack = self
317 .registry
318 .get(pack_name)
319 .ok_or_else(|| crate::Error::invalid_input(format!("unknown pack: {}", pack_name)))?;
320
321 pack.validate_inputs(&spec.inputs)?;
323
324 let solve_result = pack.solve(spec)?;
326
327 let invariant_results = pack.check_invariants(&solve_result.plan)?;
329
330 let gate = pack.evaluate_gate(&solve_result.plan, &invariant_results);
332
333 Ok(GateSolveResult {
334 plan: solve_result.plan,
335 reports: solve_result.reports,
336 invariant_results,
337 gate,
338 })
339 }
340}
341
342impl std::fmt::Debug for GateProvider {
343 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
344 f.debug_struct("GateProvider")
345 .field("packs", &self.list_packs())
346 .finish()
347 }
348}
349
350#[derive(Debug)]
352pub struct GateSolveResult {
353 pub plan: ProposedPlan,
355 pub reports: Vec<SolverReport>,
357 pub invariant_results: Vec<InvariantResult>,
359 pub gate: PromotionGate,
361}
362
363impl GateSolveResult {
364 pub fn is_feasible(&self) -> bool {
366 self.reports.iter().any(|r| r.feasible)
367 }
368
369 pub fn is_promoted(&self) -> bool {
371 self.gate.is_promoted()
372 }
373
374 pub fn is_rejected(&self) -> bool {
376 self.gate.is_rejected()
377 }
378
379 pub fn requires_escalation(&self) -> bool {
381 self.gate.requires_escalation()
382 }
383
384 pub fn failed_invariants(&self) -> Vec<&str> {
386 self.invariant_results
387 .iter()
388 .filter(|r| !r.passed)
389 .map(|r| r.invariant.as_str())
390 .collect()
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397 use crate::gate::ObjectiveSpec;
398 use crate::packs::meeting_scheduler::{
399 Attendee, MeetingRequirements, MeetingSchedulerInput, SlotPreference, TimeSlot,
400 };
401
402 #[test]
403 fn test_assignment_provider() {
404 let provider = OptimizationProvider::new(OptimizationType::Assignment);
405 let request = OptimizationRequest::Assignment {
406 costs: vec![vec![10, 5], vec![3, 8]],
407 };
408
409 let response = provider.solve(request);
410 match response {
411 OptimizationResponse::Assignment { total_cost, .. } => {
412 assert_eq!(total_cost, 8);
416 }
417 _ => panic!("unexpected response"),
418 }
419 }
420
421 #[test]
422 fn test_knapsack_provider() {
423 let provider = OptimizationProvider::new(OptimizationType::Knapsack);
424 let request = OptimizationRequest::Knapsack {
425 weights: vec![10, 20, 30],
426 values: vec![60, 100, 120],
427 capacity: 50,
428 };
429
430 let response = provider.solve(request);
431 match response {
432 OptimizationResponse::Knapsack { total_value, .. } => {
433 assert_eq!(total_value, 220);
434 }
435 _ => panic!("unexpected response"),
436 }
437 }
438
439 #[test]
440 fn test_gate_provider_new() {
441 let provider = GateProvider::new();
442 assert!(provider.has_pack("meeting-scheduler"));
443 assert!(provider.has_pack("inventory-rebalancing"));
444 assert!(!provider.has_pack("nonexistent"));
445 }
446
447 #[test]
448 fn test_gate_provider_list_packs() {
449 let provider = GateProvider::new();
450 let packs = provider.list_packs();
451 assert!(packs.contains(&"meeting-scheduler"));
452 assert!(packs.contains(&"inventory-rebalancing"));
453 }
454
455 #[test]
456 fn test_gate_provider_solve_meeting_scheduler() {
457 let provider = GateProvider::new();
458
459 let input = MeetingSchedulerInput {
460 slots: vec![TimeSlot {
461 id: "slot-1".to_string(),
462 start: 1000,
463 end: 2000,
464 room: Some("Room A".to_string()),
465 capacity: 10,
466 }],
467 attendees: vec![Attendee {
468 id: "alice".to_string(),
469 name: "Alice".to_string(),
470 required: true,
471 available_slots: vec!["slot-1".to_string()],
472 preferences: vec![SlotPreference {
473 slot_id: "slot-1".to_string(),
474 score: 10.0,
475 }],
476 }],
477 requirements: MeetingRequirements {
478 duration_minutes: 60,
479 min_attendees: 1,
480 require_room: false,
481 },
482 };
483
484 let spec = ProblemSpec::builder("test-gate-001", "test-tenant")
485 .objective(ObjectiveSpec::maximize("attendance"))
486 .inputs(&input)
487 .unwrap()
488 .seed(42)
489 .build()
490 .unwrap();
491
492 let result = provider.solve("meeting-scheduler", &spec).unwrap();
493
494 assert!(result.is_feasible());
495 assert!(result.is_promoted());
496 assert!(result.failed_invariants().is_empty());
497 }
498
499 #[test]
500 fn test_gate_provider_unknown_pack() {
501 let provider = GateProvider::new();
502
503 let spec = ProblemSpec::builder("test", "tenant")
504 .objective(ObjectiveSpec::minimize("cost"))
505 .inputs_raw(serde_json::json!({}))
506 .build()
507 .unwrap();
508
509 let result = provider.solve("nonexistent-pack", &spec);
510 assert!(result.is_err());
511 }
512
513 #[test]
514 fn test_gate_solve_result_methods() {
515 let provider = GateProvider::new();
516
517 let input = MeetingSchedulerInput {
518 slots: vec![TimeSlot {
519 id: "slot-1".to_string(),
520 start: 1000,
521 end: 2000,
522 room: None,
523 capacity: 10,
524 }],
525 attendees: vec![Attendee {
526 id: "alice".to_string(),
527 name: "Alice".to_string(),
528 required: true,
529 available_slots: vec!["slot-1".to_string()],
530 preferences: vec![SlotPreference {
531 slot_id: "slot-1".to_string(),
532 score: 10.0, }],
534 }],
535 requirements: MeetingRequirements {
536 duration_minutes: 60,
537 min_attendees: 1,
538 require_room: false,
539 },
540 };
541
542 let spec = ProblemSpec::builder("test-methods", "tenant")
543 .objective(ObjectiveSpec::maximize("attendance"))
544 .inputs(&input)
545 .unwrap()
546 .seed(42)
547 .build()
548 .unwrap();
549
550 let result = provider.solve("meeting-scheduler", &spec).unwrap();
551
552 assert!(result.is_feasible());
554 assert!(result.is_promoted());
555 assert!(!result.is_rejected());
556 assert!(!result.requires_escalation());
557 }
558}