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
10 changes: 6 additions & 4 deletions src/matkit/graspa/graspa.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ def _extract_adsorbate_averages(text: str) -> dict[str, tuple[float, float]]:
for raw in text.splitlines():
line = raw.strip()
matched_section = next(
(name for name, marker in _SECTION_MARKERS.items() if marker in line),
(
name
for name, marker in _SECTION_MARKERS.items()
if marker in line
),
None,
)
if matched_section is not None:
Expand Down Expand Up @@ -116,9 +120,7 @@ def get_output_data(
try:
text = (Path(output_path) / output_fname).read_text()

time_line = next(
(ln for ln in text.splitlines() if "Work" in ln), None
)
time_line = next((ln for ln in text.splitlines() if "Work" in ln), None)
if time_line is None:
raise ValueError("Could not find timing line in output.")

Expand Down
60 changes: 60 additions & 0 deletions tests/data/graspa/single_component.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Synthetic gRASPA output for parser tests (MIT).
# Values are intentionally distinct, not physically consistent.

HEAT OF ADSORPTION
COMPONENT [1] (CO2)
Block[0]: Average: 24.0
Block[1]: Average: 26.0
Overall: Average: 25.0, +/- 0.1

LOADING: # MOLECULES
COMPONENT [0] (framework)
Block[0]: Average: 512.0
Block[1]: Average: 512.0
Overall: Average: 512.0, +/- 0.0
EXCESS LOADING
Overall: Average: 511.0, +/- 0.0
COMPONENT [1] (CO2)
Block[0]: Average: 7.0
Block[1]: Average: 9.0
Overall: Average: 8.0, +/- 0.1
EXCESS LOADING
Overall: Average: 7.0, +/- 0.2

LOADING: mg/g
COMPONENT [0] (framework)
Block[0]: Average: 1000.0
Block[1]: Average: 1000.0
Overall: Average: 1000.0, +/- 0.0
EXCESS LOADING
Overall: Average: 999.0, +/- 0.0
COMPONENT [1] (CO2)
Block[0]: Average: 5.0
Block[1]: Average: 7.0
Overall: Average: 6.0, +/- 0.1
EXCESS LOADING
Overall: Average: 4.0, +/- 0.2

LOADING: mol/kg
COMPONENT [0] (framework)
Block[0]: Average: 2000.0
Block[1]: Average: 2000.0
Overall: Average: 2000.0, +/- 0.0
EXCESS LOADING
Overall: Average: 1999.0, +/- 0.0
COMPONENT [1] (CO2)
Block[0]: Average: 11.0
Block[1]: Average: 13.0
Overall: Average: 12.0, +/- 0.1
EXCESS LOADING
Overall: Average: 10.0, +/- 0.2

LOADING: g/L
COMPONENT [1] (CO2)
Block[0]: Average: 13.0
Block[1]: Average: 15.0
Overall: Average: 14.0, +/- 0.1
EXCESS LOADING
Overall: Average: ********, +/- ********

Work time 2.0
11 changes: 7 additions & 4 deletions tests/fixtures/fake_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@
(fixtures / f"test_structure.{analysis}").read_bytes()
)
elif mode == "graspa":
for i in range(14):
print(f"Overall: Average: {25 if i == 0 else i + 1}, +/- 0.1")
if "--partial" not in sys.argv:
print("Work time 2.0")
fixture = (
Path(__file__).parents[1] / "data" / "graspa" / "single_component.txt"
)
for line in fixture.read_text().splitlines():
if "--partial" in sys.argv and line.startswith("Work time"):
continue
print(line)
else:
raise SystemExit("unknown fixture engine")
13 changes: 6 additions & 7 deletions tests/test_api_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,14 +316,9 @@ def test_subprocess_execution_and_timeout(sample_cif, tmp_path):


