Skip to content

Commit 553fc3f

Browse files
committed
Make Results.converged tolerate an unparseable solver log
Results.converged built a dict comprehension over solver_runs, so the first solver whose .data log raised GmatOutputParseError (an unsupported solver type, a malformed file) aborted the whole property — hiding the convergence status of every other, parseable solver. - converged now loops, catching GmatOutputParseError per solver: an unparseable log is omitted from the returned dict and a UserWarning names it. Parseable solvers are unaffected. Keys are now the solvers whose log parsed (use `name in result.converged` to spot an omitted one). - Add a test: a Results with one parseable DC solver and one unrecognised-header .data — converged returns the DC status and warns, rather than raising. Closes #143
1 parent e07a196 commit 553fc3f

2 files changed

Lines changed: 48 additions & 3 deletions

File tree

src/gmat_run/results.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@
2525
import os
2626
import shutil
2727
import tempfile
28+
import warnings
2829
from collections.abc import Iterator, Mapping
2930
from pathlib import Path
3031
from types import MappingProxyType
3132

3233
import pandas as pd
3334

3435
from gmat_run._path_utils import resolve_user_path
36+
from gmat_run.errors import GmatOutputParseError
3537
from gmat_run.parsers.contact import parse as _parse_contact
3638
from gmat_run.parsers.ephemeris import parse as _parse_oem_ephemeris
3739
from gmat_run.parsers.reportfile import parse as _parse_reportfile
@@ -315,15 +317,35 @@ def converged(self) -> dict[str, bool]:
315317
"""``{solver name: bool}`` — did each solver run reach its goal?
316318
317319
A convenience view over :attr:`solver_runs` for the common branching
318-
case (``if not result.converged["DC"]: ...``). Same keys as
319-
:attr:`solver_runs`; ``{}`` when the mission declared no solvers.
320+
case (``if not result.converged["DC"]: ...``). ``{}`` when the mission
321+
declared no solvers.
322+
323+
Keys are the solvers whose ``.data`` log parsed successfully. A solver
324+
whose log cannot be parsed — an unsupported solver type, a malformed
325+
file — is omitted with a :class:`UserWarning` rather than failing the
326+
whole property, so one bad log does not hide every other solver's
327+
status. Use ``name in result.converged`` to tell an omitted solver
328+
apart from a converged/diverged one.
320329
321330
Reading this materialises every solver run (it inspects each
322331
DataFrame's ``attrs["converged"]``), so the lazy-parse cost is paid on
323332
first access — the same trade-off as iterating :attr:`solver_runs`
324333
values directly.
325334
"""
326-
return {name: bool(self.solver_runs[name].attrs["converged"]) for name in self.solver_runs}
335+
statuses: dict[str, bool] = {}
336+
for name in self.solver_runs:
337+
try:
338+
df = self.solver_runs[name]
339+
except GmatOutputParseError as exc:
340+
warnings.warn(
341+
f"solver run {name!r} could not be parsed; omitting it from "
342+
f"Results.converged: {exc}",
343+
UserWarning,
344+
stacklevel=2,
345+
)
346+
continue
347+
statuses[name] = bool(df.attrs["converged"])
348+
return statuses
327349

328350
def persist(self, path: str | os.PathLike[str]) -> Results:
329351
"""Copy every output artefact under :attr:`output_dir` into ``path``.

tests/test_results.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,16 @@ def test_contacts_unknown_key_raises_keyerror(tmp_path: Path) -> None:
427427
# Same file with the achieved value far from the goal — does not converge.
428428
_SOLVER_DC_DIVERGED = _SOLVER_DC_CONVERGED.replace("6999.9995", "5000.0")
429429

430+
# A solver-log header that is neither DifferentialCorrector nor Yukon — the
431+
# solver_log parser raises GmatOutputParseError on it (unsupported solver type).
432+
_SOLVER_UNRECOGNISED = """\
433+
********************************************************
434+
*** Performing SNOPT Optimization (using "Opt1")
435+
********************************************************
436+
437+
Iteration 1
438+
"""
439+
430440

431441
def _write_solver(path: Path, content: str = _SOLVER_DC_CONVERGED) -> Path:
432442
path.write_text(content, encoding="utf-8")
@@ -496,6 +506,19 @@ def test_converged_reflects_each_solver(tmp_path: Path) -> None:
496506
assert result.converged == {"DC": True, "DC2": False}
497507

498508

509+
def test_converged_omits_unparseable_solver_with_warning(tmp_path: Path) -> None:
510+
"""A solver whose .data log cannot be parsed (an unsupported solver type)
511+
is omitted from converged with a UserWarning — one bad log must not fail
512+
the whole property and hide every other solver's status (#143)."""
513+
ok = _write_solver(tmp_path / "DC.data", _SOLVER_DC_CONVERGED)
514+
bad = _write_solver(tmp_path / "Opt1.data", _SOLVER_UNRECOGNISED)
515+
result = Results(output_dir=tmp_path, log="", solver_paths={"DC": ok, "Opt1": bad})
516+
with pytest.warns(UserWarning, match="Opt1"):
517+
converged = result.converged
518+
# The parseable solver's status survives; the unparseable one is absent.
519+
assert converged == {"DC": True}
520+
521+
499522
def test_solver_max_iterations_threaded_to_parser(tmp_path: Path) -> None:
500523
"""``solver_max_iterations`` reaches the parser — it distinguishes max_iter."""
501524
data = _write_solver(tmp_path / "DC.data", _SOLVER_DC_DIVERGED)

0 commit comments

Comments
 (0)