Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 41 additions & 18 deletions crates/cnvx-cli/src/solve.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
use cnvx::prelude::*;
use cnvx::lp::LpSolver; // TODO: Behind a feature flag?
use cnvx_core::solver::Solver;

/// Entry point for the `cnvx solve` command.
///
/// Reads a model from a file (or stdin), parses it using the appropriate
/// [`LanguageParser`](cnvx_parse::LanguageParser), and solves it.
///
/// Solver selection is performed without a global registry: the parsed model's
/// [`Problem::kind()`](cnvx_core::problem::Problem::kind) is matched against the
/// available domain solvers by calling [`Solver::supports`]. Currently only LP
/// problems are supported; additional domain solvers can be added to the
/// `candidates` list below as new sub-crates are introduced.
pub fn solve(
command: &crate::args::SolveCommand,
) -> Result<(), Box<dyn std::error::Error>> {
Expand Down Expand Up @@ -27,23 +38,35 @@ pub fn solve(
}
};

if let Ok(model) = cnvx_parse::parse(&contents, &ext) {
let mut solver = AutoSolver::new(&model);
// Match on the solution (Ok or Err) and print the appropriate messagej
let sol = match solver.solve() {
Ok(solution) => solution,
Err(e) => {
println!("Solver error: {}", e);
return Err("Solver error".into());
}
};

// TODO: Also support writing to a file, and saving to a file
println!("{}", sol);
} else {
println!("Failed to parse model");
return Err("Failed to parse model".into());
}
let model = cnvx_parse::parse(&contents, &ext)
.map_err(|e| format!("Failed to parse model: {e}"))?;

// Build a ranked list of candidate solvers.
//
// Each entry is tried in order; the first solver for which `supports`
// returns `true` is used. Adding a new domain (e.g. `cnvx-nlp`) means
// appending one line here and one `Cargo.toml` dependency — nothing else
// needs to change.
let mut candidates: Vec<Box<dyn Solver>> = vec![
Box::new(LpSolver::new()),
// ...
// ...
];

let solver = candidates.iter_mut().find(|s| s.supports(&model)).ok_or_else(|| {
format!(
"No solver supports '{}' problems. \
Is the required sub-crate linked?",
cnvx_core::problem::Problem::kind(&model)
)
})?;

println!("Using solver: {}", solver.name());

let solution = solver.solve(&model).map_err(|e| format!("Solver error: {e}"))?;

// TODO: Also support writing to a file.
println!("{}", solution);

Ok(())
}
66 changes: 53 additions & 13 deletions crates/cnvx-core/src/constraint.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
use crate::LinExpr;
//! Linear constraints for optimization models.

use std::fmt::Display;

use crate::LinExpr;

/// Comparison operators used in constraints.
#[derive(Copy, Clone, Debug)]
pub enum Cmp {
/// Equality: `==`
Eq,
EQ,

/// Less than or equal: `<=`
Leq,
LEQ,

/// Greater than or equal: `>=`
Geq,
GEQ,
}

