Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
23 changes: 22 additions & 1 deletion linopy/solvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1367,6 +1367,27 @@ def get_solver_solution() -> Solution:
class GLPK(Solver[None]):
"""
Solver subclass for the GLPK solver.
"""

_OBJECTIVE_TOKEN: ClassVar[re.Pattern[str]] = re.compile(
r"[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?"
Comment thread
danielelerede-oet marked this conversation as resolved.
Outdated
)

@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))

Attributes
Comment thread
danielelerede-oet marked this conversation as resolved.
Outdated
----------
Expand Down Expand Up @@ -1472,7 +1493,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
36 changes: 36 additions & 0 deletions test/test_solvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -570,6 +570,42 @@ 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