@pytest.mark.parametrize(
"unit,fugacity,expected",
[
("mol/kg", "PR-EOS", 12),
("mg/g", "PR-EOS", 6),
("g/L", "PR-EOS", 14),
("mol/kg", 1.0, 7),
],
"unit,expected", [("mol/kg", 12), ("mg/g", 6), ("g/L", 14)]
)
@pytest.mark.parametrize("fugacity", ["PR-EOS", 1.0])
def test_single_component_adsorption_requires_charges_and_collects(
sample_cif, tmp_path, unit, fugacity, expected
):
Expand Down Expand Up @@ -360,6 +355,10 @@ def test_single_component_adsorption_requires_charges_and_collects(
)
assert result.accepted
assert result.payload.uptake == expected
assert result.payload.unit == unit
assert result.payload.uncertainty == 0.1
assert result.payload.heat_of_adsorption == 25
assert result.payload.heat_uncertainty == 0.1
assert result.payload.component == "CO2"
assert result.checks[0].status == "unknown"
assert (
Expand Down
108 changes: 105 additions & 3 deletions tests/test_graspa.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pytest
from pathlib import Path

from matkit.graspa import get_output_data
from matkit.graspa.graspa import (
generate_component_blocks,
setup_batch,
Expand All @@ -14,6 +15,109 @@
from matkit.utils.cif import sanitize_cif_stem


@pytest.fixture
def graspa_log(test_data_dir):
return (test_data_dir / "graspa" / "single_component.txt").read_text()


class TestGetOutputData:
"""Select absolute adsorbate statistics from sectioned engine output."""

@pytest.mark.parametrize(
"unit,expected", [("mol/kg", 12), ("mg/g", 6), ("g/L", 14)]
)
@pytest.mark.parametrize("eos", [False, True])
def test_absolute_adsorbate_statistics(
self, tmp_path, graspa_log, unit, expected, eos
):
# The fixture also contains framework and excess averages, including
# overflowing excess g/L values that must never be parsed as floats.
(tmp_path / "raspa.log").write_text(graspa_log)

assert get_output_data(str(tmp_path), unit=unit, eos=eos) == {
"success": True,
"uptake": expected,
"error": 0.1,
"unit": unit,
"qst": 25.0,
"error_qst": 0.1,
"qst_unit": "kJ/mol",
"calc_time_in_s": 2.0,
}

@pytest.mark.parametrize(
"unit,expected", [("mol/kg", 12), ("mg/g", 6), ("g/L", 14)]
)
@pytest.mark.parametrize(
"variation", ["extra_blocks", "reordered_sections", "later_component"]
)
def test_layout_variations(
self, tmp_path, graspa_log, unit, expected, variation
):
sections = graspa_log.strip().split("\n\n")
if variation == "extra_blocks":
text = graspa_log.replace(
"Overall: Average:",
"Block[2]: Average: 999.0\nOverall: Average:",
)
elif variation == "reordered_sections":
text = "\n\n".join(
[sections[0], *reversed(sections[1:-1]), sections[-1]]
)
else:
sections[1:-1] = [
section
+ "\nCOMPONENT [2] (N2)\n"
+ "Overall: Average: 999.0, +/- 99.0"
for section in sections[1:-1]
]
text = "\n\n".join(sections)
(tmp_path / "raspa.log").write_text(text)

result = get_output_data(str(tmp_path), unit=unit)
assert result["uptake"] == expected
assert result["error"] == 0.1
assert result["qst"] == 25.0
assert result["error_qst"] == 0.1

@pytest.mark.parametrize(
"missing,message",
[
("timing", "Could not find timing line"),
("loading", "Could not find mol/kg loading"),
("headers", "Could not find uptake lines"),
],
)
def test_incomplete_output_raises(
self, tmp_path, graspa_log, missing, message
):
if missing == "timing":
text = graspa_log.replace("Work time 2.0", "")
elif missing == "loading":
text = "\n\n".join(
section
for section in graspa_log.split("\n\n")
if not section.startswith("LOADING: mol/kg")
)
else:
text = "\n".join(
line
for line in graspa_log.splitlines()
if line.startswith(("Overall:", "Work time"))
)
(tmp_path / "raspa.log").write_text(text)

with pytest.raises(ValueError, match=message):
get_output_data(str(tmp_path))

def test_unsupported_unit_raises(self, tmp_path, graspa_log):
(tmp_path / "raspa.log").write_text(graspa_log)
with pytest.raises(
ValueError, match="Unit unsupported is not supported"
):
get_output_data(str(tmp_path), unit="unsupported")


class TestGenerateComponentBlocks:
"""Tests for gRASPA component block generation."""

Expand Down Expand Up @@ -167,9 +271,7 @@ def test_multiple_periods_replaced(self):
class TestSetupBatch:
"""Tests for gRASPA batch setup, focusing on CIF rename mapping."""

def test_batch_writes_mapping_only_for_renamed(
self, sample_cif, tmp_path
):
def test_batch_writes_mapping_only_for_renamed(self, sample_cif, tmp_path):
"""cif_mapping.json should list only CIFs that needed renaming."""
cif_dir = tmp_path / "cifs"
cif_dir.mkdir()
Expand Down
Loading