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