/// A linear constraint of the form `expr cmp rhs`.
Expand All @@ -27,42 +30,79 @@ pub enum Cmp {
/// let c2 = Constraint::geq(expr.clone(), 1.0); // 2*x0 + 3 >= 1
/// let c3 = Constraint::eq(expr, 4.0); // 2*x0 + 3 == 4
/// ```
#[derive(Clone, Debug)]
#[derive(Debug)]
pub struct Constraint {
/// The left-hand side linear expression of the constraint.
pub expr: LinExpr,
pub expr: LinExpr, // TODO: Allow for this to be a more general expression type

/// The right-hand side value of the constraint.
pub rhs: f64,

/// The comparison operator (==, <=, >=).
pub cmp: Cmp,

/// Optional human-readable name for the constraint, used in diagnostics and
/// dual-variable reporting.
pub name: Option<String>,
}

impl Clone for Constraint {
fn clone(&self) -> Self {
// ExtensionMap does not implement Clone (its values are `dyn Any`);
// cloning a constraint preserves all fields but drops extensions.
// Sub-crates that rely on extensions should clone them explicitly.
Self {
expr: self.expr.clone(),
rhs: self.rhs,
cmp: self.cmp,
name: self.name.clone(),
}
}
}

impl Constraint {
/// Creates a `<=` constraint: `lhs <= rhs`.
pub fn leq(lhs: LinExpr, rhs: f64) -> Self {
Self { expr: lhs, rhs, cmp: Cmp::Leq }
Self { expr: lhs, rhs, cmp: Cmp::LEQ, name: None }
}

/// Creates a `>=` constraint: `lhs >= rhs`.
pub fn geq(lhs: LinExpr, rhs: f64) -> Self {
Self { expr: lhs, rhs, cmp: Cmp::Geq }
Self { expr: lhs, rhs, cmp: Cmp::GEQ, name: None }
}

/// Creates a `==` constraint: `lhs == rhs`.
pub fn eq(lhs: LinExpr, rhs: f64) -> Self {
Self { expr: lhs, rhs, cmp: Cmp::Eq }
Self { expr: lhs, rhs, cmp: Cmp::EQ, name: None }
}

/// Attaches a human-readable name to this constraint (builder-style).
///
/// # Examples
///
/// ```rust
/// # use cnvx_core::{LinExpr, VarId, Constraint};
/// let c = Constraint::leq(LinExpr::from(VarId(0)), 10.0)
/// .named("capacity");
/// assert_eq!(c.name.as_deref(), Some("capacity"));
/// ```
pub fn named(mut self, name: &str) -> Self {
self.name = Some(name.to_string());
self
}
}

impl Display for Constraint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let cmp_str = match self.cmp {
Cmp::Eq => "==",
Cmp::Leq => "<=",
Cmp::Geq => ">=",
Cmp::EQ => "==",
Cmp::LEQ => "<=",
Cmp::GEQ => ">=",
};
write!(f, "{} {} {}", self.expr, cmp_str, self.rhs)
if let Some(name) = &self.name {
write!(f, "[{}] {} {} {}", name, self.expr, cmp_str, self.rhs)
} else {
write!(f, "{} {} {}", self.expr, cmp_str, self.rhs)
}
}
}
22 changes: 20 additions & 2 deletions crates/cnvx-core/src/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ pub struct LinExpr {
pub constant: f64,
}

// TODO: Currently LinExpr only implements addition, but we want support for subtraction and negation.
// This will later likely pivot to a more general `Expr` type for non-linear support.
// TODO: This will later likely pivot to a more general `Expr` type for non-linear support.

impl LinExpr {
/// Creates a new linear expression from a single variable and coefficient.
Expand Down Expand Up @@ -167,9 +166,28 @@ impl Add<f64> for LinExpr {
}
}

/// LinExpr - LinExpr
impl std::ops::Sub for LinExpr {
type Output = LinExpr;

fn sub(self, rhs: LinExpr) -> LinExpr {
let mut terms = self.terms;
for term in rhs.terms {
terms.push(LinTerm { var: term.var, coeff: -term.coeff });
}
LinExpr { terms, constant: self.constant - rhs.constant }
}
}

/// Allows converting a single variable into a linear expression with coefficient 1.0.
impl From<VarId> for LinExpr {
fn from(var: VarId) -> Self {
LinExpr::new(var, 1.0)
}
}

impl From<f64> for LinExpr {
fn from(c: f64) -> Self {
LinExpr::constant(c)
}
}
4 changes: 3 additions & 1 deletion crates/cnvx-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
//!
//! # Modules
//!
//! - [`constraint`]: Defines linear constraints and comparison operators ([`Eq`](Cmp::Eq), [`Leq`](Cmp::Leq), [`Geq`](Cmp::Geq)).
//! - [`constraint`]: Defines linear constraints and comparison operators ([`EQ`](Cmp::EQ), [`LEQ`](Cmp::LEQ), [`GEQ`](Cmp::GEQ)).
//! - [`expr`]: Linear expressions ([`LinExpr`]) and terms ([`LinTerm`]) for building objectives and constraints.
//! - [`model`]: The [`Model`] struct, containing variables, constraints, and objectives.
//! - [`objective`]: Objective functions ([`Objective`]) and builder API.
Expand All @@ -23,6 +23,7 @@ pub mod constraint;
pub mod expr;
pub mod model;
pub mod objective;
pub mod problem;
pub mod solution;
pub mod solver;
pub mod status;
Expand All @@ -33,6 +34,7 @@ pub use constraint::*;
pub use expr::*;
pub use model::*;
pub use objective::*;
pub use problem::*;
pub use solution::*;
pub use solver::*;
pub use status::*;
Expand Down
Loading
Loading