Skip to content

Commit 117dc75

Browse files
Incorporate phases in frontend
We retain the concentrations in the backend api, hence it will only be possible to provide the co2 feed (and it is implicitly set) - not able to give different phases.
1 parent d35f8ff commit 117dc75

11 files changed

Lines changed: 87 additions & 43 deletions

File tree

backend/src/acidwatch_api/models/datamodel.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,16 @@ class Phase(_BaseModel):
3636

3737

3838
class Simulation(_BaseModel):
39-
phases: list[Phase]
39+
concentrations: dict[str, int | float]
4040
conditions: Conditions = Field(default_factory=Conditions)
4141
models: list[ModelInput] = Field(min_length=1)
4242

43+
@property
44+
def phases(self) -> list[Phase]:
45+
return [
46+
Phase(kind="co2-rich", fraction=1.0, concentrations=self.concentrations)
47+
]
48+
4349

4450
class ModelResult(_BaseModel):
4551
phases: list[Phase]

backend/src/acidwatch_api/routes/models.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,9 @@ def get_result_for_simulation(
219219
)
220220

221221
simulation_input = Simulation(
222-
phases=db_simulation.phases,
222+
concentrations=_phases_to_concentrations(
223+
[Phase(**p) for p in db_simulation.phases]
224+
),
223225
conditions=Conditions(**(db_simulation.conditions or {})),
224226
models=model_inputs,
225227
)
@@ -277,7 +279,7 @@ async def run_simulation(
277279
except ValueError as exc:
278280
raise HTTPException(status_code=422, detail=exc.args)
279281

280-
concentrations = _phases_to_concentrations(create_simulation.phases)
282+
concentrations = create_simulation.concentrations
281283
try:
282284
adapters[0].validate_concentrations(concentrations)
283285
except InputError as exc:

backend/tests/test_models_endpoints.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def test_dummy_model_only_valid_substances_are_present(
138138
):
139139
monkeypatch.setattr(dummy_model, "valid_substances", valid_substances)
140140
simulation = {
141-
"phases": _make_phases(concentrations),
141+
"concentrations": concentrations,
142142
"models": [{"modelId": dummy_model.model_id, "parameters": {}}],
143143
}
144144
response = client.post(
@@ -317,7 +317,7 @@ async def run(self):
317317
response = client.post(
318318
"/simulations/",
319319
json={
320-
"phases": _make_phases({}),
320+
"concentrations": {},
321321
"models": [
322322
{"modelId": dummy_model.model_id, "parameters": input_parameters}
323323
],
@@ -340,7 +340,7 @@ async def run(self):
340340
assert response.json() == {
341341
"status": "done",
342342
"input": {
343-
"phases": _make_phases({}),
343+
"concentrations": {},
344344
"conditions": {"temperature": 25.0, "pressure": 10.0},
345345
"models": [
346346
{
@@ -367,7 +367,7 @@ async def run(self):
367367
def test_running_empty_list_of_models_is_an_error(client):
368368
response = client.post(
369369
"/simulations",
370-
json={"phases": _make_phases({"H2O": 2.0}), "models": []},
370+
json={"concentrations": {"H2O": 2.0}, "models": []},
371371
)
372372
assert response.status_code == 422
373373

@@ -401,7 +401,7 @@ def test_running_empty_list_of_models_is_an_error(client):
401401
)
402402
def test_running_models(client, input_models, result_concentrations):
403403
simulation_input = {
404-
"phases": _make_phases({"H2O": 1}),
404+
"concentrations": {"H2O": 1},
405405
"models": input_models,
406406
}
407407

@@ -452,7 +452,7 @@ def test_running_models(client, input_models, result_concentrations):
452452
)
453453
def test_failing_model(client, input_models, result):
454454
simulation_input = {
455-
"phases": _make_phases({"H2O": 1}),
455+
"concentrations": {"H2O": 1},
456456
"models": input_models,
457457
}
458458

@@ -530,7 +530,7 @@ def test_results_order(client, sql_session, swap):
530530
assert response.json() == {
531531
"status": "done",
532532
"input": {
533-
"phases": _make_phases({}),
533+
"concentrations": {},
534534
"conditions": {"temperature": 25.0, "pressure": 10.0},
535535
"models": [first_model, second_model],
536536
},

frontend/src/components/ParityPlots.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, { useState } from "react";
22
import { Button, NativeSelect, Typography } from "@equinor/eds-core-react";
33
import ScatterPlot, { ScatterDataSet } from "@/components/ScatterPlot";
44
import { ExperimentResult } from "@/dto/ExperimentResult";
5-
import { SimulationResults } from "@/dto/SimulationResults";
5+
import { SimulationResults, getCo2RichConcentrations } from "@/dto/SimulationResults";
66

77
const buildParityDatasets = (
88
experiments: ExperimentResult[],
@@ -16,7 +16,7 @@ const buildParityDatasets = (
1616
const simulations = simulationsPerExperiment[exp.name] ?? [];
1717
simulations.forEach((sim) => {
1818
const modelId = sim.input.models[0].modelId;
19-
const modelled = sim.results[0]?.concentrations?.[component] ?? 0;
19+
const modelled = getCo2RichConcentrations(sim.results[0]?.phases)[component] ?? 0;
2020
(byModel[modelId] ??= []).push({ x: measured, y: modelled });
2121
});
2222
});

frontend/src/components/Simulation/MassBalanceError.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Accordion, Card, Table, Typography } from "@equinor/eds-core-react";
22
import { useEffect, useState } from "react";
3+
import { Phase } from "@/dto/SimulationResults";
34

45
const ERROR_THRESHOLD = 1e-3;
56

@@ -127,13 +128,24 @@ export function getMassBalanceError(
127128
return { error, initMasses, finalMasses, substances };
128129
}
129130

131+
export function mergePhasesConcentrations(phases: Phase[]): Record<string, number> {
132+
const merged: Record<string, number> = {};
133+
for (const phase of phases) {
134+
for (const [substance, conc] of Object.entries(phase.concentrations)) {
135+
merged[substance] = (merged[substance] ?? 0) + conc * phase.fraction;
136+
}
137+
}
138+
return merged;
139+
}
140+
130141
interface MassBalanceErrorProps {
131142
initial: Record<string, number>;
132-
final: Record<string, number>;
143+
phases: Phase[];
133144
}
134145

135-
export function MassBalanceError({ initial, final }: MassBalanceErrorProps) {
146+
export function MassBalanceError({ initial, phases }: MassBalanceErrorProps) {
136147
const [isExpanded, setExpanded] = useState<boolean>(false);
148+
const final = mergePhasesConcentrations(phases);
137149
const { error, initMasses, finalMasses, substances } = getMassBalanceError(initial, final);
138150
const significantError = error >= 1;
139151

frontend/src/components/Simulation/Results.tsx

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,26 +69,23 @@ const Results: React.FC<ResultsProps> = ({ simulationResults }) => {
6969
const modelId = simulationResults.input.models[modelIndex]?.modelId || `Model ${modelIndex + 1}`;
7070
const modelPrefix = simulationResults.results.length > 1 ? `${modelId}: ` : "";
7171

72-
const hasConcentrations = Object.keys(result.concentrations).length > 0;
73-
if (hasConcentrations) {
74-
panelTabs.push(`${modelPrefix}Output concentrations`);
72+
for (const phase of result.phases) {
73+
const hasConcentrations = Object.keys(phase.concentrations).length > 0;
74+
if (!hasConcentrations) continue;
7575

76-
// For the first model, compare with input concentrations
77-
// For subsequent models, compare with previous model's output
78-
const initialConcentrations =
79-
modelIndex === 0
80-
? simulationResults.input.concentrations
81-
: simulationResults.results[modelIndex - 1].concentrations;
76+
panelTabs.push(`${modelPrefix}${phase.kind} (${(phase.fraction * 100).toFixed(1)}%)`);
77+
78+
const initialConcentrations = simulationResults.input.concentrations;
8279

8380
panelContents.push(
84-
<Tabs.Panel key={`conc-${modelIndex}`}>
85-
<MassBalanceError initial={initialConcentrations} final={result.concentrations} />
81+
<Tabs.Panel key={`phase-${modelIndex}-${phase.kind}`}>
82+
<MassBalanceError initial={initialConcentrations} phases={result.phases} />
8683

8784
<BarChart
8885
aspectRatio={2}
8986
graphData={extractPlotData({
9087
...simulationResults,
91-
results: [result],
88+
results: [{ phases: [phase], panels: [] }],
9289
input: {
9390
...simulationResults.input,
9491
concentrations: initialConcentrations,
@@ -100,7 +97,7 @@ const Results: React.FC<ResultsProps> = ({ simulationResults }) => {
10097

10198
<ResultConcTable
10299
initialConcentrations={initialConcentrations}
103-
finalConcentrations={result.concentrations}
100+
finalConcentrations={phase.concentrations}
104101
/>
105102
</Tabs.Panel>
106103
);

frontend/src/dto/SimulationResults.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,25 @@ const TablePanel = z.object({
2929
export const Panel = z.discriminatedUnion("type", [TextPanel, JsonPanel, ReactionPathsPanel, TablePanel]);
3030
export type Panel = z.infer<typeof Panel>;
3131

32+
export const Phase = z.object({
33+
kind: z.enum(["aqueous", "co2-rich"]),
34+
fraction: z.number(),
35+
concentrations: z.record(z.string(), z.number()),
36+
});
37+
export type Phase = z.infer<typeof Phase>;
38+
39+
export const getCo2RichPhase = (phases: Phase[] = []): Phase | undefined =>
40+
phases.find((phase) => phase.kind === "co2-rich");
41+
42+
export const getCo2RichConcentrations = (phases: Phase[] = []): Record<string, number> =>
43+
getCo2RichPhase(phases)?.concentrations ?? {};
44+
3245
export const SimulationResults = z.object({
3346
status: z.enum(["done", "pending"]),
3447
input: ModelInput,
3548
results: z.array(
3649
z.object({
37-
concentrations: z.record(z.string(), z.number()),
50+
phases: z.array(Phase),
3851
panels: z.array(Panel),
3952
})
4053
),

frontend/src/functions/Formatting.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SimulationResults } from "@/dto/SimulationResults";
1+
import { SimulationResults, getCo2RichConcentrations } from "@/dto/SimulationResults";
22
import { ChartDataSet, TabulatedResultRow } from "@/dto/ChartData";
33
import { ExperimentResult } from "@/dto/ExperimentResult";
44

@@ -15,7 +15,7 @@ export const convertToSubscripts = (chemicalFormula: string): React.ReactNode =>
1515

1616
export const extractPlotData = (simulationResults: SimulationResults) => {
1717
const inputConcentrations = simulationResults.input.concentrations;
18-
const finalConcentrations = simulationResults.results[0].concentrations;
18+
const finalConcentrations = getCo2RichConcentrations(simulationResults.results[0]?.phases);
1919
const keys = Object.keys(finalConcentrations).filter(
2020
(key) => (inputConcentrations[key] ?? 0) >= 0.001 || (finalConcentrations[key] ?? 0) >= 0.001
2121
);
@@ -47,9 +47,10 @@ export const extractPlotData = (simulationResults: SimulationResults) => {
4747
};
4848

4949
export const convertSimulationToChartData = (simulation: SimulationResults, experimentName: string): ChartDataSet => {
50+
const concentrations = getCo2RichConcentrations(simulation.results[0]?.phases);
5051
return {
5152
label: `${simulation.input.models[0].modelId} - ${experimentName}`,
52-
data: Object.entries(simulation.results[0].concentrations)
53+
data: Object.entries(concentrations)
5354
.filter(([, y]) => y !== 0)
5455
.map(([x, y]) => ({ x, y })),
5556
};
@@ -91,7 +92,7 @@ export const convertSimulationQueriesResultToTabulatedData = (
9192
buildTabulatedRow(
9293
`${simulation.input.models[0].modelId || "Unknown"} - ${experimentName}`,
9394
simulation.input.concentrations,
94-
simulation.results[0].concentrations,
95+
getCo2RichConcentrations(simulation.results[0]?.phases),
9596
{ ...simulation.input.conditions, ...simulation.input.models[0].parameters }
9697
)
9798
);

frontend/src/pages/Compare.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useQueries } from "@tanstack/react-query";
44
import { getResultForSimulation, ResultIsPending } from "@/api/api";
55
import { MainContainer } from "@/components/styles";
66
import { Typography, Table, CircularProgress, Banner } from "@equinor/eds-core-react";
7-
import { SimulationResults } from "@/dto/SimulationResults";
7+
import { SimulationResults, getCo2RichConcentrations } from "@/dto/SimulationResults";
88
import BarChart from "@/components/BarChart";
99
import { ChartDataSet } from "@/dto/ChartData";
1010

@@ -116,17 +116,14 @@ const Compare: React.FC = () => {
116116
const simulationResults = queries.map((q) => q.data as SimulationResults);
117117

118118
const comparisons: SimulationComparison[] = simulationResults.map((result, index) => {
119-
// Find the last result that has concentrations
120-
const finalOutput = [...result.results]
121-
.reverse()
122-
.find((r) => r.concentrations && Object.keys(r.concentrations).length > 0);
119+
const finalResult = [...result.results].reverse().find((r) => r.phases.length > 0);
123120
const firstModel = result.input.models[0];
124121

125122
return {
126123
id: simulationIds[index],
127124
modelName: firstModel?.modelId || "Unknown",
128125
inputConcentrations: result.input.concentrations || {},
129-
outputConcentrations: finalOutput?.concentrations || {},
126+
outputConcentrations: getCo2RichConcentrations(finalResult?.phases),
130127
};
131128
});
132129

frontend/tests/components/LabResultPlot.test.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,21 @@ describe("LabResultsPlot Component", () => {
3737
"Experiment Gondor": [
3838
{
3939
input: { concentrations: { CO2: 0.5, H2O: 0.3 }, models: [{ parameters: {}, modelId: "Narnia" }] },
40-
results: [{ concentrations: { H2CO3: 0.3, N2: 0.9 }, panels: [] }],
40+
results: [
41+
{
42+
phases: [{ kind: "co2-rich", fraction: 1.0, concentrations: { H2CO3: 0.3, N2: 0.9 } }],
43+
panels: [],
44+
},
45+
],
4146
},
4247
{
4348
input: { concentrations: { CO2: 0.5, H2O: 0.3 }, models: [{ parameters: {}, modelId: "Mordor" }] },
44-
results: [{ concentrations: { CO: 0.3, NO2: 0.9 }, panels: [] }],
49+
results: [
50+
{
51+
phases: [{ kind: "co2-rich", fraction: 1.0, concentrations: { CO: 0.3, NO2: 0.9 } }],
52+
panels: [],
53+
},
54+
],
4555
},
4656
],
4757
};

0 commit comments

Comments
 (0)