oximo-solver 0.4.0

Solver-trait abstraction for oximo backends
Documentation
# oximo-solver

Solver trait, result types, status codes, and shared option building blocks for [oximo](https://github.com/oximo-rs/oximo).

This crate defines the contract that backend crates implement. End users interact with concrete backends (`Highs`, `Gurobi`, `Gams`) exposed by the umbrella `oximo` crate, they do not depend on this crate directly unless they are writing a new backend.

## Usage

```toml
[dependencies]
oximo-solver = "0.4"
oximo-core   = "0.4"
```

## `Solver` trait

```rust,ignore
pub trait Solver {
    type Options;

    fn name(&self) -> &str;
    fn supports(&self, kind: ModelKind) -> bool;
    fn solve(&mut self, model: &Model, opts: &Self::Options) -> Result<SolverResult, SolverError>;
}
```

Each backend defines its own `Options` type. Users get compile-time validation and LSP autocomplete on the options that actually apply to that backend.

## `SolverResult`

`termination` records why the solve stopped, while `primal_status` records whether a usable
point came back.

| Field           | Type                           | Description                                       |
|-----------------|--------------------------------|---------------------------------------------------|
| `termination`   | `TerminationStatus`            | Why the solve stopped                             |
| `primal_status` | `PrimalStatus`                 | Whether a usable primal point is available        |
| `solutions`     | `Vec<SolutionPoint>`           | Primal points, best first (empty if no solution)  |
| `dual`          | `FxHashMap<ConstraintId, f64>` | Constraint duals at the best point                |
| `reduced_costs` | `FxHashMap<VarId, f64>`        | Variable reduced costs at the best point          |
| `best_bound`    | `Option<f64>`                  | Dual/relaxation bound (branch-and-bound backends) |
| `gap`           | `Option<f64>`                  | Relative optimality gap, when reported            |
| `solve_time`    | `Duration`                     | Wall time around the solve call                   |
| `iterations`    | `u64`                          | Simplex iteration count (if reported)             |
| `raw_log`       | `Option<String>`               | Solver stdout/stderr                              |

Each `SolutionPoint` holds the `primal` variable values (`FxHashMap<VarId, f64>`) and that point's `objective` (`Option<f64>`). Index `0` is the best/incumbent. Backends with solution pools return the extra points after it.

### Accessors

```rust,ignore
result.objective()    // Option<f64>, best solution's objective
result.value_of(expr) // Option<f64>, primal value for a Var expr (best solution)
result.value(var_id)  // Option<f64>, primal value by VarId (best solution)
result.dual_of(c_id)  // Option<f64>, dual for a constraint
result.best()         // Option<&SolutionPoint>, same as .solution(0)
result.solution(i)    // Option<&SolutionPoint>, i-th pooled point
result.result_count() // usize, number of returned points
result.has_solution() // true when a usable primal point is available
result.report(&model) // Display: model-aware summary (status, vars, duals)

// Indexed variables
result.value_of_idx(&flow, "nyc")                  // Option<f64>, value at a specific index
result.values_of(&flow)                            // Iterator<(&IndexKey, f64)>, all entries with a primal value
result.values_of(&flow).filter(|(_, v)| *v != 0.0) // nonzero only (sparse solutions)
```

## `TerminationStatus`

Why the solve stopped (complementary to whether a point was returned).

| Variant                 | Meaning                                                |
|-------------------------|--------------------------------------------------------|
| `Optimal`               | Proven globally optimal                                |
| `LocallyOptimal`        | A local optimum                                        |
| `Infeasible`            | No feasible solution exists                            |
| `Unbounded`             | Objective is unbounded                                 |
| `InfeasibleOrUnbounded` | Infeasible or unbounded; solver could not tell which   |
| `IterationLimit`        | Stopped at an iteration limit                          |
| `TimeLimit`             | Stopped at a time limit                                |
| `NodeLimit`             | Stopped at a branch-and-bound node limit               |
| `Interrupted`           | Stopped early (user limit/interrupt, sub-optimal stop) |
| `NumericError`          | Solver reported numerical difficulties                 |
| `NotSolved`             | Default, solve not yet called                          |
| `Other(String)`         | Backend-specific status not covered above              |

## `PrimalStatus`

| Variant         | Meaning                                           |
|-----------------|---------------------------------------------------|
| `NoSolution`    | No primal point is available                      |
| `FeasiblePoint` | A feasible point is available, not proven optimal |
| `OptimalPoint`  | A proven-optimal point is available               |

## `SolverError`

| Variant                      | Cause                                                     |
|------------------------------|-----------------------------------------------------------|
| `UnsupportedKind(ModelKind)` | Backend does not support this model kind                  |
| `NoObjective`                | Model has no objective set                                |
| `Nonlinear`                  | Backend cannot handle nonlinear expressions               |
| `Backend(String)`            | Backend-reported error (e.g. license failure, bad option) |
| `Core(Error)`                | Error from `oximo-core`                                   |

## Universal options

All backend options structs embed `UniversalOptions` and implement `HasUniversal`, which enables the `UniversalOptionsExt` blanket impl:

```rust,ignore
use oximo_solver::UniversalOptionsExt;
use std::time::Duration;

let opts = MyBackendOptions::default()
    .time_limit(Duration::from_secs(120))
    .threads(4)
    .verbose(true);
```

| Method                  | Field        | Type               |
|-------------------------|--------------|--------------------|
| `.time_limit(Duration)` | `time_limit` | `Option<Duration>` |
| `.threads(u32)`         | `threads`    | `Option<u32>`      |
| `.verbose(bool)`        | `verbose`    | `Option<bool>`     |

### Implementing `HasUniversal` for a new backend

```rust
use oximo_solver::{HasUniversal, UniversalOptions};

#[derive(Default)]
pub struct MyOptions {
    universal: UniversalOptions,
    // backend-specific fields ...
}

impl HasUniversal for MyOptions {
    fn universal(&self) -> &UniversalOptions { &self.universal }
    fn universal_mut(&mut self) -> &mut UniversalOptions { &mut self.universal }
}
```

## Writing a new backend

Mirror the layout of an existing backend crate (`oximo-highs`, `oximo-gurobi`, `oximo-gams`):

1. `lib.rs`: public struct + `impl Solver`. `supports()` declares which `ModelKind`s are handled. `solve()` delegates to `translate::solve`.
2. `options.rs`: converts `MyOptions` into the backend's native option calls.
3. `translate.rs`: `Model` -> backend conversion and result extraction.

Add an optional dep + feature in `oximo/Cargo.toml` and re-export the type under `oximo::solvers`.

## License

MIT OR Apache-2.0