Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Export variable and constraint names in LP files #413

Merged
merged 20 commits into from
Feb 6, 2025
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Remove the lp-debug io_api and replace it with parameter. Extend it t…
…o polars export too
olivierjuan committed Dec 13, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
commit 701a8903f1063338a0620663f824dd736de6e6a5
141 changes: 84 additions & 57 deletions linopy/io.py
Original file line number Diff line number Diff line change
@@ -69,29 +69,48 @@ def print_coord(coord):
return coord


def get_print(m: Model, anonymously: bool = True):
def get_printers(m: Model, io_api: str, anonymously: bool = True):
if anonymously:
if io_api == "lp":
def print_variable_anonymous(var):
return f"x{var}"

def print_variable_anonymous(var):
return f"x{var}"
def print_constraint_anonymous(cons):
return f"c{cons}"

def print_constraint_anonymous(cons):
return f"c{cons}"
elif io_api == "lp-polars":
def print_variable_anonymous(series):
return pl.lit(" x").alias("x"), series.cast(pl.String)

def print_constraint_anonymous(series):
return pl.lit("c").alias("c"), series.cast(pl.String)
else:
return None

return print_variable_anonymous, print_constraint_anonymous
else:

def print_variable(var):
def print_variable_lp(var):
name, coord = m.variables.get_label_position(var)
name = clean_name(name)
return f"{name}{print_coord(coord)}"
return f"{name}{print_coord(coord)}#{var}"

def print_constraint(cons):
def print_constraint_lp(cons):
name, coord = m.constraints.get_label_position(cons)
name = clean_name(name)
return f"{name}{print_coord(coord)}"
return f"{name}{print_coord(coord)}#{cons}"

def print_variable_polars(series):
return pl.lit(" "), series.map_elements(print_variable_lp, return_dtype=pl.String)

return print_variable, print_constraint
def print_constraint_polars(series):
return pl.lit(None), series.map_elements(print_constraint_lp, return_dtype=pl.String)

if io_api == "lp":
return print_variable_lp, print_constraint_lp
elif io_api == "lp-polars":
return print_variable_polars, print_constraint_polars
else:
return None


