Skip to main content

converge_optimization/provider/
mod.rs

1//! Converge platform integration
2//!
3//! This module provides the interface for using optimization
4//! algorithms as converge-provider capabilities.
5//!
6//! ## Usage in Converge
7//!
8//! ```rust,ignore
9//! use converge_optimization::provider::{OptimizationProvider, OptimizationType};
10//!
11//! // Register as a capability
12//! let provider = OptimizationProvider::new(OptimizationType::Assignment);
13//! capability_registry.register("optimize.assignment", provider);
14//! ```
15//!
16//! ## Gate-Based Solving
17//!
18//! For domain-specific optimization with invariants and promotion gates:
19//!
20//! ```rust,ignore
21//! use converge_optimization::provider::GateProvider;
22//! use converge_optimization::gate::ProblemSpec;
23//!
24//! let provider = GateProvider::new();
25//! let result = provider.solve("meeting-scheduler", &spec)?;
26//! assert!(result.gate.is_promoted());
27//! ```
28
29use 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/// Types of optimization available
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum OptimizationType {
44    /// Linear assignment problem
45    Assignment,
46    /// Knapsack problem
47    Knapsack,
48    /// Shortest path
49    ShortestPath,
50    /// Max flow
51    MaxFlow,
52    /// Min cost flow
53    MinCostFlow,
54    /// Set cover
55    SetCover,
56    /// Scheduling
57    Scheduling,
58    /// Vehicle routing (requires FFI)
59    VehicleRouting,
60    /// Constraint programming (requires FFI)
61    ConstraintProgramming,
62}
63
64/// Request for optimization
65#[derive(Debug, Clone, Serialize, Deserialize)]
66#[serde(tag = "type", rename_all = "snake_case")]
67pub enum OptimizationRequest {
68    /// Assignment problem
69    Assignment {
70        /// Cost matrix
71        costs: Vec<Vec<i64>>,
72    },
73    /// Knapsack problem
74    Knapsack {
75        /// Item weights
76        weights: Vec<i64>,
77        /// Item values
78        values: Vec<i64>,
79        /// Capacity
80        capacity: i64,
81    },
82    /// Shortest path problem
83    ShortestPath {
84        /// Edges as (from, to, cost)
85        edges: Vec<(usize, usize, i64)>,
86        /// Source node
87        source: usize,
88        /// Target node
89        target: usize,
90    },
91}
92
93/// Response from optimization
94#[derive(Debug, Clone, Serialize, Deserialize)]
95#[serde(tag = "type", rename_all = "snake_case")]
96pub enum OptimizationResponse {
97    /// Assignment solution
98    Assignment {
99        /// Assignment: assignments[agent] = task
100        assignments: Vec<usize>,
101        /// Total cost
102        total_cost: i64,
103    },
104    /// Knapsack solution
105    Knapsack {
106        /// Selected items
107        selected: Vec<usize>,
108        /// Total value
109        total_value: i64,
110        /// Total weight
111        total_weight: i64,
112    },
113    /// Shortest path solution
114    ShortestPath {
115        /// Path nodes
116        path: Vec<usize>,
117        /// Total cost
118        cost: i64,
119    },
120    /// Error response
121    Error {
122        /// Error message
123        message: String,
124    },
125}
126
127/// Optimization provider for converge platform
128#[derive(Debug, Clone)]
129pub struct OptimizationProvider {
130    /// Type of optimization
131    pub optimization_type: OptimizationType,
132    /// Solver parameters
133    pub params: SolverParams,
134}
135
136impl Default for OptimizationProvider {
137    fn default() -> Self {
138        Self::new(OptimizationType::Assignment)
139    }
140}
141
142impl OptimizationProvider {
143    /// Create a new provider
144    pub fn new(optimization_type: OptimizationType) -> Self {
145        Self {
146            optimization_type,
147            params: SolverParams::default(),
148        }
149    }
150
151    /// Set solver parameters
152    pub fn with_params(mut self, params: SolverParams) -> Self {
153        self.params = params;
154        self
155    }
156
157    /// Solve an optimization problem
158    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        // Build graph
219        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        // Add nodes
227        let nodes: Vec<_> = (0..=max_node).map(|_| graph.add_node(())).collect();
228
229        // Add edges
230        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], // Simplified
245                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// ============================================================================
258// Gate-Based Provider
259// ============================================================================
260
261/// Gate-based optimization provider
262///
263/// Provides access to domain packs through the solver gate architecture.
264/// Each solve returns a complete result including the proposed plan,
265/// solver reports, invariant checks, and promotion gate decision.
266#[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    /// Create with default built-in packs
279    pub fn new() -> Self {
280        Self {
281            registry: Arc::new(PackRegistry::with_builtins()),
282        }
283    }
284
285    /// Create with custom registry
286    pub fn with_registry(registry: Arc<PackRegistry>) -> Self {
287        Self { registry }
288    }
289
290    /// Get the underlying registry
291    pub fn registry(&self) -> &PackRegistry {
292        &self.registry
293    }
294
295    /// List available packs
296    pub fn list_packs(&self) -> Vec<&str> {
297        self.registry.list()
298    }
299
300    /// Check if a pack is available
301    pub fn has_pack(&self, name: &str) -> bool {
302        self.registry.contains(name)
303    }
304
305    /// Solve a problem through the gate
306    ///
307    /// This method:
308    /// 1. Validates inputs against the pack schema
309    /// 2. Solves the problem using the pack's solver
310    /// 3. Checks all invariants
311    /// 4. Evaluates the promotion gate
312    ///
313    /// Returns a complete result including all artifacts.
314    pub fn solve(&self, pack_name: &str, spec: &ProblemSpec) -> crate::Result<GateSolveResult> {
315        // Get the pack
316        let pack = self
317            .registry
318            .get(pack_name)
319            .ok_or_else(|| crate::Error::invalid_input(format!("unknown pack: {}", pack_name)))?;
320
321        // Validate inputs against pack schema
322        pack.validate_inputs(&spec.inputs)?;
323
324        // Solve
325        let solve_result = pack.solve(spec)?;
326
327        // Check invariants
328        let invariant_results = pack.check_invariants(&solve_result.plan)?;
329
330        // Evaluate gate
331        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/// Complete result from gate-based solving
351#[derive(Debug)]
352pub struct GateSolveResult {
353    /// The proposed plan
354    pub plan: ProposedPlan,
355    /// Solver reports (may have tried multiple solvers)
356    pub reports: Vec<SolverReport>,
357    /// Results of invariant checks
358    pub invariant_results: Vec<InvariantResult>,
359    /// The promotion gate decision
360    pub gate: PromotionGate,
361}
362
363impl GateSolveResult {
364    /// Check if the solution is feasible
365    pub fn is_feasible(&self) -> bool {
366        self.reports.iter().any(|r| r.feasible)
367    }
368
369    /// Check if the plan was promoted
370    pub fn is_promoted(&self) -> bool {
371        self.gate.is_promoted()
372    }
373
374    /// Check if the plan was rejected
375    pub fn is_rejected(&self) -> bool {
376        self.gate.is_rejected()
377    }
378
379    /// Check if escalation is required
380    pub fn requires_escalation(&self) -> bool {
381        self.gate.requires_escalation()
382    }
383
384    /// Get failed invariants
385    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                // (0,1)=5, (1,0)=3 -> 8
413                // (0,0)=10, (1,1)=8 -> 18
414                // Optimal is 8
415                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, // Add preference to pass advisory invariant
533                }],
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        // Test all helper methods
553        assert!(result.is_feasible());
554        assert!(result.is_promoted());
555        assert!(!result.is_rejected());
556        assert!(!result.requires_escalation());
557    }
558}