diff --git a/.gitmodules b/.gitmodules index 7209310..5e4464f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "HiGHS"] path = HiGHS - url = git@github.com:ERGO-Code/HiGHS.git + url = https://github.com/ERGO-Code/HiGHS.git diff --git a/Cargo.toml b/Cargo.toml index 5eaeb10..00e8856 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,3 +23,13 @@ build = ["dep:cmake"] highs_release = [] ninja = [] libz = [] +# Enable HiGHS' HiPO interior-point solver (HiGHS >= 1.14). Requires the +# `build` feature: HiPO is wired up via cmake (-DHIPO=ON) and needs a BLAS. +# +# The BLAS backend is fully configurable through HiGHS' own cmake, exposed +# via environment variables (see README): HIGHS_BUILD_OPENBLAS, +# HIGHS_BLA_VENDOR, HIGHS_BLAS_ROOT, HIGHS_NO_AVX512 and HIGHS_BLAS_LINK. +# By default, HiGHS fetches and builds OpenBLAS (a convenient fallback when +# no system BLAS is available), except on Apple targets where the system +# Accelerate framework is used. +hipo = ["build"] diff --git a/README.md b/README.md index 6eb1c83..4f0528a 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,42 @@ Note that at the time of writing, HiGHS is packaged in few package managers, so `libz`: enable HiGHS libz linking to enable support for reading 'mps.gz'; only takes effect when `build` is enabled. `ninja`: set CMake generator to Ninja; only takes effect when `build` is enabled. `discover`: use pkg-config to discover and link against an already installed version of HiGHS; takes precedence over `build` if both are enabled +`hipo`: build and link HiGHS' HiPO interior-point solver (HiGHS >= 1.14); only takes effect when `build` is enabled. See below. + +### HiPO interior-point solver + +Enabling the `hipo` feature compiles HiGHS with `-DHIPO=ON` and links its +Apache/BSD-licensed dependencies (the `highs_extras` library, built statically) +along with a BLAS backend. Once enabled, the solver can be selected at runtime +by setting the `solver` option to `"hipo"`. + +HiPO requires a BLAS. Rather than hard-coding one strategy, the build defers to +HiGHS' own CMake (`FindHipoDeps.cmake`) to **discover** a system BLAS, and only +builds a bundled OpenBLAS when none is found. The library that HiGHS' CMake +actually resolves is read back from `CMakeCache.txt` and linked, so the Rust +link line follows CMake's decision. + +Default behaviour: + +- **Apple targets** use the system Accelerate framework (always present). +- **Other targets** let HiGHS' CMake look for an installed BLAS (an OpenBLAS + CMake config package, `find_package(BLAS)`, MKL, …). **If no system BLAS is + found, HiGHS fetches and builds a static OpenBLAS automatically** (via CMake + `FetchContent`) — a convenient fallback when nothing is installed. This may + run CMake twice: a discovery probe, then the OpenBLAS build. + +You can pin or redirect any of this through environment variables: + +| Environment variable | Effect | +| --- | --- | +| `HIGHS_BUILD_OPENBLAS` | `1` forces building the bundled OpenBLAS (skips discovery); `0` forces discovery only (no fallback — the build fails if no BLAS is found). Unset = discover-then-fallback (default). Ignored on Apple. | +| `HIGHS_BLA_VENDOR` | Sets CMake `BLA_VENDOR` to select a system BLAS (e.g. `OpenBLAS`, `Intel10_64lp` for MKL). | +| `HIGHS_BLAS_ROOT` | Sets CMake `BLAS_ROOT` to point at a custom BLAS install; its `lib`/`lib64` dirs are added to the link search path. | +| `HIGHS_NO_AVX512` | `1` — build OpenBLAS without AVX512 (works around register-spill issues on some CPUs). | +| `HIGHS_BLAS_LINK` | Explicit link spec(s), space/comma/semicolon separated, e.g. `static=openblas`, `dylib=mkl_rt`, `framework=Accelerate`. Overrides the resolved link line entirely — the escape hatch for setups the automatic detection can't express. | + +Note that HiPO's dependencies carry Apache/BSD licenses, distinct from HiGHS' +MIT license; see HiGHS' `THIRD_PARTY_NOTICES.md`. ## Example diff --git a/build.rs b/build.rs index 8f1fbee..be7d449 100644 --- a/build.rs +++ b/build.rs @@ -87,11 +87,365 @@ fn generate_bindings<'a>( .expect("Couldn't write bindings!"); } +/// Read an environment variable that configures the HiPO BLAS backend, +/// registering a `rerun-if-env-changed` directive so cargo rebuilds when it +/// changes. Empty values are treated as unset. +#[cfg(feature = "build")] +fn hipo_env(key: &str) -> Option { + println!("cargo:rerun-if-env-changed={key}"); + env::var(key).ok().filter(|v| !v.trim().is_empty()) +} + +/// Interpret an environment variable as a boolean flag (accepts +/// `1/on/true/yes`, case-insensitive). +#[cfg(feature = "build")] +fn hipo_env_bool(key: &str) -> Option { + hipo_env(key).map(|v| matches!(v.trim().to_ascii_lowercase().as_str(), "1" | "on" | "true" | "yes")) +} + +/// How HiGHS should obtain the BLAS backend that HiPO links against. +/// +/// The default (`DiscoverOrBuild`) defers to HiGHS' own cmake +/// (`FindHipoDeps.cmake`) to find a system BLAS, and only builds a bundled +/// OpenBLAS when none is found — rather than hard-coding a single strategy. +#[cfg(feature = "build")] +#[derive(Clone, Copy)] +enum HipoBlas { + /// The `hipo` feature is disabled; no BLAS is involved. + Disabled, + /// Let HiGHS' cmake discover a system BLAS (Accelerate on Apple, an + /// installed OpenBLAS/BLAS/MKL elsewhere). No OpenBLAS is built. + Discover, + /// Have HiGHS fetch and build its own static OpenBLAS. + BuildOpenblas, + /// Try `Discover` first; if cmake finds no BLAS, fall back to + /// `BuildOpenblas`. The default on non-Apple targets. + DiscoverOrBuild, +} + +/// Configure HiGHS' HiPO interior-point solver via cmake and decide how its +/// BLAS backend is obtained. +/// +/// The guiding principle is to defer to HiGHS' own cmake (`FindHipoDeps.cmake`) +/// for BLAS handling rather than hard-coding it: by default we let HiGHS +/// *discover* a system BLAS and only build a bundled OpenBLAS when none is +/// found. The user can pin the behaviour or point HiPO at a specific BLAS via +/// environment variables (`HIGHS_BUILD_OPENBLAS`, `HIGHS_BLA_VENDOR`, +/// `HIGHS_BLAS_ROOT`, `HIGHS_NO_AVX512`, `HIGHS_BLAS_LINK`). +/// +/// Returns the chosen plan, which drives the (possibly two-pass) build and the +/// link step. Returns `HipoBlas::Disabled` when the `hipo` feature is off. +#[cfg(feature = "build")] +#[cfg_attr(not(feature = "hipo"), allow(unused_variables))] +fn configure_hipo(dst: &mut cmake::Config, apple: bool) -> HipoBlas { + if !cfg!(feature = "hipo") { + return HipoBlas::Disabled; + } + + dst.define("HIPO", "ON"); + + // HiGHS ships its Apache/BSD-licensed HiPO dependencies ("extras") in a + // separate library that defaults to a *shared* `highs_extras` dylib loaded + // at runtime via dlopen. That doesn't work for a statically-linked -sys + // crate (the dylib wouldn't sit next to the consumer's binary), so build it + // statically and link it in below: HiGHS then compiles the extras straight + // into the build instead of dynamically loading them. + dst.define("BUILD_SHARED_EXTRAS_LIB", "OFF"); + + // Pass through the standard HiGHS/CMake BLAS knobs when the user set them. + // These feed HiGHS' own discovery logic in FindHipoDeps.cmake. + if let Some(vendor) = hipo_env("HIGHS_BLA_VENDOR") { + dst.define("BLA_VENDOR", vendor); + } + if let Some(root) = hipo_env("HIGHS_BLAS_ROOT") { + dst.define("BLAS_ROOT", root); + } + if hipo_env_bool("HIGHS_NO_AVX512") == Some(true) { + dst.define("NO_AVX512", "ON"); + } + + // Decide how BLAS is obtained: + // - Apple always uses the system Accelerate framework (HiGHS forces + // BUILD_OPENBLAS off), so plain discovery. + // - If the user pinned HIGHS_BUILD_OPENBLAS, honour it exactly. + // - Otherwise default to "discover a system BLAS, build OpenBLAS if none is + // found". + match (apple, hipo_env_bool("HIGHS_BUILD_OPENBLAS")) { + (true, _) => HipoBlas::Discover, + (false, Some(true)) => HipoBlas::BuildOpenblas, + (false, Some(false)) => HipoBlas::Discover, + (false, None) => HipoBlas::DiscoverOrBuild, + } +} + +/// Run the cmake build honouring the BLAS `plan`, retrying with a bundled +/// OpenBLAS if system-BLAS discovery fails. Returns the install prefix and +/// whether OpenBLAS ended up being built (which the link step needs to know). +#[cfg(feature = "build")] +fn build_highs_with_blas(dst: &mut cmake::Config, plan: HipoBlas) -> (PathBuf, bool) { + match plan { + HipoBlas::Disabled => (dst.build(), false), + HipoBlas::Discover => { + dst.define("BUILD_OPENBLAS", "OFF"); + (dst.build(), false) + } + HipoBlas::BuildOpenblas => { + dst.define("BUILD_OPENBLAS", "ON"); + (dst.build(), true) + } + HipoBlas::DiscoverOrBuild => { + // First let HiGHS try to discover an installed BLAS. + dst.define("BUILD_OPENBLAS", "OFF"); + match try_cmake_build(dst) { + Ok(out) => (out, false), + Err(()) => { + println!( + "cargo:warning=highs-sys: HiGHS' cmake found no system BLAS; building the bundled OpenBLAS instead. Set HIGHS_BLA_VENDOR/HIGHS_BLAS_ROOT to use a system BLAS, or HIGHS_BUILD_OPENBLAS=1 to skip this probe." + ); + // Reconfigure from a clean build tree so NOTFOUND cache + // entries from the failed probe don't leak into the build. + clean_cmake_build_dir(); + dst.define("BUILD_OPENBLAS", "ON"); + (dst.build(), true) + } + } + } + } +} + +/// Run `cmake::Config::build`, catching the panic the `cmake` crate raises when +/// the configure/build step fails, so the caller can fall back. The panic +/// message is silenced for the duration so a successful fallback doesn't print +/// an alarming Rust backtrace (cmake's own error text is still shown). +#[cfg(feature = "build")] +fn try_cmake_build(dst: &mut cmake::Config) -> Result { + use std::panic::{catch_unwind, AssertUnwindSafe}; + let prev_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(|_| {})); + let result = catch_unwind(AssertUnwindSafe(|| dst.build())); + std::panic::set_hook(prev_hook); + result.map_err(|_| ()) +} + +/// Remove the cmake build tree (`$OUT_DIR/build`) so a retry reconfigures from +/// scratch. +#[cfg(feature = "build")] +fn clean_cmake_build_dir() { + let build_dir = PathBuf::from(env::var("OUT_DIR").unwrap()).join("build"); + let _ = std::fs::remove_dir_all(build_dir); +} + +/// Emit the link directives for the BLAS backend HiPO was built against. +/// +/// `HIGHS_BLAS_LINK` is an escape hatch that always wins: a space/comma/ +/// semicolon separated list of `rustc-link-lib` specs (e.g. `static=openblas`, +/// `dylib=mkl_rt`, `framework=Accelerate`). Otherwise: if OpenBLAS was built, +/// link the bundled static archive; if a system BLAS was discovered, link +/// whatever HiGHS' cmake resolved (read back from `CMakeCache.txt`). +#[cfg(feature = "build")] +#[cfg_attr(not(feature = "hipo"), allow(unused_variables))] +fn link_hipo_blas(out_dir: &Path, apple: bool, built_openblas: bool) { + if !cfg!(feature = "hipo") { + return; + } + + // The static `highs_extras` library (HiPO's Apache/BSD-licensed deps: AMD, + // METIS, RCM, cblas shims) is installed into the HiGHS prefix and must be + // linked explicitly. It must come *before* BLAS on the link line since it + // references BLAS symbols. + println!("cargo:rustc-link-lib=static=highs_extras"); + + // A BLAS_ROOT install lives outside the HiGHS prefix, so add its lib dirs. + if let Some(root) = hipo_env("HIGHS_BLAS_ROOT") { + println!("cargo:rustc-link-search=native={root}/lib"); + println!("cargo:rustc-link-search=native={root}/lib64"); + } + + // Explicit override wins over everything. + if let Some(spec) = hipo_env("HIGHS_BLAS_LINK") { + for lib in spec + .split([',', ';', ' ']) + .map(str::trim) + .filter(|s| !s.is_empty()) + { + println!("cargo:rustc-link-lib={lib}"); + } + return; + } + + if built_openblas { + // HiGHS installed the OpenBLAS static lib into the same prefix, whose + // lib/ and lib64/ dirs are already on the link search path. + // + // We deliberately do NOT add OpenBLAS's transitive deps (pthread, libm) + // here. They conceptually belong to OpenBLAS's CMake target, but a -sys + // crate links the static archives directly rather than through CMake's + // link interface, so that information doesn't reach us (and HiGHS does + // not re-export it via pkg-config). It also isn't needed in practice: + // rustc's std runtime already links pthread and m (and on glibc >= 2.34 + // they live in libc), and both are shared libraries so link order is + // irrelevant. HIGHS_BLAS_LINK is the escape hatch for any exotic + // toolchain that needs something extra. + println!("cargo:rustc-link-lib=static=openblas"); + return; + } + + // A system BLAS was discovered by HiGHS' cmake. Link exactly what it + // resolved. + if apple { + // On Apple, HiGHS' FindHipoDeps always resolves to Accelerate. + println!("cargo:rustc-link-lib=framework=Accelerate"); + return; + } + link_discovered_blas(out_dir); +} + +/// Read HiGHS' `CMakeCache.txt` and emit link directives for the BLAS that its +/// discovery step resolved. Two cases matter, because the final `BLAS_LIBRARIES` +/// variable is not cached: +/// * FindBLAS / `find_library` cache full library paths under +/// `BLAS__LIBRARY` and `OPENBLAS_LIB` — we parse those directly. +/// * An OpenBLAS *CMake config package* only caches its config directory as +/// `OpenBLAS_DIR` (the library path lives in non-cached variables), so we +/// detect that and link `openblas` from the enclosing lib dir. +#[cfg(feature = "build")] +fn link_discovered_blas(out_dir: &Path) { + let cache = out_dir.join("build").join("CMakeCache.txt"); + let contents = std::fs::read_to_string(&cache).unwrap_or_default(); + + // Parse `KEY:TYPE=VALUE` cache lines into (key, value), dropping empty and + // NOTFOUND values. + let entries: Vec<(&str, &str)> = contents + .lines() + .filter_map(|line| { + let (lhs, value) = line.split_once('=')?; + let (key, _ty) = lhs.split_once(':')?; + Some((key, value.trim())) + }) + .filter(|(_, v)| !v.is_empty() && !v.ends_with("-NOTFOUND")) + .collect(); + + let mut seen = std::collections::BTreeSet::new(); + let mut emit = |directive: String| { + if seen.insert(directive.clone()) { + println!("cargo:{directive}"); + true + } else { + false + } + }; + let mut emitted = false; + + // 1. Libraries resolved by FindBLAS / find_library, cached as full paths. + for &(key, value) in &entries { + let is_blas_lib = (key.starts_with("BLAS_") && key.ends_with("_LIBRARY")) + || key == "OPENBLAS_LIB" + || key == "OpenBLAS_LIBRARIES"; + if is_blas_lib { + for directive in blas_value_to_link_directives(value) { + emitted |= emit(directive); + } + } + } + + // 2. OpenBLAS found via its CMake config package: only `OpenBLAS_DIR` is + // cached. Link `openblas`, adding the enclosing lib dir (the config dir + // is `/cmake/openblas`) so non-standard prefixes still resolve. + if !emitted { + if let Some(&(_, dir)) = entries.iter().find(|&&(k, _)| k == "OpenBLAS_DIR") { + if let Some(libdir) = libdir_from_cmake_config_dir(Path::new(dir)) { + emit(format!("rustc-link-search=native={}", libdir.display())); + } + emitted |= emit("rustc-link-lib=dylib=openblas".to_string()); + } + } + + if !emitted { + // Could not read the resolution from cache; emit a best-effort default + // and point the user at the override. + println!( + "cargo:warning=highs-sys: could not determine the discovered BLAS from CMakeCache; defaulting to `-lopenblas`. Set HIGHS_BLAS_LINK (e.g. `dylib=openblas` or `dylib=mkl_rt`) if linking fails." + ); + println!("cargo:rustc-link-lib=dylib=openblas"); + } +} + +/// Given a CMake config-package directory such as `/cmake/openblas`, +/// return the enclosing library directory (``). +#[cfg(feature = "build")] +fn libdir_from_cmake_config_dir(dir: &Path) -> Option { + let mut p = dir; + loop { + let is_cmake = p + .file_name() + .and_then(|n| n.to_str()) + .map_or(false, |s| s.eq_ignore_ascii_case("cmake")); + if is_cmake { + return p.parent().map(Path::to_path_buf); + } + p = p.parent()?; + } +} + +/// Convert a cmake-resolved BLAS library value (an absolute path, a framework, +/// or a bare name) into `rustc-link-search`/`rustc-link-lib` directives. +#[cfg(feature = "build")] +fn blas_value_to_link_directives(value: &str) -> Vec { + // A macOS framework, given as `/path/Foo.framework`. + if let Some(stripped) = value.strip_suffix(".framework") { + if let Some(name) = Path::new(stripped).file_name().and_then(|s| s.to_str()) { + return vec![format!("rustc-link-lib=framework={name}")]; + } + } + + // An absolute/relative path to a library file, e.g. + // `/usr/lib/x86_64-linux-gnu/libopenblas.so`. + if value.contains('/') || value.contains('\\') { + let path = Path::new(value); + let mut out = Vec::new(); + if let Some(dir) = path.parent() { + if !dir.as_os_str().is_empty() { + out.push(format!("rustc-link-search=native={}", dir.display())); + } + } + if let Some(name) = lib_name_from_filename(path) { + // Pick the link kind from the extension: a Unix `.a` is a static + // archive; everything else (`.so`, `.dylib`, Windows `.lib` import + // libs) links as a shared/import library. + let kind = if path.extension().and_then(|e| e.to_str()) == Some("a") { + "static" + } else { + "dylib" + }; + out.push(format!("rustc-link-lib={kind}={name}")); + } + if !out.is_empty() { + return out; + } + } + + // Already a linker flag or a bare library name. + let name = value.strip_prefix("-l").unwrap_or(value); + vec![format!("rustc-link-lib=dylib={name}")] +} + +/// Derive a linker library name from a library file path +/// (`.../libopenblas.so.0` -> `openblas`, `openblas.lib` -> `openblas`). +#[cfg(feature = "build")] +fn lib_name_from_filename(path: &Path) -> Option { + let file = path.file_name()?.to_str()?; + // Strip the leading `lib` (Unix) and everything from the first extension. + let stem = file.strip_prefix("lib").unwrap_or(file); + let name = stem.split('.').next().unwrap_or(stem); + (!name.is_empty()).then(|| name.to_string()) +} + #[cfg(feature = "build")] fn build() -> bool { use cmake::Config; let target = env::var("TARGET").unwrap(); let emscripten = target.contains("emscripten"); + let apple = target.contains("apple"); let mut dst = Config::new("HiGHS"); let crt_static = target_has_feature("crt-static"); @@ -114,8 +468,11 @@ fn build() -> bool { dst.profile("Release"); } - let dst = dst - .define("FAST_BUILD", "ON") + // Configure the HiPO interior-point solver (and choose how its BLAS backend + // is obtained) if the `hipo` feature is enabled. + let hipo_blas = configure_hipo(&mut dst, apple); + + dst.define("FAST_BUILD", "ON") .define("BUILD_SHARED_LIBS", "OFF") .define( "CMAKE_MSVC_RUNTIME_LIBRARY", @@ -126,8 +483,12 @@ fn build() -> bool { }, ) .define("CMAKE_INTERPROCEDURAL_OPTIMIZATION", "FALSE") - .define("ZLIB", if cfg!(feature = "libz") { "ON" } else { "OFF" }) - .build(); + .define("ZLIB", if cfg!(feature = "libz") { "ON" } else { "OFF" }); + + // Build HiGHS, honouring the HiPO BLAS plan. This may run cmake twice: once + // to try system-BLAS discovery and, if that finds nothing, again with a + // bundled OpenBLAS. `built_openblas` records which path was taken. + let (dst, built_openblas) = build_highs_with_blas(&mut dst, hipo_blas); let include_path = dst.join("include").join("highs"); generate_bindings(Some(dst.clone()), [include_path.as_path()]); @@ -140,9 +501,12 @@ fn build() -> bool { println!("cargo:rustc-link-lib=z"); } - let apple = target.contains("apple"); let linux = target.contains("linux"); let mingw = target.contains("pc-windows-gnu"); + + // Link the BLAS backend that HiPO was built against. + link_hipo_blas(&dst, apple, built_openblas); + if apple || emscripten { println!("cargo:rustc-link-lib=c++"); } else if linux || mingw { @@ -234,7 +598,20 @@ fn main() { ); } - if !discover() && !build() { + // The `hipo` feature requires a HiGHS compiled with `-DHIPO=ON` and its + // BLAS-backed extras linked in (see `configure_hipo`). A system HiGHS found + // via `discover` (pkg-config) is not guaranteed to have been built with + // HiPO, so linking it would make `solver = "hipo"` fail at runtime even + // though the feature was requested. When `hipo` is enabled we therefore + // skip discovery and always build HiGHS ourselves (the `hipo` feature + // implies `build`). + let try_discover = !cfg!(feature = "hipo"); + if cfg!(all(feature = "discover", feature = "hipo")) { + println!( + "cargo:warning=highs-sys: the 'hipo' feature requires building HiGHS with HiPO, so the 'discover' feature is ignored (a discovered system HiGHS may lack HiPO)." + ); + } + if !(try_discover && discover()) && !build() { panic!("Could neither discover nor build HiGHS"); } } diff --git a/tests/test_hipo.rs b/tests/test_hipo.rs new file mode 100644 index 0000000..e938bb4 --- /dev/null +++ b/tests/test_hipo.rs @@ -0,0 +1,125 @@ +//! Tests for the HiPO interior-point solver, only compiled when the `hipo` +//! feature is enabled (i.e. HiGHS was built with `-DHIPO=ON`). +#![cfg(feature = "hipo")] + +use std::convert::TryInto; +use std::ffi::CString; + +use highs_sys::*; + +fn c(n: usize) -> HighsInt { + n.try_into().unwrap() +} + +fn ptr(a: &mut [T]) -> *mut T { + a.as_mut_ptr() +} + +/// Solve a small LP with the HiPO interior-point solver and check the optimum. +/// +/// This exercises the whole HiPO path: selecting `solver = "hipo"` (only a +/// valid option value when HiGHS was compiled with HIPO) and running the BLAS +/// backend that `build.rs` linked against. +#[test] +fn hipo_solves_lp() { + unsafe { + // Max f = 2x_0 + 3x_1 + // s.t. x_1 <= 6 + // 10 <= x_0 + 2x_1 <= 14 + // 8 <= 2x_0 + x_1 + // 0 <= x_0 <= 3; 1 <= x_1 + // Optimum: (x_0, x_1) = (3, 5.5), objective 22.5. + let highs = Highs_create(); + + let opt = CString::new("output_flag").unwrap(); + Highs_setBoolOptionValue(highs, opt.as_ptr(), 0); + + // Select the HiPO interior-point solver. + let solver_opt = CString::new("solver").unwrap(); + let solver_val = CString::new("hipo").unwrap(); + let status = + Highs_setStringOptionValue(highs, solver_opt.as_ptr(), solver_val.as_ptr()); + assert_eq!(status, STATUS_OK, "selecting the hipo solver should succeed"); + + let inf = Highs_getInfinity(highs); + + let colcost: &mut [f64] = &mut [2.0, 3.0]; + let collower: &mut [f64] = &mut [0.0, 1.0]; + let colupper: &mut [f64] = &mut [3.0, inf]; + let rowlower: &mut [f64] = &mut [-inf, 10.0, 8.0]; + let rowupper: &mut [f64] = &mut [6.0, 14.0, inf]; + + // Constraint matrix, row-wise. + let arstart: &mut [HighsInt] = &mut [0, 1, 3]; + let arindex: &mut [HighsInt] = &mut [1, 0, 1, 0, 1]; + let arvalue: &mut [f64] = &mut [1.0, 1.0, 2.0, 2.0, 1.0]; + + use std::ptr::null; + assert_eq!( + STATUS_OK, + Highs_addCols( + highs, + c(2), + ptr(colcost), + ptr(collower), + ptr(colupper), + 0, + null(), + null(), + null(), + ), + "addCols" + ); + assert_eq!( + STATUS_OK, + Highs_addRows( + highs, + c(3), + ptr(rowlower), + ptr(rowupper), + c(5), + ptr(arstart), + ptr(arindex), + ptr(arvalue), + ), + "addRows" + ); + assert_eq!( + STATUS_OK, + Highs_changeObjectiveSense(highs, OBJECTIVE_SENSE_MAXIMIZE) + ); + + assert_eq!(Highs_run(highs), STATUS_OK, "run"); + assert_eq!( + Highs_getModelStatus(highs), + MODEL_STATUS_OPTIMAL, + "hipo should reach an optimal solution" + ); + + let mut objective: f64 = 0.0; + let info_name = CString::new("objective_function_value").unwrap(); + Highs_getHighsDoubleInfoValue(highs, info_name.as_ptr(), &mut objective as *mut f64); + // Interior-point solvers converge to the optimum only to a tolerance. + assert!( + (objective - 22.5).abs() < 1e-5, + "objective = {}, expected 22.5", + objective + ); + + let colvalue: &mut [f64] = &mut vec![0.; 2]; + let coldual: &mut [f64] = &mut vec![0.; 2]; + let rowvalue: &mut [f64] = &mut vec![0.; 3]; + let rowdual: &mut [f64] = &mut vec![0.; 3]; + Highs_getSolution( + highs, + ptr(colvalue), + ptr(coldual), + ptr(rowvalue), + ptr(rowdual), + ); + assert!((colvalue[0] - 3.0).abs() < 1e-5, "x_0 = {}", colvalue[0]); + assert!((colvalue[1] - 5.5).abs() < 1e-5, "x_1 = {}", colvalue[1]); + + Highs_destroy(highs); + } +}