Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions doc/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Upcoming Version

**Bug fixes**

* Fix GLPK objective parsing. (https://github.com/PyPSA/linopy/pull/818)
* LP file export now honors bounds tightened below ``[0, 1]`` on a binary variable via the ``.lower``/``.upper`` setters after creation (e.g. ``upper = 0``). Previously such bounds were written only by ``io_api="direct"`` and dropped by ``io_api="lp"``. (https://github.com/PyPSA/linopy/issues/776)
* Freezing an empty constraint group (e.g. an empty ``isel`` slice) no longer raises ``ValueError: cannot reshape array of size 0``. ``Model(freeze_constraints=True)`` and ``Constraint.freeze()`` now round-trip zero-row constraints losslessly.
* ``Variable.where`` no longer raises ``ValueError: exact match required for all data variable names`` once a solution is attached (after ``Model.solve``) or the variable is fixed. The fill value now covers auxiliary data variables (``solution``, stashed bounds) instead of only ``labels``/``lower``/``upper``.
Expand Down
22 changes: 21 additions & 1 deletion linopy/solvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1382,6 +1382,26 @@ class GLPK(Solver[None]):
}
)

_OBJECTIVE_TOKEN: ClassVar[re.Pattern[str]] = re.compile(
r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?"
)

@classmethod
def _parse_objective(cls, text: str) -> float:
"""
Extract the objective value from a GLPK ``Objective:`` line.
GLPK reports the objective as ``<name> = <value> (MINimum)``. Anchoring
on the ``=`` drops the objective name before matching the first
float/scientific-notation token, so surrounding text can no longer
corrupt the parsed value.
"""
_, _, tail = text.rpartition("=")
match = cls._OBJECTIVE_TOKEN.search(tail)
if match is None:
raise ValueError(f"Could not parse objective value: {text!r}")
return float(match.group(0))

@classmethod
@functools.cache
def is_available(cls) -> bool:
Expand Down Expand Up @@ -1472,7 +1492,7 @@ def read_until_break(f: io.TextIOWrapper) -> Generator[str, None, None]:
info_io = io.StringIO("".join(read_until_break(f))[:-2])
info = pd.read_csv(info_io, sep=":", index_col=0, header=None)[1]
condition = info.Status.lower().strip()
objective = float(re.sub(r"[^0-9\.\+\-e]+", "", info.Objective))
objective = self._parse_objective(info.Objective)

termination_condition = CONDITION_MAP.get(condition, condition)
status = Status.from_termination_condition(termination_condition)
Expand Down
35 changes: 35 additions & 0 deletions test/test_solvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,41 @@ def test_assign_result_without_solver_kwarg_leaves_solver_unset(self) -> None:
assert m.solver is None


@pytest.mark.parametrize(
"objective_text, expected",
[
(" obj = 1234.56 (MINimum)", 1234.56),
(" obj = 0 (MINimum)", 0.0),
(" obj = -3.5 (MAXimum)", -3.5),
(" obj = +42 (MINimum)", 42.0),
(" obj = .5 (MINimum)", 0.5),
(" obj = 1.5e+06 (MAXimum)", 1.5e6),
(" obj = -2E-3 (MINimum)", -2e-3),
(" net_present_value = 1234.5 (MINimum)", 1234.5),
(" obj1 = 1234.56 (MINimum)", 1234.56),
(" c2e = -7.5e2 (MAXimum)", -750.0),
(" 3.5 (MINimum)", 3.5),
(" -1.5e3 (MAXimum)", -1500.0),
(" 0 (MINimum)", 0.0),
],
)
def test_parse_glpk_objective(objective_text: str, expected: float) -> None:
assert solvers.GLPK._parse_objective(objective_text) == pytest.approx(expected)


@pytest.mark.parametrize(
"objective_text",
[
" obj = (MINimum)",
" unbounded",
"",
],
)
def test_parse_glpk_objective_no_value_raises(objective_text: str) -> None:
with pytest.raises(ValueError, match="Could not parse objective value"):
solvers.GLPK._parse_objective(objective_text)


mosek_installed = pytest.importorskip("mosek", reason="Mosek is not installed")


Expand Down
Loading