Skip to content

Commit 5233983

Browse files
lars-petter-haugekvashchuka
authored andcommitted
Handle showing partial results in the grid simulation
1 parent 360c504 commit 5233983

8 files changed

Lines changed: 120 additions & 29 deletions

File tree

backend/tests/test_grid_simulations_endpoint.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from fastapi.testclient import TestClient as _BaseTestClient
33
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
44

5+
import acidwatch_api.database as db
56
from acidwatch_api.app import fastapi_app
67
from acidwatch_api.authentication import authenticated_user_claims
78
from acidwatch_api.models import base
@@ -31,6 +32,11 @@ def client(monkeypatch):
3132
yield c
3233

3334

35+
@pytest.fixture
36+
def sql_session(client):
37+
return client.app_state["session"]
38+
39+
3440
class HalvingAdapter(base.BaseAdapter):
3541
model_id = "halving"
3642
display_name = "Halving Model"
@@ -182,6 +188,77 @@ def test_grid_surfaces_per_point_errors_without_failing_whole_request(client):
182188
assert sim["error"] is not None
183189

184190

191+
def test_grid_returns_finished_points_while_others_are_pending(client, sql_session):
192+
simulation_ids = []
193+
with sql_session() as session:
194+
for index, concentration in enumerate((10, 20, 30, 40)):
195+
model_input = db.ModelInput(
196+
previous_model_input_id=None,
197+
model_id="halving",
198+
parameters={},
199+
)
200+
simulation = db.Simulation(
201+
owner_id=None,
202+
phases=[
203+
{
204+
"kind": "co2-rich",
205+
"fraction": 1.0,
206+
"concentrations": {"H2O": concentration},
207+
}
208+
],
209+
conditions={},
210+
model_inputs=[model_input],
211+
)
212+
session.add(simulation)
213+
session.flush()
214+
simulation_ids.append(str(simulation.id))
215+
216+
if index < 2:
217+
session.add(
218+
db.ModelResult(
219+
model_input=model_input,
220+
phases=[
221+
{
222+
"kind": "co2-rich",
223+
"fraction": 1.0,
224+
"concentrations": {"H2O": concentration / 2},
225+
}
226+
],
227+
panels=[],
228+
error=None,
229+
)
230+
)
231+
232+
grid = db.GridSimulation(
233+
owner_id=None,
234+
axes=[
235+
{
236+
"substance": "H2O",
237+
"range": {"min": 10, "max": 40, "step": 10},
238+
}
239+
],
240+
simulation_ids=simulation_ids,
241+
)
242+
session.add(grid)
243+
session.commit()
244+
grid_id = grid.id
245+
246+
result = client.get_json(f"/grid-simulations/{grid_id}/result")
247+
248+
assert result["status"] == "pending"
249+
assert [sim["status"] for sim in result["simulations"]] == [
250+
"done",
251+
"done",
252+
"pending",
253+
"pending",
254+
]
255+
256+
finished, still_running = result["simulations"][0], result["simulations"][2]
257+
assert finished["results"][0]["phases"][0]["concentrations"] == {"H2O": 5}
258+
assert still_running["results"] == []
259+
assert still_running["input"]["concentrations"] == {"H2O": 30}
260+
261+
185262
@pytest.mark.usefixtures("dummy_adapters")
186263
def test_grid_rejects_unknown_model(client):
187264
response = _create_grid(

frontend/src/api/api.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,7 @@ export const startGridSimulation = async (grid: CreateGridSimulation): Promise<s
129129
};
130130

131131
export const getGridSimulationResult = async (gridId: string): Promise<GridSimulationResult> => {
132-
const data = await apiRequest("GET", `/grid-simulations/${gridId}/result`, { responseModel: GridSimulationResult });
133-
134-
if (data.status === "pending") {
135-
throw new ResultIsPending();
136-
}
137-
138-
return data;
132+
return await apiRequest("GET", `/grid-simulations/${gridId}/result`, { responseModel: GridSimulationResult });
139133
};
140134

