Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions backend/tests/test_grid_simulations_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from fastapi.testclient import TestClient as _BaseTestClient
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY

import acidwatch_api.database as db
from acidwatch_api.app import fastapi_app
from acidwatch_api.authentication import authenticated_user_claims
from acidwatch_api.models import base
Expand Down Expand Up @@ -31,6 +32,11 @@ def client(monkeypatch):
yield c


@pytest.fixture
def sql_session(client):
return client.app_state["session"]


class HalvingAdapter(base.BaseAdapter):
model_id = "halving"
display_name = "Halving Model"
Expand Down Expand Up @@ -182,6 +188,77 @@ def test_grid_surfaces_per_point_errors_without_failing_whole_request(client):
assert sim["error"] is not None


def test_grid_returns_finished_points_while_others_are_pending(client, sql_session):
simulation_ids = []
with sql_session() as session:
for index, concentration in enumerate((10, 20, 30, 40)):
model_input = db.ModelInput(
previous_model_input_id=None,
model_id="halving",
parameters={},
)
simulation = db.Simulation(
owner_id=None,
phases=[
{
"kind": "co2-rich",
"fraction": 1.0,
"concentrations": {"H2O": concentration},
}
],
conditions={},
model_inputs=[model_input],
)
session.add(simulation)
session.flush()
simulation_ids.append(str(simulation.id))

if index < 2:
session.add(
db.ModelResult(
model_input=model_input,
phases=[
{
"kind": "co2-rich",
"fraction": 1.0,
"concentrations": {"H2O": concentration / 2},
}
],
panels=[],
error=None,
)
)

grid = db.GridSimulation(
owner_id=None,
axes=[
{
"substance": "H2O",
"range": {"min": 10, "max": 40, "step": 10},
}
],
simulation_ids=simulation_ids,
)
session.add(grid)
session.commit()
grid_id = grid.id

result = client.get_json(f"/grid-simulations/{grid_id}/result")

assert result["status"] == "pending"
assert [sim["status"] for sim in result["simulations"]] == [
"done",
"done",
"pending",
"pending",
]

finished, still_running = result["simulations"][0], result["simulations"][2]
assert finished["results"][0]["phases"][0]["concentrations"] == {"H2O": 5}
assert still_running["results"] == []
assert still_running["input"]["concentrations"] == {"H2O": 30}


@pytest.mark.usefixtures("dummy_adapters")
def test_grid_rejects_unknown_model(client):
response = _create_grid(
Expand Down
8 changes: 1 addition & 7 deletions frontend/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,7 @@ export const startGridSimulation = async (grid: CreateGridSimulation): Promise<s
};

export const getGridSimulationResult = async (gridId: string): Promise<GridSimulationResult> => {
const data = await apiRequest("GET", `/grid-simulations/${gridId}/result`, { responseModel: GridSimulationResult });

if (data.status === "pending") {
throw new ResultIsPending();
}

return data;
return await apiRequest("GET", `/grid-simulations/${gridId}/result`, { responseModel: GridSimulationResult });
};

export const getModels = async (): Promise<ModelConfig[]> => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { useMemo, useState } from "react";
import { useQueries } from "@tanstack/react-query";
import { useQueries, type Query } from "@tanstack/react-query";
import { CircularProgress, NativeSelect, Typography } from "@equinor/eds-core-react";
import { getGridSimulationResult, ResultIsPending } from "@/api/api";
import { getGridSimulationResult } from "@/api/api";
import { MainContainer } from "@/components/styles";
import { GridSimulationResult } from "@/dto/GridSimulation";
import LineChart, { LineSeries } from "@/components/LineChart";
Expand Down Expand Up @@ -110,8 +110,8 @@ const CompareGridSimulations: React.FC<CompareGridSimulationsProps> = ({ gridIds
queries: gridIds.map((id) => ({
queryKey: ["grid-simulation", id],
queryFn: () => getGridSimulationResult(id),
retry: (_count: number, error: Error) => error instanceof ResultIsPending,
retryDelay: () => 2000,
refetchInterval: (query: Query<GridSimulationResult>) =>
query.state.data?.status === "pending" ? 2000 : false,
})),
});