def objective_write_linear_terms(
@@ -392,12 +411,10 @@ def to_lp_file(
integer_label: str,
slice_size: int = 10_000_000,
progress: bool = True,
anonymously: bool = True,
printers = None,
) -> None:
batch_size = 5000

printers = get_print(m, anonymously)

with open(fn, mode="w") as f:
start = time.time()

@@ -443,27 +460,25 @@ def to_lp_file(
logger.info(f" Writing time: {round(time.time()-start, 2)}s")


def objective_write_linear_terms_polars(f, df):
def objective_write_linear_terms_polars(f, df, print_variable):
cols = [
pl.when(pl.col("coeffs") >= 0).then(pl.lit("+")).otherwise(pl.lit("")),
pl.col("coeffs").cast(pl.String),
pl.lit(" x"),
pl.col("vars").cast(pl.String),
*print_variable(pl.col("vars")),
]
df = df.select(pl.concat_str(cols, ignore_nulls=True))
df.write_csv(
f, separator=" ", null_value="", quote_style="never", include_header=False
)


def objective_write_quadratic_terms_polars(f, df):
def objective_write_quadratic_terms_polars(f, df, print_variable):
cols = [
pl.when(pl.col("coeffs") >= 0).then(pl.lit("+")).otherwise(pl.lit("")),
pl.col("coeffs").mul(2).cast(pl.String),
pl.lit(" x"),
pl.col("vars1").cast(pl.String),
pl.lit(" * x"),
pl.col("vars2").cast(pl.String),
*print_variable(pl.col("vars1")),
pl.lit(" *"),
*print_variable(pl.col("vars2")),
]
f.write(b"+ [\n")
df = df.select(pl.concat_str(cols, ignore_nulls=True))
@@ -473,7 +488,7 @@ def objective_write_quadratic_terms_polars(f, df):
f.write(b"] / 2\n")


def objective_to_file_polars(m, f, progress=False):
def objective_to_file_polars(m, f, progress=False, printers=None):
"""
Write out the objective of a model to a lp file.
"""
@@ -484,8 +499,10 @@ def objective_to_file_polars(m, f, progress=False):
f.write(f"{sense}\n\nobj:\n\n".encode())
df = m.objective.to_polars()

print_variable, _ = printers

if m.is_linear:
objective_write_linear_terms_polars(f, df)
objective_write_linear_terms_polars(f, df, print_variable)

elif m.is_quadratic:
lins = df.filter(pl.col("vars1").eq(-1) | pl.col("vars2").eq(-1))
@@ -495,20 +512,22 @@ def objective_to_file_polars(m, f, progress=False):
.otherwise(pl.col("vars1"))
.alias("vars")
)
objective_write_linear_terms_polars(f, lins)
objective_write_linear_terms_polars(f, lins, print_variable)

quads = df.filter(pl.col("vars1").ne(-1) & pl.col("vars2").ne(-1))
objective_write_quadratic_terms_polars(f, quads)
objective_write_quadratic_terms_polars(f, quads, print_variable)


def bounds_to_file_polars(m, f, progress=False, slice_size=2_000_000):
def bounds_to_file_polars(m, f, progress=False, slice_size=2_000_000, printers=None):
"""
Write out variables of a model to a lp file.
"""
names = list(m.variables.continuous) + list(m.variables.integers)
if not len(list(names)):
return

print_variable, _ = printers

f.write(b"\n\nbounds\n\n")
if progress:
names = tqdm(
@@ -525,8 +544,8 @@ def bounds_to_file_polars(m, f, progress=False, slice_size=2_000_000):
columns = [
pl.when(pl.col("lower") >= 0).then(pl.lit("+")).otherwise(pl.lit("")),
pl.col("lower").cast(pl.String),
pl.lit(" <= x"),
pl.col("labels").cast(pl.String),
pl.lit(" <= "),
*print_variable(pl.col("labels")),
pl.lit(" <= "),
pl.when(pl.col("upper") >= 0).then(pl.lit("+")).otherwise(pl.lit("")),
pl.col("upper").cast(pl.String),
@@ -539,14 +558,16 @@ def bounds_to_file_polars(m, f, progress=False, slice_size=2_000_000):
formatted.write_csv(f, **kwargs)


def binaries_to_file_polars(m, f, progress=False, slice_size=2_000_000):
def binaries_to_file_polars(m, f, progress=False, slice_size=2_000_000, printers=None):
"""
Write out binaries of a model to a lp file.
"""
names = m.variables.binaries
if not len(list(names)):
return

print_variable, _ = printers

f.write(b"\n\nbinary\n\n")
if progress:
names = tqdm(
@@ -561,8 +582,7 @@ def binaries_to_file_polars(m, f, progress=False, slice_size=2_000_000):
df = var_slice.to_polars()

columns = [
pl.lit("x"),
pl.col("labels").cast(pl.String),
*print_variable(pl.col("labels")),
]

kwargs = dict(
@@ -573,7 +593,7 @@ def binaries_to_file_polars(m, f, progress=False, slice_size=2_000_000):


def integers_to_file_polars(
m, f, progress=False, integer_label="general", slice_size=2_000_000
m, f, progress=False, integer_label="general", slice_size=2_000_000, printers=None
):
"""
Write out integers of a model to a lp file.
@@ -582,6 +602,8 @@ def integers_to_file_polars(
if not len(list(names)):
return

print_variable, _ = printers

f.write(f"\n\n{integer_label}\n\n".encode())
if progress:
names = tqdm(
@@ -596,8 +618,7 @@ def integers_to_file_polars(
df = var_slice.to_polars()

columns = [
pl.lit("x"),
pl.col("labels").cast(pl.String),
*print_variable(pl.col("labels")),
]

kwargs = dict(
@@ -607,10 +628,12 @@ def integers_to_file_polars(
formatted.write_csv(f, **kwargs)


def constraints_to_file_polars(m, f, progress=False, lazy=False, slice_size=2_000_000):
def constraints_to_file_polars(m, f, progress=False, lazy=False, slice_size=2_000_000, printers=None):
if not len(m.constraints):
return

print_variable, print_constraint = printers

f.write(b"\n\ns.t.\n\n")
names = m.constraints
if progress:
@@ -636,14 +659,16 @@ def constraints_to_file_polars(m, f, progress=False, lazy=False, slice_size=2_00
.alias("labels")
)

row_labels = print_constraint(pl.col("labels"))
col_labels = print_variable(pl.col("vars"))
columns = [
pl.when(pl.col("labels").is_not_null()).then(pl.lit("c")).alias("c"),
pl.col("labels").cast(pl.String),
pl.when(pl.col("labels").is_not_null()).then(row_labels[0]),
pl.when(pl.col("labels").is_not_null()).then(row_labels[1]),
pl.when(pl.col("labels").is_not_null()).then(pl.lit(":\n")).alias(":"),
pl.when(pl.col("coeffs") >= 0).then(pl.lit("+")),
pl.col("coeffs").cast(pl.String),
pl.when(pl.col("vars").is_not_null()).then(pl.lit(" x")).alias("x"),
pl.col("vars").cast(pl.String),
pl.when(pl.col("vars").is_not_null()).then(col_labels[0]),
pl.when(pl.col("vars").is_not_null()).then(col_labels[1]),
"sign",
pl.lit(" "),
pl.col("rhs").cast(pl.String),
@@ -662,21 +687,27 @@ def constraints_to_file_polars(m, f, progress=False, lazy=False, slice_size=2_00


def to_lp_file_polars(
m, fn, integer_label="general", slice_size=2_000_000, progress: bool = True
m,
fn,
integer_label="general",
slice_size=2_000_000,
progress: bool = True,
printers=None,
):
with open(fn, mode="wb") as f:
start = time.time()

objective_to_file_polars(m, f, progress=progress)
constraints_to_file_polars(m, f=f, progress=progress, slice_size=slice_size)
bounds_to_file_polars(m, f=f, progress=progress, slice_size=slice_size)
binaries_to_file_polars(m, f=f, progress=progress, slice_size=slice_size)
objective_to_file_polars(m, f, progress=progress, printers=printers)
constraints_to_file_polars(m, f=f, progress=progress, slice_size=slice_size, printers=printers)
bounds_to_file_polars(m, f=f, progress=progress, slice_size=slice_size, printers=printers)
binaries_to_file_polars(m, f=f, progress=progress, slice_size=slice_size, printers=printers)
integers_to_file_polars(
m,
integer_label=integer_label,
f=f,
progress=progress,
slice_size=slice_size,
printers = printers,
)
f.write(b"end\n")

@@ -690,6 +721,7 @@ def to_file(
integer_label: str = "general",
slice_size: int = 2_000_000,
progress: bool | None = None,
with_names: bool = True,
) -> Path:
"""
Write out a model to a lp or mps file.
@@ -707,20 +739,15 @@ def to_file(
if progress is None:
progress = m._xCounter > 10_000

printers = get_printers(m, io_api, anonymously=not with_names)
if with_names and io_api not in ["lp", "lp-polars"]:
logger.warning("Debug names are only supported for LP files.")

if io_api == "lp":
to_lp_file(m, fn, integer_label, slice_size=slice_size, progress=progress)
elif io_api == "lp-debug":
to_lp_file(
m,
fn,
integer_label,
slice_size=slice_size,
progress=progress,
anonymously=False,
)
to_lp_file(m, fn, integer_label, slice_size=slice_size, progress=progress, printers=printers)
elif io_api == "lp-polars":
to_lp_file_polars(
m, fn, integer_label, slice_size=slice_size, progress=progress
m, fn, integer_label, slice_size=slice_size, progress=progress, printers=printers
)

elif io_api == "mps":
@@ -735,7 +762,7 @@ def to_file(
h.writeModel(str(fn))
else:
raise ValueError(
f"Invalid io_api '{io_api}'. Choose from 'lp', 'lp-debug', 'lp-polars' or 'mps'."
f"Invalid io_api '{io_api}'. Choose from 'lp', 'lp-polars' or 'mps'."
)

return fn
21 changes: 18 additions & 3 deletions linopy/model.py
Original file line number Diff line number Diff line change
@@ -958,6 +958,7 @@ def solve(
self,
solver_name: str | None = None,
io_api: str | None = None,
with_names: bool = False,
problem_fn: str | Path | None = None,
solution_fn: str | Path | None = None,
log_fn: str | Path | None = None,
@@ -990,6 +991,11 @@ def solve(
'direct' the problem is communicated to the solver via the solver
specific API, e.g. gurobipy. This may lead to faster run times.
The default is set to 'lp' if available.
with_names : bool, optional
If the Api to use for communicating with the solver is based on 'lp',
this option allows to keep the variable and constraint names in the
lp file. This may lead to slower run times.
The default is set to False.
problem_fn : path_like, optional
Path of the lp file or output file/directory which is written out
during the process. The default None results in a temporary file.
@@ -1119,6 +1125,8 @@ def solve(
**solver_options,
)
if io_api == "direct":
if with_names:
logger.warning("Passing variable and constraint names is only supported with lp files")
# no problem file written and direct model is set for solver
result = solver.solve_problem_from_model(
model=self,
@@ -1131,7 +1139,8 @@ def solve(
else:
problem_fn = self.to_file(
to_path(problem_fn),
io_api,
io_api=io_api,
with_names=with_names,
slice_size=slice_size,
progress=progress,
)
@@ -1212,10 +1221,16 @@ def compute_infeasibilities(self) -> list[int]:
f = NamedTemporaryFile(suffix=".ilp", prefix="linopy-iis-", delete=False)
solver_model.write(f.name)
labels = []
pattern = re.compile(r"^ [^:]+#([0-9]+):")
for line in f.readlines():
line_decoded = line.decode()
if line_decoded.startswith(" c"):
labels.append(int(line_decoded.split(":")[0][2:]))
try:
if line_decoded.startswith(" c"):
labels.append(int(line_decoded.split(":")[0][2:]))
except ValueError as _:
match = pattern.match(line_decoded)
if match:
labels.append(int(match.group(1)))
return labels

def print_infeasibilities(self, display_max_terms: int | None = None) -> None:
7 changes: 5 additions & 2 deletions linopy/solvers.py
Original file line number Diff line number Diff line change
@@ -43,7 +43,7 @@
"mindopt",
]

FILE_IO_APIS = ["lp", "lp-debug", "lp-polars", "mps"]
FILE_IO_APIS = ["lp", "lp-polars", "mps"]
IO_APIS = FILE_IO_APIS + ["direct"]

available_solvers = []
@@ -135,7 +135,10 @@ def set_int_index(series: Series) -> Series:
"""
Convert string index to int index.
"""
series.index = series.index.str[1:].astype(int)
try:
series.index = series.index.str[1:].astype(int)
except:
series.index = series.index.str.replace(".*#", "", regex=True).astype(int)
return series


4 changes: 2 additions & 2 deletions test/test_io.py
Original file line number Diff line number Diff line change
@@ -127,11 +127,11 @@ def test_to_file_lp(model, tmp_path):


@pytest.mark.skipif("gurobi" not in available_solvers, reason="Gurobipy not installed")
def test_to_file_lp_debug(model, tmp_path):
def test_to_file_lp_with_names(model, tmp_path):
import gurobipy

fn = tmp_path / "test.lp"
model.to_file(fn, io_api="lp-debug")
model.to_file(fn, io_api="lp", with_names=True)

gurobipy.read(str(fn))

219 changes: 114 additions & 105 deletions test/test_optimization.py
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
@author: fabian
"""

import itertools
import logging
import platform

@@ -22,19 +22,26 @@

io_apis = ["lp", "lp-polars"]

with_names = [False, True]

if "highs" in available_solvers:
# mps io is only supported via highspy
io_apis.append("mps")

params = [(name, io_api) for name in available_solvers for io_api in io_apis]
params = [
(name, io_api, with_names)
for (name, io_api, with_names) in list(itertools.product(available_solvers, io_apis, with_names))
if "lp" in io_api or with_names == False
]

direct_solvers = ["gurobi", "highs", "mosek"]
for solver in direct_solvers:
if solver in available_solvers:
params.append((solver, "direct"))
params.append((solver, "direct", False))

if "mosek" in available_solvers:
params.append(("mosek", "lp"))
params.append(("mosek", "lp", False))
params.append(("mosek", "lp", True))


feasible_quadratic_solvers = quadratic_solvers
@@ -329,18 +336,18 @@ def test_model_types(
assert masked_constraint_model.type == "LP"


@pytest.mark.parametrize("solver,io_api", params)
def test_default_setting(model, solver, io_api):
@pytest.mark.parametrize("solver,io_api,with_names", params)
def test_default_setting(model, solver, io_api, with_names):
assert model.objective.sense == "min"
status, condition = model.solve(solver, io_api=io_api)
status, condition = model.solve(solver, io_api=io_api, with_names=with_names)
assert status == "ok"
assert np.isclose(model.objective.value, 3.3)
assert model.solver_name == solver


@pytest.mark.parametrize("solver,io_api", params)
def test_default_setting_sol_and_dual_accessor(model, solver, io_api):
status, condition = model.solve(solver, io_api=io_api)
@pytest.mark.parametrize("solver,io_api,with_names", params)
def test_default_setting_sol_and_dual_accessor(model, solver, io_api, with_names):
status, condition = model.solve(solver, io_api=io_api, with_names=with_names)
assert status == "ok"
x = model["x"]
assert_equal(x.solution, model.solution["x"])
@@ -351,9 +358,9 @@ def test_default_setting_sol_and_dual_accessor(model, solver, io_api):
assert model.matrices.dual[0] == model.dual["con0"]


@pytest.mark.parametrize("solver,io_api", params)
def test_default_setting_expression_sol_accessor(model, solver, io_api):
status, condition = model.solve(solver, io_api=io_api)
@pytest.mark.parametrize("solver,io_api,with_names", params)
def test_default_setting_expression_sol_accessor(model, solver, io_api, with_names):
status, condition = model.solve(solver, io_api=io_api, with_names=with_names)
assert status == "ok"
x = model["x"]
y = model["y"]
@@ -368,49 +375,49 @@ def test_default_setting_expression_sol_accessor(model, solver, io_api):
assert_equal(qexpr.solution, 4 * x.solution * y.solution)


@pytest.mark.parametrize("solver,io_api", params)
def test_anonymous_constraint(model, model_anonymous_constraint, solver, io_api):
status, condition = model_anonymous_constraint.solve(solver, io_api=io_api)
@pytest.mark.parametrize("solver,io_api,with_names", params)
def test_anonymous_constraint(model, model_anonymous_constraint, solver, io_api, with_names):
status, condition = model_anonymous_constraint.solve(solver, io_api=io_api, with_names=with_names)
assert status == "ok"
assert np.isclose(model_anonymous_constraint.objective.value, 3.3)

model.solve(solver, io_api=io_api)
model.solve(solver, io_api=io_api, with_names=with_names)
assert_equal(model.solution, model_anonymous_constraint.solution)


@pytest.mark.parametrize("solver,io_api", params)
def test_model_maximization(model_maximization, solver, io_api):
@pytest.mark.parametrize("solver,io_api,with_names", params)
def test_model_maximization(model_maximization, solver, io_api, with_names):
m = model_maximization
assert m.objective.sense == "max"
assert m.objective.value is None

if solver in ["cbc", "glpk"] and io_api == "mps" and _new_highspy_mps_layout:
with pytest.raises(ValueError):
m.solve(solver, io_api=io_api)
m.solve(solver, io_api=io_api, with_names=with_names)
else:
status, condition = m.solve(solver, io_api=io_api)
status, condition = m.solve(solver, io_api=io_api, with_names=with_names)
assert status == "ok"
assert np.isclose(m.objective.value, 3.3)


@pytest.mark.parametrize("solver,io_api", params)
def test_default_settings_chunked(model_chunked, solver, io_api):
status, condition = model_chunked.solve(solver, io_api=io_api)
@pytest.mark.parametrize("solver,io_api,with_names", params)
def test_default_settings_chunked(model_chunked, solver, io_api, with_names):
status, condition = model_chunked.solve(solver, io_api=io_api, with_names=with_names)
assert status == "ok"
assert np.isclose(model_chunked.objective.value, 3.3)


@pytest.mark.parametrize("solver,io_api", params)
def test_default_settings_small_slices(model, solver, io_api):
@pytest.mark.parametrize("solver,io_api,with_names", params)
def test_default_settings_small_slices(model, solver, io_api, with_names):
assert model.objective.sense == "min"
status, condition = model.solve(solver, io_api=io_api, slice_size=2)
status, condition = model.solve(solver, io_api=io_api, with_names=with_names, slice_size=2)
assert status == "ok"
assert np.isclose(model.objective.value, 3.3)
assert model.solver_name == solver


@pytest.mark.parametrize("solver,io_api", params)
def test_solver_options(model, solver, io_api):
@pytest.mark.parametrize("solver,io_api,with_names", params)
def test_solver_options(model, solver, io_api, with_names):
time_limit_option = {
"cbc": {"sec": 1},
"gurobi": {"TimeLimit": 1},
@@ -423,20 +430,20 @@ def test_solver_options(model, solver, io_api):
"mindopt": {"MaxTime": 1},
"copt": {"TimeLimit": 1},
}
status, condition = model.solve(solver, io_api=io_api, **time_limit_option[solver])
status, condition = model.solve(solver, io_api=io_api, with_names=with_names, **time_limit_option[solver])
assert status == "ok"


@pytest.mark.parametrize("solver,io_api", params)
def test_duplicated_variables(model_with_duplicated_variables, solver, io_api):
status, condition = model_with_duplicated_variables.solve(solver, io_api=io_api)
@pytest.mark.parametrize("solver,io_api,with_names", params)
def test_duplicated_variables(model_with_duplicated_variables, solver, io_api, with_names):
status, condition = model_with_duplicated_variables.solve(solver, io_api=io_api, with_names=with_names)
assert status == "ok"
assert all(model_with_duplicated_variables.solution["x"] == 5)


@pytest.mark.parametrize("solver,io_api", params)
def test_non_aligned_variables(model_with_non_aligned_variables, solver, io_api):
status, condition = model_with_non_aligned_variables.solve(solver, io_api=io_api)
@pytest.mark.parametrize("solver,io_api,with_names", params)
def test_non_aligned_variables(model_with_non_aligned_variables, solver, io_api, with_names):
status, condition = model_with_non_aligned_variables.solve(solver, io_api=io_api, with_names=with_names)
assert status == "ok"
with pytest.warns(UserWarning):
assert model_with_non_aligned_variables.solution["x"][0] == 0
@@ -448,11 +455,12 @@ def test_non_aligned_variables(model_with_non_aligned_variables, solver, io_api)
assert np.issubdtype(dtype, np.floating)


@pytest.mark.parametrize("solver,io_api", params)
def test_set_files(tmp_path, model, solver, io_api):
@pytest.mark.parametrize("solver,io_api,with_names", params)
def test_set_files(tmp_path, model, solver, io_api, with_names):
status, condition = model.solve(
solver,
io_api=io_api,
with_names=with_names,
problem_fn=tmp_path / "problem.lp",
solution_fn=tmp_path / "solution.sol",
log_fn=tmp_path / "logging.log",
@@ -461,11 +469,12 @@ def test_set_files(tmp_path, model, solver, io_api):
assert status == "ok"


@pytest.mark.parametrize("solver,io_api", params)
def test_set_files_and_keep_files(tmp_path, model, solver, io_api):
@pytest.mark.parametrize("solver,io_api,with_names", params)
def test_set_files_and_keep_files(tmp_path, model, solver, io_api, with_names):
status, condition = model.solve(
solver,
io_api=io_api,
with_names=with_names,
problem_fn=tmp_path / "problem.lp",
solution_fn=tmp_path / "solution.sol",
log_fn=tmp_path / "logging.log",
@@ -478,12 +487,12 @@ def test_set_files_and_keep_files(tmp_path, model, solver, io_api):
assert (tmp_path / "logging.log").exists()


@pytest.mark.parametrize("solver,io_api", params)
def test_infeasible_model(model, solver, io_api):
@pytest.mark.parametrize("solver,io_api,with_names", params)
def test_infeasible_model(model, solver, io_api, with_names):
model.add_constraints([(1, "x")], "<=", 0)
model.add_constraints([(1, "y")], "<=", 0)

status, condition = model.solve(solver, io_api=io_api)
status, condition = model.solve(solver, io_api=io_api, with_names=with_names)
assert status == "warning"
assert "infeasible" in condition

@@ -498,131 +507,131 @@ def test_infeasible_model(model, solver, io_api):
model.compute_infeasibilities()


@pytest.mark.parametrize("solver,io_api", params)
def test_model_with_inf(model_with_inf, solver, io_api):
status, condition = model_with_inf.solve(solver, io_api=io_api)
@pytest.mark.parametrize("solver,io_api,with_names", params)
def test_model_with_inf(model_with_inf, solver, io_api, with_names):
status, condition = model_with_inf.solve(solver, io_api=io_api, with_names=with_names)
assert condition == "optimal"
assert (model_with_inf.solution.x == 0).all()
assert (model_with_inf.solution.y == 10).all()


@pytest.mark.parametrize(
"solver,io_api", [p for p in params if p[0] not in ["mindopt"]]
"solver,io_api,with_names", [p for p in params if p[0] not in ["mindopt"]]
)
def test_milp_binary_model(milp_binary_model, solver, io_api):
status, condition = milp_binary_model.solve(solver, io_api=io_api)
def test_milp_binary_model(milp_binary_model, solver, io_api, with_names):
status, condition = milp_binary_model.solve(solver, io_api=io_api, with_names=with_names)
assert condition == "optimal"
assert (
(milp_binary_model.solution.y == 1) | (milp_binary_model.solution.y == 0)
).all()


@pytest.mark.parametrize(
"solver,io_api", [p for p in params if p[0] not in ["mindopt"]]
"solver,io_api,with_names", [p for p in params if p[0] not in ["mindopt"]]
)
def test_milp_binary_model_r(milp_binary_model_r, solver, io_api):
status, condition = milp_binary_model_r.solve(solver, io_api=io_api)
def test_milp_binary_model_r(milp_binary_model_r, solver, io_api, with_names):
status, condition = milp_binary_model_r.solve(solver, io_api=io_api, with_names=with_names)
assert condition == "optimal"
assert (
(milp_binary_model_r.solution.x == 1) | (milp_binary_model_r.solution.x == 0)
).all()


@pytest.mark.parametrize(
"solver,io_api", [p for p in params if p[0] not in ["mindopt"]]
"solver,io_api,with_names", [p for p in params if p[0] not in ["mindopt"]]
)
def test_milp_model(milp_model, solver, io_api):
status, condition = milp_model.solve(solver, io_api=io_api)
def test_milp_model(milp_model, solver, io_api, with_names):
status, condition = milp_model.solve(solver, io_api=io_api, with_names=with_names)
assert condition == "optimal"
assert ((milp_model.solution.y == 9) | (milp_model.solution.x == 0.5)).all()


@pytest.mark.parametrize(
"solver,io_api", [p for p in params if p[0] not in ["mindopt"]]
"solver,io_api,with_names", [p for p in params if p[0] not in ["mindopt"]]
)
def test_milp_model_r(milp_model_r, solver, io_api):
def test_milp_model_r(milp_model_r, solver, io_api, with_names):
# MPS format by Highs wrong, see https://github.com/ERGO-Code/HiGHS/issues/1325
# skip it
if io_api != "mps":
status, condition = milp_model_r.solve(solver, io_api=io_api)
status, condition = milp_model_r.solve(solver, io_api=io_api, with_names=with_names)
assert condition == "optimal"
assert ((milp_model_r.solution.x == 11) | (milp_model_r.solution.y == 0)).all()


@pytest.mark.parametrize(
"solver,io_api",
[p for p in params if p not in [("mindopt", "lp"), ("mindopt", "lp-polars")]],
"solver,io_api,with_names",
[p for p in params if (p[0],p[1]) not in [("mindopt", "lp"), ("mindopt", "lp-polars")]],
)
def test_quadratic_model(quadratic_model, solver, io_api):
def test_quadratic_model(quadratic_model, solver, io_api, with_names):
if solver in feasible_quadratic_solvers:
status, condition = quadratic_model.solve(solver, io_api=io_api)
status, condition = quadratic_model.solve(solver, io_api=io_api, with_names=with_names)
assert condition == "optimal"
assert (quadratic_model.solution.x.round(3) == 0).all()
assert (quadratic_model.solution.y.round(3) >= 10).all()
assert round(quadratic_model.objective.value, 3) == 0
else:
with pytest.raises(ValueError):
quadratic_model.solve(solver, io_api=io_api)
quadratic_model.solve(solver, io_api=io_api, with_names=with_names)


@pytest.mark.parametrize(
"solver,io_api",
[p for p in params if p not in [("mindopt", "lp"), ("mindopt", "lp-polars")]],
"solver,io_api,with_names",
[p for p in params if (p[0],p[1]) not in [("mindopt", "lp"), ("mindopt", "lp-polars")]],
)
def test_quadratic_model_cross_terms(quadratic_model_cross_terms, solver, io_api):
def test_quadratic_model_cross_terms(quadratic_model_cross_terms, solver, io_api, with_names):
if solver in feasible_quadratic_solvers:
status, condition = quadratic_model_cross_terms.solve(solver, io_api=io_api)
status, condition = quadratic_model_cross_terms.solve(solver, io_api=io_api, with_names=with_names)
assert condition == "optimal"
assert (quadratic_model_cross_terms.solution.x.round(3) == 1.5).all()
assert (quadratic_model_cross_terms.solution.y.round(3) == 8.5).all()
assert round(quadratic_model_cross_terms.objective.value, 3) == 77.5
else:
with pytest.raises(ValueError):
quadratic_model_cross_terms.solve(solver, io_api=io_api)
quadratic_model_cross_terms.solve(solver, io_api=io_api, with_names=with_names)


@pytest.mark.parametrize(
"solver,io_api",
[p for p in params if p not in [("mindopt", "lp"), ("mindopt", "lp-polars")]],
"solver,io_api,with_names",
[p for p in params if (p[0],p[1]) not in [("mindopt", "lp"), ("mindopt", "lp-polars")]],
)
def test_quadratic_model_wo_constraint(quadratic_model, solver, io_api):
def test_quadratic_model_wo_constraint(quadratic_model, solver, io_api, with_names):
quadratic_model.constraints.remove("con0")
if solver in feasible_quadratic_solvers:
status, condition = quadratic_model.solve(solver, io_api=io_api)
status, condition = quadratic_model.solve(solver, io_api=io_api, with_names=with_names)
assert condition == "optimal"
assert (quadratic_model.solution.x.round(3) == 0).all()
assert round(quadratic_model.objective.value, 3) == 0
else:
with pytest.raises(ValueError):
quadratic_model.solve(solver, io_api=io_api)
quadratic_model.solve(solver, io_api=io_api, with_names=with_names)


@pytest.mark.parametrize(
"solver,io_api",
[p for p in params if p not in [("mindopt", "lp"), ("mindopt", "lp-polars")]],
"solver,io_api,with_names",
[p for p in params if (p[0],p[1]) not in [("mindopt", "lp"), ("mindopt", "lp-polars")]],
)
def test_quadratic_model_unbounded(quadratic_model_unbounded, solver, io_api):
def test_quadratic_model_unbounded(quadratic_model_unbounded, solver, io_api, with_names):
if solver in feasible_quadratic_solvers:
status, condition = quadratic_model_unbounded.solve(solver, io_api=io_api)
status, condition = quadratic_model_unbounded.solve(solver, io_api=io_api, with_names=with_names)
assert condition in ["unbounded", "unknown", "infeasible_or_unbounded"]
else:
with pytest.raises(ValueError):
quadratic_model_unbounded.solve(solver, io_api=io_api)
quadratic_model_unbounded.solve(solver, io_api=io_api, with_names=with_names)


@pytest.mark.parametrize(
"solver,io_api", [p for p in params if p[0] not in ["mindopt"]]
"solver,io_api,with_names", [p for p in params if p[0] not in ["mindopt"]]
)
def test_modified_model(modified_model, solver, io_api):
status, condition = modified_model.solve(solver, io_api=io_api)
def test_modified_model(modified_model, solver, io_api, with_names):
status, condition = modified_model.solve(solver, io_api=io_api, with_names=with_names)
assert condition == "optimal"
assert (modified_model.solution.x == 0).all()
assert (modified_model.solution.y == 10).all()


@pytest.mark.parametrize("solver,io_api", params)
def test_masked_variable_model(masked_variable_model, solver, io_api):
masked_variable_model.solve(solver, io_api=io_api)
@pytest.mark.parametrize("solver,io_api,with_names", params)
def test_masked_variable_model(masked_variable_model, solver, io_api, with_names):
masked_variable_model.solve(solver, io_api=io_api, with_names=with_names)
x = masked_variable_model.variables.x
y = masked_variable_model.variables.y
assert y.solution[-2:].isnull().all()
@@ -633,24 +642,24 @@ def test_masked_variable_model(masked_variable_model, solver, io_api):
assert_equal(x.add(y).solution, x.solution + y.solution.fillna(0))


@pytest.mark.parametrize("solver,io_api", params)
def test_masked_constraint_model(masked_constraint_model, solver, io_api):
masked_constraint_model.solve(solver, io_api=io_api)
@pytest.mark.parametrize("solver,io_api,with_names", params)
def test_masked_constraint_model(masked_constraint_model, solver, io_api, with_names):
masked_constraint_model.solve(solver, io_api=io_api, with_names=with_names)
assert (masked_constraint_model.solution.y[:-2] == 10).all()
assert (masked_constraint_model.solution.y[-2:] == 5).all()


@pytest.mark.parametrize("solver,io_api", params)
def test_basis_and_warmstart(tmp_path, model, solver, io_api):
@pytest.mark.parametrize("solver,io_api,with_names", params)
def test_basis_and_warmstart(tmp_path, model, solver, io_api, with_names):
basis_fn = tmp_path / "basis.bas"
model.solve(solver, basis_fn=basis_fn)
model.solve(solver, warmstart_fn=basis_fn)
model.solve(solver, basis_fn=basis_fn, io_api=io_api, with_names=with_names)
model.solve(solver, warmstart_fn=basis_fn, io_api=io_api, with_names=with_names)


@pytest.mark.parametrize("solver,io_api", params)
def test_solution_fn_parent_dir_doesnt_exist(model, solver, io_api, tmp_path):
@pytest.mark.parametrize("solver,io_api,with_names", params)
def test_solution_fn_parent_dir_doesnt_exist(model, solver, io_api, with_names, tmp_path):
solution_fn = tmp_path / "non_existent_dir" / "non_existent_file"
status, condition = model.solve(solver, io_api=io_api, solution_fn=solution_fn)
status, condition = model.solve(solver, io_api=io_api, with_names=with_names, solution_fn=solution_fn)
assert status == "ok"


@@ -660,8 +669,8 @@ def test_non_supported_solver_io(model, solver):
model.solve(solver, io_api="non_supported")


@pytest.mark.parametrize("solver,io_api", params)
def test_solver_attribute_getter(model, solver, io_api):
@pytest.mark.parametrize("solver,io_api,with_names", params)
def test_solver_attribute_getter(model, solver, io_api, with_names):
model.solve(solver)
if solver != "gurobi":
with pytest.raises(NotImplementedError):
@@ -672,24 +681,24 @@ def test_solver_attribute_getter(model, solver, io_api):
assert set(rc) == set(model.variables)


@pytest.mark.parametrize("solver,io_api", params)
def test_model_resolve(model, solver, io_api):
status, condition = model.solve(solver, io_api=io_api)
@pytest.mark.parametrize("solver,io_api,with_names", params)
def test_model_resolve(model, solver, io_api, with_names):
status, condition = model.solve(solver, io_api=io_api, with_names=with_names)
assert status == "ok"
# x = -0.1, y = 1.7
assert np.isclose(model.objective.value, 3.3)

# add another constraint after solve
model.add_constraints(model.variables.y >= 3)

status, condition = model.solve(solver, io_api=io_api)
status, condition = model.solve(solver, io_api=io_api, with_names=with_names)
assert status == "ok"
# x = -0.75, y = 3.0
assert np.isclose(model.objective.value, 5.25)


@pytest.mark.parametrize("solver,io_api", [p for p in params if "direct" not in p])
def test_solver_classes_from_problem_file(model, solver, io_api):
@pytest.mark.parametrize("solver,io_api,with_names", [p for p in params if "direct" not in p])
def test_solver_classes_from_problem_file(model, solver, io_api, with_names):
# first test initialization of super class. Should not be possible to initialize
with pytest.raises(TypeError):
solver_super = solvers.Solver() # noqa F841
@@ -699,7 +708,7 @@ def test_solver_classes_from_problem_file(model, solver, io_api):
solver_ = solver_class()
# get problem file for testing
problem_fn = model.get_problem_file(io_api=io_api)
model.to_file(to_path(problem_fn), io_api)
model.to_file(to_path(problem_fn), io_api=io_api, with_names=with_names)
solution_fn = model.get_solution_file() if solver in ["glpk", "cbc"] else None
result = solver_.solve_problem(problem_fn=problem_fn, solution_fn=solution_fn)
assert result.status.status.value == "ok"
@@ -720,8 +729,8 @@ def test_solver_classes_from_problem_file(model, solver, io_api):
solver_.solve_problem(problem_fn=solution_fn)


@pytest.mark.parametrize("solver,io_api", params)
def test_solver_classes_direct(model, solver, io_api):
@pytest.mark.parametrize("solver,io_api,with_names", params)
def test_solver_classes_direct(model, solver, io_api, with_names):
# initialize the solver as object of solver subclass <solver_class>
solver_class = getattr(solvers, f"{solvers.SolverName(solver).name}")
solver_ = solver_class()