Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Button } from "@equinor/eds-core-react";

ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend, zoomPlugin);

interface BarChartProps {
export interface BarChartProps {
graphData: ChartDataSet[];
aspectRatio?: number;
xLabel?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface LineSeries {
color?: string;
}

interface LineChartProps {
export interface LineChartProps {
xValues: number[];
series: LineSeries[];
xAxisLabel?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ export interface ScatterDataSet {
color?: string;
}

interface ScatterPlotProps {
export interface ScatterChartProps {
datasets: ScatterDataSet[];
xLabel?: string;
yLabel?: string;
showDiagonal?: boolean;
}

const ScatterPlot: React.FC<ScatterPlotProps> = ({ datasets, xLabel, yLabel, showDiagonal }) => {
const ScatterChart: React.FC<ScatterChartProps> = ({ datasets, xLabel, yLabel, showDiagonal }) => {
if (datasets.length === 0) return null;

const maxVal = Math.max(...datasets.flatMap((ds) => ds.data.flatMap((p) => [Math.abs(p.x), Math.abs(p.y)])), 0);
Expand Down Expand Up @@ -101,4 +101,4 @@ const ScatterPlot: React.FC<ScatterPlotProps> = ({ datasets, xLabel, yLabel, sho
);
};

export default ScatterPlot;
export default ScatterChart;
Original file line number Diff line number Diff line change
@@ -1,36 +1,37 @@
import React, { useMemo, useState } from "react";
import { useMemo, useState } from "react";
import { useQueries, type Query } from "@tanstack/react-query";
import { CircularProgress, NativeSelect, Typography } from "@equinor/eds-core-react";
import { NativeSelect, Typography } from "@equinor/eds-core-react";
import { getGridSimulationResult } from "@/api/api";
import { MainContainer } from "@/components/styles";
import { GridSimulationResult } from "@/dto/GridSimulation";
import LineChart, { LineSeries } from "@/components/LineChart";
import { getGridAxis, getGridInput, GridSimulationResult } from "@/dto/GridSimulation";
import { Phase } from "@/dto/SimulationResults";
import LineChart, { LineSeries } from "@/components/Charts/LineChart";
import {
collectOutputSubstances,
pointOutput,
significantSubstances,
visiblePhaseKinds,
} from "@/functions/GridSimulation";
} from "@/components/GridSimulation/gridSimulationUtils";
import { optionName } from "@/functions/Substance";
import { useAvailableModels } from "@/contexts/ModelContext";
import { buildModelSections, phaseLabel } from "@/utils/modelUtils";
import ModelAccordionLayout, { AccordionItem } from "@/components/ModelAccordionLayout";
import ConcentrationTable, { SimulationConcentrations } from "@/components/ConcentrationTable";
import ConcentrationTable, { SimulationConcentrations } from "@/components/Tables/ConcentrationTable";
import ComparisonPage from "@/components/Comparison/ComparisonPage";

interface CompareGridSimulationsProps {
gridIds: string[];
}

const modelChainLabel = (result: GridSimulationResult): string => {
const firstSim = result.simulations[0];
if (!firstSim) return "Unknown model";
return firstSim.input.models.map((m) => m.modelId).join(" → ") || "Unknown model";
const input = getGridInput(result);
if (!input) return "Unknown model";
return input.models.map((m) => m.modelId).join(" → ") || "Unknown model";
};

interface CompareSectionProps {
results: GridSimulationResult[];
modelIndex: number;
phaseKind: string;
phaseKind: Phase["kind"];
}

const CompareSection: React.FC<CompareSectionProps> = ({ results, modelIndex, phaseKind }) => {
Expand All @@ -50,23 +51,24 @@ const CompareSection: React.FC<CompareSectionProps> = ({ results, modelIndex, ph
const [substance, setSubstance] = useState<string>("");
const selectedSubstance = substance || allSubstances[0] || "";

const xAxisSubstance = results[0]?.axes[0]?.substance ?? "";
const referenceResult = results.at(0);
const xAxisSubstance = referenceResult ? (getGridAxis(referenceResult)?.substance ?? "") : "";
const unifiedXValues = Array.from(
new Set(results.flatMap((r) => r.simulations.map((sim) => sim.input.concentrations[xAxisSubstance] ?? 0)))
).sort((a, b) => a - b);

const series: LineSeries[] = results.map((r) => {
const valueToSim = new Map(r.simulations.map((sim) => [sim.input.concentrations[xAxisSubstance] ?? 0, sim]));
return {
label: `${modelChainLabel(r)} · ${r.axes[0]?.substance ?? ""}`,
label: `${modelChainLabel(r)} · ${getGridAxis(r)?.substance ?? ""}`,
data: unifiedXValues.map((x) => {
const sim = valueToSim.get(x);
return sim ? pointOutput(sim, selectedSubstance, modelIndex, phaseKind) : null;
}),
};
});

const axisSubstances = new Set(results.map((r) => r.axes[0]?.substance).filter(Boolean));
const axisSubstances = new Set(results.map((r) => getGridAxis(r)?.substance).filter(Boolean));
const xAxisLabel =
axisSubstances.size === 1 ? `${[...axisSubstances][0]} (ppm·mol)` : "Varied concentration (ppm·mol)";
const unit = phaseKind === "aqueous" ? "wt%" : "ppm·mol";
Expand Down Expand Up @@ -118,52 +120,19 @@ const CompareGridSimulations: React.FC<CompareGridSimulationsProps> = ({ gridIds
const isLoading = queries.some((q) => q.isLoading);
const hasError = queries.some((q) => q.isError);
const results = queries.map((q) => q.data).filter((data): data is GridSimulationResult => data !== undefined);
const referenceResult = results.at(0);

const header = (
<Typography variant="h2" style={{ marginBottom: "2rem" }}>
Compare Grid Simulations
</Typography>
);

if (gridIds.length === 0) {
return (
<MainContainer>
{header}
<Typography variant="body_short">No grid simulations selected for comparison.</Typography>
</MainContainer>
);
}
if (isLoading) {
return (
<MainContainer>
{header}
<CircularProgress />
</MainContainer>
);
}
if (hasError) {
return (
<MainContainer>
{header}
<Typography variant="body_short" style={{ color: "red" }}>
Error loading grid simulation results
</Typography>
</MainContainer>
);
}

const firstSim = results[0]?.simulations[0];
const inputModels = firstSim?.input.models ?? [];
const inputModels = referenceResult ? (getGridInput(referenceResult)?.models ?? []) : [];
const sections = buildModelSections(inputModels, models);

const allPhasesByModel = new Map<number, string[]>();
const allPhasesByModel = new Map<number, Phase["kind"][]>();
sections.forEach((section) => {
section.indices.forEach((modelIndex) => {
const phases = new Set<string>();
const phases = new Set<Phase["kind"]>();
results.forEach((r) => {
visiblePhaseKinds(r.simulations, modelIndex).forEach((k) => phases.add(k));
});
const order = ["co2-rich", "aqueous"];
const order: Phase["kind"][] = ["co2-rich", "aqueous"];
allPhasesByModel.set(
modelIndex,
order.filter((k) => phases.has(k))
Expand Down Expand Up @@ -201,7 +170,7 @@ const CompareGridSimulations: React.FC<CompareGridSimulationsProps> = ({ gridIds
return {
id: gridIds[index],
modelName: modelChainLabel(result),
concentrations: result.simulations[0]?.input.concentrations ?? {},
concentrations: getGridInput(result)?.concentrations ?? {},
ranges,
};
});
Expand All @@ -211,9 +180,14 @@ const CompareGridSimulations: React.FC<CompareGridSimulationsProps> = ({ gridIds
).sort();

return (
<MainContainer>
{header}

<ComparisonPage
title="Compare Grid Simulations"
isEmpty={gridIds.length === 0}
emptyMessage="No grid simulations selected for comparison."
isLoading={isLoading}
hasError={hasError}
errorMessage="Error loading grid simulation results"
>
<Typography variant="h4" style={{ margin: "1rem 0" }}>
Input Concentrations
</Typography>
Expand All @@ -223,7 +197,7 @@ const CompareGridSimulations: React.FC<CompareGridSimulationsProps> = ({ gridIds
</div>

<ModelAccordionLayout items={items} />
</MainContainer>
</ComparisonPage>
);
};

Expand Down
120 changes: 120 additions & 0 deletions frontend/src/components/Comparison/CompareSimulations.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import React from "react";
import { useQueries } from "@tanstack/react-query";
import { Banner, Typography } from "@equinor/eds-core-react";
import { getResultForSimulation, ResultIsPending } from "@/api/api";
import BarChart from "@/components/Charts/BarChart";
import ConcentrationTable from "@/components/Tables/ConcentrationTable";
import { ChartDataSet } from "@/dto/ChartData";
import { getOutputConcentrations, getPrimaryModel, SimulationResults } from "@/dto/SimulationResults";
import ComparisonPage from "@/components/Comparison/ComparisonPage";

type SimulationComparison = {
id: string;
modelName: string;
inputConcentrations: Record<string, number>;
outputConcentrations: Record<string, number>;
};

interface CompareSimulationsProps {
simulationIds: string[];
}

const CompareSimulations: React.FC<CompareSimulationsProps> = ({ simulationIds }) => {
const queries = useQueries({
queries: simulationIds.map((id) => ({
queryKey: ["simulation", id],
queryFn: () => getResultForSimulation(id),
retry: (_count: number, error: Error) => error instanceof ResultIsPending,
retryDelay: () => 2000,
})),
});

const isLoading = queries.some((query) => query.isLoading);
const hasError = queries.some((query) => query.isError);

const simulationResults = queries
.map((query) => query.data)
.filter((result): result is SimulationResults => result !== undefined);
const comparisons: SimulationComparison[] = simulationResults.map((result, index) => ({
id: simulationIds[index],
modelName: getPrimaryModel(result)?.modelId ?? "Unknown",
inputConcentrations: result.input.concentrations,
outputConcentrations: getOutputConcentrations(result),
}));

const firstInput = comparisons[0] ? JSON.stringify(Object.entries(comparisons[0].inputConcentrations).sort()) : "";
const allInputsMatch =
comparisons.length < 2 ||
comparisons.every(
(comparison) => JSON.stringify(Object.entries(comparison.inputConcentrations).sort()) === firstInput
);

const collectUniqueSubstances = (
field: keyof Pick<SimulationComparison, "inputConcentrations" | "outputConcentrations">
) => Array.from(new Set(comparisons.flatMap((comparison) => Object.keys(comparison[field])))).sort();

const inputSubstances = collectUniqueSubstances("inputConcentrations");
const significantOutputs = collectUniqueSubstances("outputConcentrations").filter((substance) =>
comparisons.some((comparison) => (comparison.outputConcentrations[substance] ?? 0) >= 0.01)
);
const chartData: ChartDataSet[] = comparisons.map((comparison) => ({
label: `${comparison.modelName} (${comparison.id.slice(0, 8)})`,
data: significantOutputs.map((substance) => ({
x: substance,
y: comparison.outputConcentrations[substance] ?? 0,
})),
}));

return (
<ComparisonPage
title="Compare Simulations"
isEmpty={simulationIds.length === 0}
emptyMessage="No simulations selected for comparison."
isLoading={isLoading}
hasError={hasError}
errorMessage="Error loading simulation results"
>
{!allInputsMatch && (
<Banner style={{ marginBottom: "2rem" }}>
<Banner.Icon variant="warning">⚠️</Banner.Icon>
<Banner.Message>
Warning: The selected simulations have different input concentrations. You may be comparing
incompatible scenarios.
</Banner.Message>
</Banner>
)}

<Typography variant="h4" style={{ marginBottom: "1rem" }}>
Input Concentrations
</Typography>
<ConcentrationTable
substances={inputSubstances}
simulations={comparisons.map((comparison) => ({
id: comparison.id,
modelName: comparison.modelName,
concentrations: comparison.inputConcentrations,
}))}
highlightDifferences
/>

<Typography variant="h4" style={{ marginBottom: "1rem", marginTop: "3rem" }}>
Output Concentrations
</Typography>
<BarChart graphData={chartData} aspectRatio={3} xLabel="Components" yLabel="Concentration (ppm·mol)" />

<Typography variant="h5" style={{ marginBottom: "1rem" }}>
Concentration Values (≥ 0.01)
</Typography>
<ConcentrationTable
substances={significantOutputs}
simulations={comparisons.map((comparison) => ({
id: comparison.id,
modelName: comparison.modelName,
concentrations: comparison.outputConcentrations,
}))}
/>
</ComparisonPage>
);
};

export default CompareSimulations;
47 changes: 47 additions & 0 deletions frontend/src/components/Comparison/ComparisonPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from "react";
import { CircularProgress, Typography } from "@equinor/eds-core-react";
import { MainContainer } from "@/components/styles";

interface ComparisonPageProps {
title: string;
isEmpty: boolean;
emptyMessage: string;
isLoading: boolean;
hasError: boolean;
errorMessage: string;
children: React.ReactNode;
}

const ComparisonPage: React.FC<ComparisonPageProps> = ({
title,
isEmpty,
emptyMessage,
isLoading,
hasError,
errorMessage,
children,
}) => {
let content = children;
if (isEmpty) {
content = <Typography variant="body_short">{emptyMessage}</Typography>;
} else if (isLoading) {
content = <CircularProgress />;
} else if (hasError) {
content = (
<Typography variant="body_short" style={{ color: "red" }}>
{errorMessage}
</Typography>
);
}

return (
<MainContainer>
<Typography variant="h2" style={{ marginBottom: "2rem" }}>
{title}
</Typography>
{content}
</MainContainer>
);
};

export default ComparisonPage;
Loading
Loading