Expand Down
3 changes: 3 additions & 0 deletions frontend/src/components/GridSimulation/GridResultStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ const GridResultStep: React.FC<GridResultStepProps> = ({ result, isLoading, erro
if (result === undefined) {
return <NoResults />;
}
if (result.simulations.every((sim) => sim.status === "pending")) {
return <Working />;
}
return <GridResults result={result} />;
};

Expand Down
19 changes: 15 additions & 4 deletions frontend/src/components/GridSimulation/GridResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,8 @@ const GridPhaseChart: React.FC<GridPhaseChartProps> = ({ result, modelIndex, pha
() => collectOutputSubstances(simulations, modelIndex, phaseKind),
[simulations, modelIndex, phaseKind]
);
const [selectedSubstances, setSelectedSubstances] = useState<string[]>(() =>
defaultSelectedSubstances(simulations, modelIndex, phaseKind)
);
const [selection, setSelection] = useState<string[] | null>(null);
const selectedSubstances = selection ?? defaultSelectedSubstances(simulations, modelIndex, phaseKind);

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

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

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

{pendingSimulations.length > 0 && (
<Banner style={{ marginBottom: "1rem" }}>
<Banner.Icon variant="info">⏳</Banner.Icon>
<Banner.Message>
Showing partial results. {simulations.length - pendingSimulations.length} of{" "}
{simulations.length} runs have finished; the remaining {pendingSimulations.length} appear as
they complete.
</Banner.Message>
</Banner>
)}

{erroredSimulations.length > 0 && (
<Banner style={{ marginBottom: "1rem" }}>
<Banner.Icon variant="warning">⚠️</Banner.Icon>
Expand Down
25 changes: 12 additions & 13 deletions frontend/src/pages/Models.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,7 @@ const Models: React.FC = () => {
queryKey: ["grid-simulation", gridId],
queryFn: () => getGridSimulationResult(gridId!),
enabled: gridId !== undefined,
retry: (_count, error) => error instanceof ResultIsPending,
retryDelay: () => 2000,
refetchInterval: (query) => (query.state.data?.status === "pending" ? 2000 : false),
});

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

useEffect(() => {
if (gridId && !gridIsLoading) {
if (gridId && (gridResult?.status === "done" || gridResultError)) {
simulationHistory.finalizeEntry(gridId);
}
}, [gridId, gridIsLoading]);
}, [gridId, gridResult?.status, gridResultError]);

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

useEffect(() => {
if (gridResult && models.length > 0) {
const firstSim = gridResult.simulations[0];
if (!firstSim) return;
const gridInput = gridResult?.simulations[0]?.input;
const gridAxes = gridResult?.axes;

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

firstSim.input.models.forEach((modelInput) => {
gridInput.models.forEach((modelInput) => {
const model = models.find((m) => m.modelId === modelInput.modelId);
if (model) {
loadedModels.push(model);
Expand All @@ -152,16 +151,16 @@ const Models: React.FC = () => {
}
});

useConcentrationsStore.getState().reset(firstSim.input.concentrations);
useConditionsStore.getState().reset(firstSim.input.conditions);
useConcentrationsStore.getState().reset(gridInput.concentrations);
useConditionsStore.getState().reset(gridInput.conditions);

useGridRangeStore.getState().reset({
axes: gridResult.axes,
axes: gridAxes,
});

setSelectedModels(loadedModels);
}
}, [gridResult, models]);
}, [gridInput, gridAxes, models, gridId]);

const isGridMode = gridId !== undefined;

Expand Down
7 changes: 7 additions & 0 deletions frontend/tests/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ Object.defineProperty(window, "matchMedia", {
})),
});

// jsdom does not implement the Popover API used by EDS overlays
for (const method of ["showPopover", "hidePopover", "togglePopover"] as const) {
if (!(method in HTMLElement.prototype)) {
HTMLElement.prototype[method] = vi.fn();
}
}

// Ensure that the DOM is clean between every test
afterEach(() => {
cleanup();
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading