Skip to content

Fix GLPK objective parsing#818

Open
danielelerede-oet wants to merge 6 commits into
PyPSA:masterfrom
open-energy-transition:fix/glpk-objective-parser
Open

Fix GLPK objective parsing#818
danielelerede-oet wants to merge 6 commits into
PyPSA:masterfrom
open-energy-transition:fix/glpk-objective-parser

Conversation

@danielelerede-oet

Copy link
Copy Markdown
Contributor

Changes proposed in this Pull Request

While working at open-energy-transition/solver-benchmark#490 I noticed an issue in GLPK result parsing: linopy currently removes all characters except those in scientific notation and e characters are preserved in the surrounding text, raising a ValueError. In this PR the first valid floating-point/scientific-notation token using a regular expression is extracted and the error is now raised if no objective value can be parsed.

Motivation

While benchmarking GLPK through Linopy, we encountered parsing failures caused by objective strings containing additional text. This change makes the parser robust to such cases while preserving the existing behavior for valid objective values.

Checklist

  • AI-generated content is marked (see AGENTS.md).
  • Code changes are sufficiently documented; i.e. new functions contain docstrings and further explanations may be given in doc.
  • Unit tests for new features were added (if applicable).
  • A note for the release notes doc/release_notes.rst of the upcoming release is included.
  • I consent to the release of this PR's code under the MIT license.

@codspeed-hq

codspeed-hq Bot commented Jul 6, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 173 untouched benchmarks
⏩ 173 skipped benchmarks1


Comparing open-energy-transition:fix/glpk-objective-parser (8d7f0d4) with master (e861678)

Open in CodSpeed

Footnotes

  1. 173 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@FBumann

FBumann commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Hi @danielelerede-oet,
I looked into it and think this is a good change. However, id like to make this airtight as we are on it.

I would suggest refactoring this into a classmethod and private class-attribute. I propose this:

_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 (which may itself contain digits
    or the letter ``e``) 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))

...
objective = self._parse_objective(info.Objective)

And we need tests to verify it works

@pytest.mark.parametrize(
    "objective_text, expected",
    [
        # ``info.Objective`` after pd.read_csv splits off the "Objective:" key.
        (" 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),
        # scientific notation
        (" obj = 1.5e+06 (MAXimum)", 1.5e6),
        (" obj = -2E-3 (MINimum)", -2e-3),
        # An objective name whose letters include ``e`` must not leak into the stripped number and crash float().
        (" net_present_value = 1234.5 (MINimum)", 1234.5),
        # robustness: a digit in the objective name must not be picked up as the value
        (" obj1 = 1234.56 (MINimum)", 1234.56),
        (" c2e = -7.5e2 (MAXimum)", -750.0),
        # GLPK omits the "<name> =" prefix when the objective is unnamed. Fallback must work
        ("  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 GLPK._parse_objective(objective_text) == pytest.approx(expected)


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

Those tests cover your bug and edge cases. Feel free to add more and check if the regex works for them

@FBumann

FBumann commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

If you dont have the time i could push the changes to aour branch or superseed the PR. Tell me

@danielelerede-oet

Copy link
Copy Markdown
Contributor Author

Hi @FBumann , no problem, what you asked was quite easy.
I refactored the GLPK objective parsing into a class-level regex and _parse_objective helper, and added parser unit tests covering the reported issue and related edge cases.

BTW, I couldn't run the test file locally because master fails during collection with an unrelated SyntaxError in linopy/solvers.py around the HiGHS docstring (invalid character '—').

Comment thread linopy/solvers.py Outdated
Comment thread linopy/solvers.py Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants