|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import pandas as pd |
| 4 | +import pytest |
| 5 | + |
| 6 | +from linopy import Model, available_solvers |
| 7 | + |
| 8 | + |
| 9 | +def test_add_sos_constraints_registers_variable() -> None: |
| 10 | + m = Model() |
| 11 | + locations = pd.Index([0, 1, 2], name="locations") |
| 12 | + build = m.add_variables(coords=[locations], name="build") |
| 13 | + |
| 14 | + m.add_sos_constraints(build, sos_type=1, sos_dim="locations") |
| 15 | + |
| 16 | + assert build.attrs["sos_type"] == 1 |
| 17 | + assert build.attrs["sos_dim"] == "locations" |
| 18 | + assert list(m.variables.sos) == ["build"] |
| 19 | + |
| 20 | + m.remove_sos_constraints(build) |
| 21 | + assert "sos_type" not in build.attrs |
| 22 | + assert "sos_dim" not in build.attrs |
| 23 | + |
| 24 | + |
| 25 | +def test_add_sos_constraints_validation() -> None: |
| 26 | + m = Model() |
| 27 | + strings = pd.Index(["a", "b"], name="strings") |
| 28 | + with pytest.raises(ValueError, match="sos_type"): |
| 29 | + m.add_sos_constraints(m.add_variables(name="x"), sos_type=3, sos_dim="i") |
| 30 | + |
| 31 | + variable = m.add_variables(coords=[strings], name="string_var") |
| 32 | + |
| 33 | + with pytest.raises(ValueError, match="dimension"): |
| 34 | + m.add_sos_constraints(variable, sos_type=1, sos_dim="missing") |
| 35 | + |
| 36 | + with pytest.raises(ValueError, match="numeric"): |
| 37 | + m.add_sos_constraints(variable, sos_type=1, sos_dim="strings") |
| 38 | + |
| 39 | + numeric = m.add_variables(coords=[pd.Index([0, 1], name="dim")], name="num") |
| 40 | + m.add_sos_constraints(numeric, sos_type=1, sos_dim="dim") |
| 41 | + with pytest.raises(ValueError, match="already has"): |
| 42 | + m.add_sos_constraints(numeric, sos_type=1, sos_dim="dim") |
| 43 | + |
| 44 | + |
| 45 | +def test_sos_constraints_written_to_lp(tmp_path) -> None: |
| 46 | + m = Model() |
| 47 | + breakpoints = pd.Index([0.0, 1.5, 3.5], name="bp") |
| 48 | + lambdas = m.add_variables(coords=[breakpoints], name="lambda") |
| 49 | + m.add_sos_constraints(lambdas, sos_type=2, sos_dim="bp") |
| 50 | + |
| 51 | + fn = tmp_path / "sos.lp" |
| 52 | + m.to_file(fn, io_api="lp") |
| 53 | + content = fn.read_text() |
| 54 | + |
| 55 | + assert "\nsos\n" in content |
| 56 | + assert "S2 ::" in content |
| 57 | + assert "3.5" in content |
| 58 | + |
| 59 | + |
| 60 | +@pytest.mark.skipif("gurobi" not in available_solvers, reason="Gurobipy not installed") |
| 61 | +def test_to_gurobipy_emits_sos_constraints() -> None: |
| 62 | + gurobipy = pytest.importorskip("gurobipy") |
| 63 | + |
| 64 | + m = Model() |
| 65 | + segments = pd.Index([0.0, 0.5, 1.0], name="seg") |
| 66 | + var = m.add_variables(coords=[segments], name="lambda") |
| 67 | + m.add_sos_constraints(var, sos_type=1, sos_dim="seg") |
| 68 | + |
| 69 | + try: |
| 70 | + model = m.to_gurobipy() |
| 71 | + except gurobipy.GurobiError as exc: # pragma: no cover - depends on license setup |
| 72 | + pytest.skip(f"Gurobi environment unavailable: {exc}") |
| 73 | + |
| 74 | + assert model.NumSOS == 1 |
0 commit comments