#![cfg(feature = "gams")]
use std::time::Duration;
use oximo::gams::{GamsCplexOptions, GamsHighsOptions, GamsHighsSolver, GamsSolverConfig};
use oximo::prelude::*;
use oximo::solvers::Gams;
#[test]
fn gams_lp_canonical() {
let m = Model::new("lp");
variable!(m, x >= 0.0);
variable!(m, 0.0 <= y <= 4.0);
constraint!(m, c1, x + 2.0 * y <= 14.0);
constraint!(m, c2, 3.0 * x - y >= 0.0);
constraint!(m, c3, x - y <= 2.0);
objective!(m, Max, 3.0 * x + 4.0 * y);
let opts = GamsOptions::default().time_limit(Duration::from_secs(60));
let result = Gams::new().solve(&m, &opts).unwrap();
assert_eq!(result.status, SolverStatus::Optimal);
assert!((result.objective().unwrap() - 34.0).abs() < 1e-4, "obj={:?}", result.objective());
assert!((result.value_of(x).unwrap() - 6.0).abs() < 1e-4);
assert!((result.value_of(y).unwrap() - 4.0).abs() < 1e-4);
}
#[test]
fn gams_lp_duals_and_reduced_costs() {
let m = Model::new("lp_dual");
variable!(m, x >= 0.0);
let c = constraint!(m, cap, x <= 5.0);
objective!(m, Max, x);
let opts = GamsOptions::default().time_limit(Duration::from_secs(30));
let result = Gams::new().solve(&m, &opts).unwrap();
assert_eq!(result.status, SolverStatus::Optimal);
assert!((result.objective().unwrap() - 5.0).abs() < 1e-6);
let d = result.dual_of(c).expect("dual missing for cap constraint");
assert!((d.abs() - 1.0).abs() < 1e-6, "dual={d}");
let rc = result.reduced_costs.get(&VarId(0)).copied().expect("reduced cost missing for x");
assert!(rc.abs() < 1e-6, "reduced_cost(x)={rc}");
}
#[test]
fn gams_nlp_duals_at_local_point() {
let m = Model::new("nlp_dual");
variable!(m, -10.0 <= x <= 10.0);
let cap = constraint!(m, cap, x >= 1.0);
objective!(m, Min, x.exp());
let opts = GamsOptions::default().time_limit(Duration::from_secs(30));
let result = Gams::new().solve(&m, &opts).unwrap();
assert!(
matches!(result.status, SolverStatus::Optimal | SolverStatus::Feasible),
"status={:?}",
result.status
);
assert!((result.objective().unwrap() - std::f64::consts::E).abs() < 1e-5);
assert!((result.value_of(x).unwrap() - 1.0).abs() < 1e-5);
let dual = result.dual_of(cap).expect("dual missing for cap");
assert!((dual.abs() - std::f64::consts::E).abs() < 1e-5, "dual={dual}");
let rc = result.reduced_costs.get(&VarId(0)).copied().expect("reduced cost missing for x");
assert!(rc.abs() < 1e-5, "reduced_cost(x)={rc}");
}
#[test]
fn gams_mip_duals_at_fixed_point() {
let m = Model::new("mip_dual");
variable!(m, a, Bin);
variable!(m, b, Bin);
let cap = constraint!(m, cap, a + b <= 1.0);
objective!(m, Max, 2.0 * a + 3.0 * b);
let opts = GamsOptions::default().time_limit(Duration::from_secs(30));
let result = Gams::new().solve(&m, &opts).unwrap();
assert_eq!(result.status, SolverStatus::Optimal);
assert!((result.objective().unwrap() - 3.0).abs() < 1e-6);
assert!(result.dual_of(cap).is_some(), "dual missing for cap");
assert!(!result.reduced_costs.is_empty(), "reduced costs missing");
}
#[test]
fn gams_knapsack_milp() {
let weights = [3.0, 4.0, 2.0, 5.0, 1.0, 6.0, 7.0, 2.0];
let values = [10.0, 12.0, 5.0, 14.0, 3.0, 18.0, 22.0, 6.0];
let n = weights.len();
let m = Model::new("knapsack");
variable!(m, x[i in 0..n], Bin);
constraint!(m, cap, sum!(weights[i] * x[i] for i in 0..n) <= 15.0);
objective!(m, Max, sum!(values[i] * x[i] for i in 0..n));
let opts = GamsOptions::default().time_limit(Duration::from_secs(60));
let result = Gams::new().solve(&m, &opts).unwrap();
assert_eq!(result.status, SolverStatus::Optimal);
assert!((result.objective().unwrap() - 47.0).abs() < 1e-4, "obj={:?}", result.objective());
}
#[test]
fn gams_infeasible_returns_status() {
let m = Model::new("infeas");
variable!(m, 0.0 <= x <= 1.0);
constraint!(m, c1, x >= 5.0);
objective!(m, Min, x);
let opts = GamsOptions::default().time_limit(Duration::from_secs(30));
let result = Gams::new().solve(&m, &opts).unwrap();
assert_eq!(result.status, SolverStatus::Infeasible);
}
#[test]
fn gams_mip_gap_option() {
let weights = [3.0, 4.0, 2.0, 5.0, 1.0];
let values = [10.0, 12.0, 5.0, 14.0, 3.0];
let n = weights.len();
let m = Model::new("ks");
variable!(m, x[i in 0..n], Bin);
constraint!(m, cap, sum!(weights[i] * x[i] for i in 0..n) <= 8.0);
objective!(m, Max, sum!(values[i] * x[i] for i in 0..n));
let result = Gams::new().solve(&m, &GamsOptions::default().mip_gap(0.5)).unwrap();
assert!(
matches!(result.status, SolverStatus::Optimal | SolverStatus::Feasible),
"unexpected status: {:?}",
result.status
);
assert!(result.objective().unwrap() > 0.0);
}
#[test]
fn gams_highs_opt_file_simplex() {
let m = Model::new("lp");
variable!(m, x >= 0.0);
variable!(m, 0.0 <= y <= 4.0);
constraint!(m, c1, x + 2.0 * y <= 14.0);
constraint!(m, c2, 3.0 * x - y >= 0.0);
constraint!(m, c3, x - y <= 2.0);
objective!(m, Max, 3.0 * x + 4.0 * y);
let opts = GamsOptions::default().solver(GamsSolverConfig::Highs(GamsHighsOptions {
solver: Some(GamsHighsSolver::Simplex),
..Default::default()
}));
let result = Gams::new().solve(&m, &opts).unwrap();
assert_eq!(result.status, SolverStatus::Optimal);
assert!((result.objective().unwrap() - 34.0).abs() < 1e-4, "obj={:?}", result.objective());
}
#[test]
fn gams_multi_optima_returns_single_best() {
let m = Model::new("multi");
let items = Set::range(0..4usize);
variable!(m, x[i in items], Bin);
constraint!(m, cap, sum!(x[i] for i in items) <= 2.0);
objective!(m, Max, sum!(x[i] for i in items));
let opts = GamsOptions::default().time_limit(Duration::from_secs(60));
let r = Gams::new().solve(&m, &opts).unwrap();
assert_eq!(r.status, SolverStatus::Optimal);
assert_eq!(r.result_count(), 1);
assert!((r.objective().unwrap() - 2.0).abs() < 1e-4);
let chosen: f64 = (0..4).filter_map(|i| r.value_of_idx(&x, i)).sum();
assert!((chosen - 2.0).abs() < 1e-4, "best is not an optimum: sum={chosen}");
}
#[test]
fn gams_reads_cplex_solution_pool() {
let m = Model::new("multi");
let items = Set::range(0..4usize);
variable!(m, x[i in items], Bin);
constraint!(m, cap, sum!(x[i] for i in items) <= 2.0);
objective!(m, Max, sum!(x[i] for i in items));
let cfg = GamsSolverConfig::Cplex(GamsCplexOptions {
raw: vec![
"solnpool oximo_pool.gdx".into(),
"solnpoolpop 2".into(),
"populatelim 20".into(),
],
..Default::default()
});
let opts = GamsOptions::default().solver(cfg).time_limit(Duration::from_secs(60));
let r = Gams::new().solve(&m, &opts).unwrap();
assert_eq!(r.status, SolverStatus::Optimal);
assert!(r.result_count() > 1, "expected a solution pool, got {}", r.result_count());
assert!((r.objective().unwrap() - 2.0).abs() < 1e-4);
let mut prev = f64::INFINITY;
for s in &r.solutions {
let chosen: f64 = (0..4).filter_map(|i| s.value_of_idx(&x, i)).sum();
assert!(chosen <= 2.0 + 1e-6, "infeasible pool point: sum={chosen}");
let obj = s.objective.expect("pool point has an objective");
assert!(obj <= prev + 1e-9, "pool not ordered best-first");
prev = obj;
}
}