diff --git a/.github/workflows/action-sanitizers-bazel.yml b/.github/workflows/action-sanitizers-bazel.yml index 8874ae6775..0cc18fdc69 100644 --- a/.github/workflows/action-sanitizers-bazel.yml +++ b/.github/workflows/action-sanitizers-bazel.yml @@ -14,7 +14,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: bazelbuild/setup-bazelisk@v3 + - uses: bazel-contrib/setup-bazel@0.19.1 - name: Bazel clean run: bazel clean @@ -40,7 +40,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: bazelbuild/setup-bazelisk@v3 + - uses: bazel-contrib/setup-bazel@0.19.1 - name: Bazel clean run: bazel clean @@ -73,7 +73,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: bazelbuild/setup-bazelisk@v3 + - uses: bazel-contrib/setup-bazel@0.19.1 - name: Bazel clean run: bazel clean @@ -99,7 +99,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: bazelbuild/setup-bazelisk@v3 + - uses: bazel-contrib/setup-bazel@0.19.1 - name: bazel clean run: bazel clean diff --git a/.github/workflows/build-bazel.yml b/.github/workflows/build-bazel.yml index 872c9822b5..cac5391004 100644 --- a/.github/workflows/build-bazel.yml +++ b/.github/workflows/build-bazel.yml @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v6 - - uses: bazelbuild/setup-bazelisk@v3 + - uses: bazel-contrib/setup-bazel@0.19.1 - name: bazel clean run: bazel clean diff --git a/.github/workflows/test-interfaces.yml b/.github/workflows/test-interfaces.yml new file mode 100644 index 0000000000..69f1e4e6e3 --- /dev/null +++ b/.github/workflows/test-interfaces.yml @@ -0,0 +1,59 @@ +name: test-interfaces + +# Builds HiGHS from this checkout and solves a small LP and MIP through +# downstream interfaces: PuLP and Pyomo (via highspy) and the Rust highs crate + +on: + push: + branches: [ latest, master ] + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + python: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Install python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + + - name: Install highspy from this checkout + run: python3 -m pip install . + + - name: Install PuLP and Pyomo + run: python3 -m pip install pulp pyomo + + - name: Solve with PuLP + run: python3 check/interfaces/pulp_smoke.py + + - name: Solve with Pyomo + run: python3 check/interfaces/pyomo_smoke.py + + rust: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Build and install HiGHS + run: | + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF \ + -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/highs-install + cmake --build build --parallel + cmake --install build + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Solve with the highs crate + working-directory: check/interfaces/rust + env: + PKG_CONFIG_PATH: ${{ github.workspace }}/highs-install/lib/pkgconfig + LD_LIBRARY_PATH: ${{ github.workspace }}/highs-install/lib + run: HIGHS_EXPECTED_GITHASH=$(git rev-parse HEAD) cargo run --release diff --git a/check/interfaces/pulp_smoke.py b/check/interfaces/pulp_smoke.py new file mode 100644 index 0000000000..679add5f46 --- /dev/null +++ b/check/interfaces/pulp_smoke.py @@ -0,0 +1,18 @@ +# Solve a small LP and MIP through PuLP's highspy-based HiGHS solver +import pulp + +prob = pulp.LpProblem("smoke", pulp.LpMinimize) +x = prob.add_variable("x", 0, 4.5) +y = prob.add_variable("y", 1, 1) +z = prob.add_variable("z", 0, None, pulp.LpInteger) +prob += x + 4 * y + 9 * z +prob += x + y <= 5.5 +prob += x + z >= 10 +prob += -y + z <= 7.5 + +# LP relaxation, then the MIP +for mip, expected in ((False, 58.0), (True, 62.0)): + stats = prob.solve(pulp.HiGHS(msg=False, mip=mip)) + print(f"mip={mip}: {stats.status_str} obj={pulp.value(prob.objective)}") + assert stats.status_str == "Optimal" + assert abs(pulp.value(prob.objective) - expected) < 1e-6 diff --git a/check/interfaces/pyomo_smoke.py b/check/interfaces/pyomo_smoke.py new file mode 100644 index 0000000000..653080fb95 --- /dev/null +++ b/check/interfaces/pyomo_smoke.py @@ -0,0 +1,22 @@ +# Solve a small LP and MIP through Pyomo's highspy-based appsi_highs solver +import pyomo.environ as pyo + +m = pyo.ConcreteModel() +m.x = pyo.Var(bounds=(0, 4.5)) +m.y = pyo.Var(bounds=(1, 1)) +m.z = pyo.Var(within=pyo.NonNegativeIntegers) +m.obj = pyo.Objective(expr=m.x + 4 * m.y + 9 * m.z, sense=pyo.minimize) +m.c1 = pyo.Constraint(expr=m.x + m.y <= 5.5) +m.c2 = pyo.Constraint(expr=m.x + m.z >= 10) +m.c3 = pyo.Constraint(expr=-m.y + m.z <= 7.5) + +solver = pyo.SolverFactory("appsi_highs") +assert solver.available(), "appsi_highs (highspy) is not available" + +# LP relaxation, then the MIP +for relax, expected in ((True, 58.0), (False, 62.0)): + model = pyo.TransformationFactory("core.relax_integer_vars").create_using(m) if relax else m + res = solver.solve(model) + print(f"relax={relax}: {res.solver.termination_condition} obj={pyo.value(model.obj)}") + assert res.solver.termination_condition == pyo.TerminationCondition.optimal + assert abs(pyo.value(model.obj) - expected) < 1e-6 diff --git a/check/interfaces/rust/.gitignore b/check/interfaces/rust/.gitignore new file mode 100644 index 0000000000..1e7caa9ea8 --- /dev/null +++ b/check/interfaces/rust/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +target/ diff --git a/check/interfaces/rust/Cargo.toml b/check/interfaces/rust/Cargo.toml new file mode 100644 index 0000000000..f7790b05f8 --- /dev/null +++ b/check/interfaces/rust/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "highs-rust-smoke" +version = "0.1.0" +edition = "2021" + +[dependencies] +highs = "2" +# "discover" links the HiGHS found via pkg-config instead of the bundled copy +highs-sys = { version = "1", features = ["discover"] } diff --git a/check/interfaces/rust/src/main.rs b/check/interfaces/rust/src/main.rs new file mode 100644 index 0000000000..f0cbc3fbd2 --- /dev/null +++ b/check/interfaces/rust/src/main.rs @@ -0,0 +1,43 @@ +// Solve a small LP and MIP through the highs crate, linked against an +// installed HiGHS found by pkg-config (highs-sys "discover" feature) +use highs::{HighsModelStatus, RowProblem, Sense}; +use std::ffi::CStr; + +fn solve(integer_z: bool) -> f64 { + let mut pb = RowProblem::default(); + let x = pb.add_column(1., 0.0..=4.5); + let y = pb.add_column(4., 1.0..=1.0); + let z = if integer_z { + pb.add_integer_column(9., 0.0..) + } else { + pb.add_column(9., 0.0..) + }; + pb.add_row(..=5.5, &[(x, 1.), (y, 1.)]); + pb.add_row(10.0.., &[(x, 1.), (z, 1.)]); + pb.add_row(..=7.5, &[(y, -1.), (z, 1.)]); + + let mut model = pb.optimise(Sense::Minimise); + model.make_quiet(); + let solved = model.solve(); + assert_eq!(solved.status(), HighsModelStatus::Optimal); + solved.objective_value() +} + +fn main() { + let githash = unsafe { CStr::from_ptr(highs_sys::Highs_githash()) } + .to_str() + .unwrap(); + println!("Linked HiGHS git hash: {githash}"); + if let Ok(expected) = std::env::var("HIGHS_EXPECTED_GITHASH") { + assert!( + expected.starts_with(githash) || githash.starts_with(&expected), + "linked HiGHS {githash} is not the checkout {expected}" + ); + } + + let lp = solve(false); + let mip = solve(true); + println!("LP obj = {lp}, MIP obj = {mip}"); + assert!((lp - 58.).abs() < 1e-6); + assert!((mip - 62.).abs() < 1e-6); +}