Skip to content

Commit 3c3571f

Browse files
Passthrough invalid components
Whenever models where chained, only the components that would be accepted by a model would be included. Say H2O was an output in model A, but not a valid component in model B. Then the component would be ignored as of Model B. This is especially important whenever we will get another section in the chain, as otherwise a model in between could end up removing components for models later.
1 parent c0cd4bf commit 3c3571f

3 files changed

Lines changed: 123 additions & 0 deletions

File tree

backend/src/acidwatch_api/models/base.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,11 +307,39 @@ def concentrations(self) -> dict[str, float | int]:
307307
return self._concentrations
308308

309309
def set_concentrations(self, value: dict[str, float | int]) -> None:
310+
self._all_concentrations = dict(value)
310311
self._concentrations = {
311312
subst: value.get(subst, 0.0)
312313
for subst in getattr(self, "valid_substances", [])
313314
}
314315

316+
@property
317+
def passthrough_concentrations(self) -> dict[str, float | int]:
318+
return {
319+
k: v
320+
for k, v in self._all_concentrations.items()
321+
if k not in self.valid_substances
322+
}
323+
324+
def merge_passthrough(self, phases: list[Phase]) -> list[Phase]:
325+
passthrough = self.passthrough_concentrations
326+
if not passthrough:
327+
return phases
328+
329+
merged: list[Phase] = []
330+
for phase in phases:
331+
if phase.kind == "co2-rich":
332+
merged.append(
333+
Phase(
334+
kind=phase.kind,
335+
fraction=phase.fraction,
336+
concentrations={**passthrough, **phase.concentrations},
337+
)
338+
)
339+
else:
340+
merged.append(phase)
341+
return merged
342+
315343
def validate_concentrations(self, value: dict[str, float | int]) -> None:
316344
concentrations_errors = {
317345
subst: ["Extra inputs are not permitted"]

backend/src/acidwatch_api/routes/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,8 @@ async def _run_adapter(
224224
else:
225225
phases, *panels = result
226226

227+
phases = adapter.merge_passthrough(phases)
228+
227229
result_obj = db.ModelResult(
228230
model_input_id=model_input_id,
229231
phases=[p.model_dump() for p in phases],
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import pytest
2+
from acidwatch_api.models.base import BaseAdapter
3+
from acidwatch_api.models.datamodel import Phase
4+
5+
6+
class DummyAdapter(BaseAdapter):
7+
model_id = "dummy"
8+
display_name = "Dummy"
9+
description = "test"
10+
category = "ChemicalEquilibrium"
11+
valid_substances = ["CO2", "HCl"]
12+
13+
async def run(self):
14+
return []
15+
16+
17+
@pytest.fixture
18+
def adapter():
19+
a = DummyAdapter(parameters=None, jwt_token=None)
20+
return a
21+
22+
23+
class TestSetConcentrations:
24+
def test_filters_to_valid_substances(self, adapter):
25+
adapter.set_concentrations({"CO2": 1.0, "HCl": 2.0, "H2O": 3.0})
26+
assert adapter.concentrations == {"CO2": 1.0, "HCl": 2.0}
27+
28+
def test_defaults_missing_valid_substances_to_zero(self, adapter):
29+
adapter.set_concentrations({"CO2": 1.0, "H2O": 3.0})
30+
assert adapter.concentrations == {"CO2": 1.0, "HCl": 0.0}
31+
32+
33+
class TestPassthroughConcentrations:
34+
@pytest.mark.parametrize(
35+
"input_concs,expected",
36+
[
37+
({"CO2": 1.0, "HCl": 2.0}, {}),
38+
({"CO2": 1.0, "H2O": 3.0}, {"H2O": 3.0}),
39+
({"H2O": 3.0, "NaCl": 5.0}, {"H2O": 3.0, "NaCl": 5.0}),
40+
],
41+
ids=[
42+
"all_handled",
43+
"one_unhandled",
44+
"all_unhandled",
45+
],
46+
)
47+
def test_returns_unhandled_components(self, adapter, input_concs, expected):
48+
adapter.set_concentrations(input_concs)
49+
assert adapter.passthrough_concentrations == expected
50+
51+
52+
class TestMergePassthrough:
53+
def test_no_passthrough_returns_phases_unchanged(self, adapter):
54+
adapter.set_concentrations({"CO2": 1.0, "HCl": 2.0})
55+
phases = [Phase(kind="co2-rich", fraction=1.0, concentrations={"CO2": 0.5})]
56+
result = adapter.merge_passthrough(phases)
57+
assert result is phases
58+
59+
@pytest.mark.parametrize(
60+
"phase_kind,expect_merge",
61+
[
62+
("co2-rich", True),
63+
("aqueous", False),
64+
],
65+
)
66+
def test_only_merges_into_co2_rich_phases(self, adapter, phase_kind, expect_merge):
67+
adapter.set_concentrations({"CO2": 1.0, "H2O": 3.0})
68+
phases = [Phase(kind=phase_kind, fraction=1.0, concentrations={"CO2": 0.5})]
69+
result = adapter.merge_passthrough(phases)
70+
if expect_merge:
71+
assert result[0].concentrations == {"CO2": 0.5, "H2O": 3.0}
72+
else:
73+
assert result[0].concentrations == {"CO2": 0.5}
74+
75+
def test_model_output_takes_precedence_over_passthrough(self, adapter):
76+
adapter.set_concentrations({"CO2": 1.0, "H2O": 3.0, "NaCl": 7.0})
77+
phases = [
78+
Phase(
79+
kind="co2-rich", fraction=1.0, concentrations={"CO2": 0.5, "NaCl": 9.0}
80+
)
81+
]
82+
result = adapter.merge_passthrough(phases)
83+
assert result[0].concentrations == {"CO2": 0.5, "H2O": 3.0, "NaCl": 9.0}
84+
85+
def test_multiple_phases(self, adapter):
86+
adapter.set_concentrations({"CO2": 1.0, "H2O": 3.0})
87+
phases = [
88+
Phase(kind="co2-rich", fraction=0.7, concentrations={"CO2": 0.5}),
89+
Phase(kind="aqueous", fraction=0.3, concentrations={"CO2": 0.1}),
90+
]
91+
result = adapter.merge_passthrough(phases)
92+
assert result[0].concentrations == {"CO2": 0.5, "H2O": 3.0}
93+
assert result[1].concentrations == {"CO2": 0.1}

0 commit comments

Comments
 (0)