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