141135
export const getModels = async (): Promise<ModelConfig[]> => {

frontend/src/components/GridSimulation/CompareGridSimulations.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useMemo, useState } from "react";
2-
import { useQueries } from "@tanstack/react-query";
2+
import { useQueries, type Query } from "@tanstack/react-query";
33
import { CircularProgress, NativeSelect, Typography } from "@equinor/eds-core-react";
4-
import { getGridSimulationResult, ResultIsPending } from "@/api/api";
4+
import { getGridSimulationResult } from "@/api/api";
55
import { MainContainer } from "@/components/styles";
66
import { GridSimulationResult } from "@/dto/GridSimulation";
77
import LineChart, { LineSeries } from "@/components/LineChart";
@@ -110,8 +110,8 @@ const CompareGridSimulations: React.FC<CompareGridSimulationsProps> = ({ gridIds
110110
queries: gridIds.map((id) => ({
111111
queryKey: ["grid-simulation", id],
112112
queryFn: () => getGridSimulationResult(id),
113-
retry: (_count: number, error: Error) => error instanceof ResultIsPending,
114-
retryDelay: () => 2000,
113+
refetchInterval: (query: Query<GridSimulationResult>) =>
114+
query.state.data?.status === "pending" ? 2000 : false,
115115
})),
116116
});
117117

