Skip to content

Commit 58ae138

Browse files
Handle showing partial results in the grid simulation
1 parent 360c504 commit 58ae138

8 files changed

Lines changed: 78 additions & 29 deletions

File tree

backend/tests/test_grid_simulations_endpoint.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import itertools
2+
13
import pytest
4+
from fastapi import BackgroundTasks
25
from fastapi.testclient import TestClient as _BaseTestClient
36
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
47

@@ -182,6 +185,38 @@ def test_grid_surfaces_per_point_errors_without_failing_whole_request(client):
182185
assert sim["error"] is not None
183186

184187

188+
@pytest.mark.usefixtures("dummy_adapters")
189+
def test_grid_returns_finished_points_while_others_are_pending(client, monkeypatch):
190+
schedule_original = BackgroundTasks.add_task
191+
counter = itertools.count()
192+
193+
def add_first_two_only(self, func, *args, **kwargs):
194+
if next(counter) < 2:
195+
schedule_original(self, func, *args, **kwargs)
196+
197+
monkeypatch.setattr(BackgroundTasks, "add_task", add_first_two_only)
198+
199+
grid_id = _create_grid(
200+
client,
201+
axes=[{"substance": "H2O", "range": {"min": 10, "max": 40, "step": 10}}],
202+
).json()
203+
204+
result = client.get_json(f"/grid-simulations/{grid_id}/result")
205+
206+
assert result["status"] == "pending"
207+
assert [sim["status"] for sim in result["simulations"]] == [
208+
"done",
209+
"done",
210+
"pending",
211+
"pending",
212+
]
213+
214+
finished, still_running = result["simulations"][0], result["simulations"][2]
215+
assert finished["results"][0]["phases"][0]["concentrations"] == {"H2O": 5}
216+
assert still_running["results"] == []
217+
assert still_running["input"]["concentrations"] == {"H2O": 30}
218+
219+
185220
@pytest.mark.usefixtures("dummy_adapters")
186221
def test_grid_rejects_unknown_model(client):
187222
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)