Skip to content

Commit e84083e

Browse files
Incorporate phases in frontend
1 parent b7b9a2c commit e84083e

9 files changed

Lines changed: 61 additions & 35 deletions

File tree

backend/src/acidwatch_api/models/datamodel.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,14 @@ 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 [Phase(kind="co2-rich", fraction=1.0, concentrations=self.concentrations)]
46+
4347

4448
class ModelResult(_BaseModel):
4549
phases: list[Phase]

backend/src/acidwatch_api/routes/models.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,9 @@ def get_result_for_simulation(
217217
)
218218

219219
simulation_input = Simulation(
220-
phases=db_simulation.phases,
220+
concentrations=_phases_to_concentrations(
221+
[Phase(**p) for p in db_simulation.phases]
222+
),
221223
conditions=Conditions(**(db_simulation.conditions or {})),
222224
models=model_inputs,
223225
)
@@ -275,7 +277,7 @@ async def run_simulation(
275277
except ValueError as exc:
276278
raise HTTPException(status_code=422, detail=exc.args)
277279

278-
concentrations = _phases_to_concentrations(create_simulation.phases)
280+
concentrations = create_simulation.concentrations
279281
try:
280282
adapters[0].validate_concentrations(concentrations)
281283
except InputError as exc:

backend/tests/test_models_endpoints.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ def test_dummy_model_only_valid_substances_are_present(
126126
):
127127
monkeypatch.setattr(dummy_model, "valid_substances", valid_substances)
128128
simulation = {
129-
"phases": _make_phases(concentrations),
129+
"concentrations": concentrations,
130130
"models": [{"modelId": dummy_model.model_id, "parameters": {}}],
131131
}
132132
response = client.post(
@@ -301,7 +301,7 @@ async def run(self):
301301
response = client.post(
302302
"/simulations/",
303303
json={
304-
"phases": _make_phases({}),
304+
"concentrations": {},
305305
"models": [
306306
{"modelId": dummy_model.model_id, "parameters": input_parameters}
307307
],
@@ -324,7 +324,7 @@ async def run(self):
324324
assert response.json() == {
325325
"status": "done",
326326
"input": {
327-
"phases": _make_phases({}),
327+
"concentrations": {},
328328
"conditions": {"temperature": 25.0, "pressure": 10.0},
329329
"models": [
330330
{
@@ -351,7 +351,7 @@ async def run(self):
351351
def test_running_empty_list_of_models_is_an_error(client):
352352
response = client.post(
353353
"/simulations",
354-
json={"phases": _make_phases({"H2O": 2.0}), "models": []},
354+
json={"concentrations": {"H2O": 2.0}, "models": []},
355355
)
356356
assert response.status_code == 422
357357

@@ -385,7 +385,7 @@ def test_running_empty_list_of_models_is_an_error(client):
385385
)
386386
def test_running_models(client, input_models, result_concentrations):
387387
simulation_input = {
388-
"phases": _make_phases({"H2O": 1}),
388+
"concentrations": {"H2O": 1},
389389
"models": input_models,
390390
}
391391

@@ -434,7 +434,7 @@ def test_running_models(client, input_models, result_concentrations):
434434
)
435435
def test_failing_model(client, input_models, result):
436436
simulation_input = {
437-
"phases": _make_phases({"H2O": 1}),
437+
"concentrations": {"H2O": 1},
438438
"models": input_models,
439439
}
440440

@@ -512,7 +512,7 @@ def test_results_order(client, sql_session, swap):
512512
assert response.json() == {
513513
"status": "done",
514514
"input": {
515-
"phases": _make_phases({}),
515+
"concentrations": {},
516516
"conditions": {"temperature": 25.0, "pressure": 10.0},
517517
"models": [first_model, second_model],
518518
},

frontend/src/components/ParityPlots.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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 = sim.results[0]?.phases.find((p) => p.kind === "co2-rich")?.concentrations?.[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: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from "react";
22
import { Tabs, Typography } from "@equinor/eds-core-react";
33
import { useState } from "react";
4-
import { Panel, SimulationResults } from "@/dto/SimulationResults";
4+
import { Panel, Phase, SimulationResults } from "@/dto/SimulationResults";
55
import ResultConcTable from "@/components/Simulation/ConcResultTable";
66
import Reactions from "../../pages/Reactions";
77
import { MassBalanceError } from "@/components/Simulation/MassBalanceError";
@@ -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: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,19 @@ 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+
3239
export const SimulationResults = z.object({
3340
status: z.enum(["done", "pending"]),
3441
input: ModelInput,
3542
results: z.array(
3643
z.object({
37-
concentrations: z.record(z.string(), z.number()),
44+
phases: z.array(Phase),
3845
panels: z.array(Panel),
3946
})
4047
),

frontend/src/functions/Formatting.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ 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 firstPhase = simulationResults.results[0]?.phases[0];
19+
const finalConcentrations = firstPhase?.concentrations ?? {};
1920
const keys = Object.keys(finalConcentrations).filter(
2021
(key) => (inputConcentrations[key] ?? 0) >= 0.001 || (finalConcentrations[key] ?? 0) >= 0.001
2122
);
@@ -47,9 +48,11 @@ export const extractPlotData = (simulationResults: SimulationResults) => {
4748
};
4849

4950
export const convertSimulationToChartData = (simulation: SimulationResults, experimentName: string): ChartDataSet => {
51+
const firstPhase = simulation.results[0]?.phases[0];
52+
const concentrations = firstPhase?.concentrations ?? {};
5053
return {
5154
label: `${simulation.input.models[0].modelId} - ${experimentName}`,
52-
data: Object.entries(simulation.results[0].concentrations)
55+
data: Object.entries(concentrations)
5356
.filter(([, y]) => y !== 0)
5457
.map(([x, y]) => ({ x, y })),
5558
};
@@ -87,11 +90,12 @@ export const convertSimulationQueriesResultToTabulatedData = (
8790

8891
Object.entries(simulationResultsPerExperiment).forEach(([experimentName, simulations]) => {
8992
simulations.forEach((simulation) => {
93+
const firstPhase = simulation.results[0]?.phases[0];
9094
tabulatedData.push(
9195
buildTabulatedRow(
9296
`${simulation.input.models[0].modelId || "Unknown"} - ${experimentName}`,
9397
simulation.input.concentrations,
94-
simulation.results[0].concentrations,
98+
firstPhase?.concentrations ?? {},
9599
{ ...simulation.input.conditions, ...simulation.input.models[0].parameters }
96100
)
97101
);

frontend/src/pages/Compare.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -116,17 +116,17 @@ 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]
119+
const finalResult = [...result.results]
121120
.reverse()
122-
.find((r) => r.concentrations && Object.keys(r.concentrations).length > 0);
121+
.find((r) => r.phases.length > 0);
122+
const co2RichPhase = finalResult?.phases.find((p) => p.kind === "co2-rich");
123123
const firstModel = result.input.models[0];
124124

125125
return {
126126
id: simulationIds[index],
127127
modelName: firstModel?.modelId || "Unknown",
128128
inputConcentrations: result.input.concentrations || {},
129-
outputConcentrations: finalOutput?.concentrations || {},
129+
outputConcentrations: co2RichPhase?.concentrations || {},
130130
};
131131
});
132132

0 commit comments

Comments
 (0)