frontend/src/components/GridSimulation/GridResultStep.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ const GridResultStep: React.FC<GridResultStepProps> = ({ result, isLoading, erro
2626
if (result === undefined) {
2727
return <NoResults />;
2828
}
29+
if (result.simulations.every((sim) => sim.status === "pending")) {
30+
return <Working />;
31+
}
2932
return <GridResults result={result} />;
3033
};
3134

frontend/src/components/GridSimulation/GridResults.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,8 @@ const GridPhaseChart: React.FC<GridPhaseChartProps> = ({ result, modelIndex, pha
3333
() => collectOutputSubstances(simulations, modelIndex, phaseKind),
3434
[simulations, modelIndex, phaseKind]
3535
);
36-
const [selectedSubstances, setSelectedSubstances] = useState<string[]>(() =>
37-
defaultSelectedSubstances(simulations, modelIndex, phaseKind)
38-
);
36+
const [selection, setSelection] = useState<string[] | null>(null);
37+
const selectedSubstances = selection ?? defaultSelectedSubstances(simulations, modelIndex, phaseKind);
3938

4039
const xAxisSubstance = axes[0]?.substance ?? "Unknown";
4140
const xValues = simulations.map((sim) => parseFloat(sim.input.concentrations[xAxisSubstance].toFixed(2)) ?? 0);
@@ -51,7 +50,7 @@ const GridPhaseChart: React.FC<GridPhaseChartProps> = ({ result, modelIndex, pha
5150
options={allSubstances}
5251
selectedOptions={selectedSubstances}
5352
multiple
54-
onOptionsChange={({ selectedItems }) => setSelectedSubstances(selectedItems)}
53+
onOptionsChange={({ selectedItems }) => setSelection(selectedItems)}
5554
optionLabel={optionName}
5655
style={{ maxWidth: "400px", marginBottom: "1rem" }}
5756
/>
@@ -146,6 +145,7 @@ const GridResults: React.FC<GridResultsProps> = ({ result }) => {
146145
const sections = buildModelSections(inputModels, models);
147146

148147
const erroredSimulations = simulations.filter((sim) => sim.status === "error");
148+
const pendingSimulations = simulations.filter((sim) => sim.status === "pending");
149149

150150
const xAxisSubstance = axes[0]?.substance ?? "Unknown";
151151
const modelLabel = inputModels.map((model) => model.modelId).join(" → ") || "Unknown model";
@@ -165,6 +165,17 @@ const GridResults: React.FC<GridResultsProps> = ({ result }) => {
165165
<strong>{modelLabel}</strong>.
166166
</Typography>
167167

168+
{pendingSimulations.length > 0 && (
169+
<Banner style={{ marginBottom: "1rem" }}>
170+
<Banner.Icon variant="info"></Banner.Icon>
171+
<Banner.Message>
172+
Showing partial results. {simulations.length - pendingSimulations.length} of{" "}
173+
{simulations.length} runs have finished; the remaining {pendingSimulations.length} appear as
174+
they complete.
175+
</Banner.Message>
176+
</Banner>
177+
)}
178+
168179
{erroredSimulations.length > 0 && (
169180
<Banner style={{ marginBottom: "1rem" }}>
170181
<Banner.Icon variant="warning">⚠️</Banner.Icon>

frontend/src/pages/Models.tsx

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,7 @@ const Models: React.FC = () => {
8888
queryKey: ["grid-simulation", gridId],
8989
queryFn: () => getGridSimulationResult(gridId!),
9090
enabled: gridId !== undefined,
91-
retry: (_count, error) => error instanceof ResultIsPending,
92-
retryDelay: () => 2000,
91+
refetchInterval: (query) => (query.state.data?.status === "pending" ? 2000 : false),
9392
});
9493

9594
useEffect(() => {
@@ -106,10 +105,10 @@ const Models: React.FC = () => {
106105
}, [simulationId, simulationIsLoading]);
107106

108107
useEffect(() => {
109-
if (gridId && !gridIsLoading) {
108+
if (gridId && (gridResult?.status === "done" || gridResultError)) {
110109
simulationHistory.finalizeEntry(gridId);
111110
}
112-
}, [gridId, gridIsLoading]);
111+
}, [gridId, gridResult?.status, gridResultError]);
113112

114113
useEffect(() => {
115114
if (simulationResults && simulationResults.status !== "error" && models.length > 0) {
@@ -133,14 +132,14 @@ const Models: React.FC = () => {
133132
}
134133
}, [simulationResults, models]);
135134

136-
useEffect(() => {
137-
if (gridResult && models.length > 0) {
138-
const firstSim = gridResult.simulations[0];
139-
if (!firstSim) return;
135+
const gridInput = gridResult?.simulations[0]?.input;
136+
const gridAxes = gridResult?.axes;
140137

138+
useEffect(() => {
139+
if (gridInput && gridAxes && models.length > 0) {
141140
const loadedModels: ModelConfig[] = [];
142141

143-
firstSim.input.models.forEach((modelInput) => {
142+
gridInput.models.forEach((modelInput) => {
144143
const model = models.find((m) => m.modelId === modelInput.modelId);
145144
if (model) {
146145
loadedModels.push(model);
@@ -152,16 +151,16 @@ const Models: React.FC = () => {
152151
}
153152
});
154153

155-
useConcentrationsStore.getState().reset(firstSim.input.concentrations);
156-
useConditionsStore.getState().reset(firstSim.input.conditions);
154+
useConcentrationsStore.getState().reset(gridInput.concentrations);
155+
useConditionsStore.getState().reset(gridInput.conditions);
157156

158157
useGridRangeStore.getState().reset({
159-
axes: gridResult.axes,
158+
axes: gridAxes,
160159
});
161160

162161
setSelectedModels(loadedModels);
163162
}
164-
}, [gridResult, models]);
163+
}, [gridInput, gridAxes, models, gridId]);
165164

166165
const isGridMode = gridId !== undefined;
167166

frontend/tests/setup.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ Object.defineProperty(window, "matchMedia", {
1515
})),
1616
});
1717

18+
// jsdom does not implement the Popover API used by EDS overlays
19+
for (const method of ["showPopover", "hidePopover", "togglePopover"] as const) {
20+
if (!(method in HTMLElement.prototype)) {
21+
HTMLElement.prototype[method] = vi.fn();
22+
}
23+
}
24+
1825
// Ensure that the DOM is clean between every test
1926
afterEach(() => {
2027
cleanup();

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)