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: 62 additions & 15 deletions frontend/src/components/ModelSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,64 @@ import { help } from "@equinor/eds-icons";
const makeLabel = (modelConfig: ModelConfig) =>
modelConfig.accessError ? `{modelConfig.displayName} (Access Error)` : modelConfig.displayName;

const ModelButton: React.FC<{
const handleToggle = (
isActive: boolean,
setModel: (modelConfig: ModelConfig | undefined) => void,
modelConfig: ModelConfig
) => {
if (isActive) {
// If currently active, unselect it
setModel(undefined);
} else {
// If not active, select it
setModel(modelConfig);
}
};

const PrimaryModelButton: React.FC<{
modelConfig: ModelConfig;
active: boolean;
setCurrentPrimaryModel: (modelConfig: ModelConfig | undefined) => void;
}> = ({ modelConfig, active, setCurrentPrimaryModel }) => {
const [open, setOpen] = useState<boolean>(false);

return (
<Button.Group>
<Button
variant={active ? "outlined" : "contained"}
onClick={() => {
handleToggle(active, setCurrentPrimaryModel, modelConfig);
}}
disabled={!!modelConfig.accessError}
>
{makeLabel(modelConfig)}
</Button>
<Button variant={"contained"} onClick={() => setOpen(true)}>
<Icon data={help} />
</Button>
<Dialog open={open} onClose={() => setOpen(false)} isDismissable={true} style={{ width: "100%" }}>
<Dialog.Header>
<Dialog.Title>Model {modelConfig.displayName}</Dialog.Title>
</Dialog.Header>
<Dialog.Content style={{ whiteSpace: "pre-wrap" }}>{modelConfig.description}</Dialog.Content>
</Dialog>
</Button.Group>
);
};

const SecondaryModelButton: React.FC<{
modelConfig: ModelConfig;
active?: boolean;
setCurrentModel: (modelConfig: ModelConfig) => void;
}> = ({ modelConfig, active, setCurrentModel }) => {
active: boolean;
setCurrentSecondaryModel: (modelConfig: ModelConfig | undefined) => void;
}> = ({ modelConfig, active, setCurrentSecondaryModel }) => {
const [open, setOpen] = useState<boolean>(false);

return (
<Button.Group>
<Button
variant={active ? "outlined" : "contained"}
onClick={() => {
setCurrentModel(modelConfig);
handleToggle(active, setCurrentSecondaryModel, modelConfig);
}}
disabled={!!modelConfig.accessError}
>
Expand All @@ -38,10 +83,12 @@ const ModelButton: React.FC<{
);
};

const ModelSelect: React.FC<{ currentModel?: ModelConfig; setCurrentModel: (model: ModelConfig) => void }> = ({
currentModel,
setCurrentModel,
}) => {
const ModelSelect: React.FC<{
currentPrimaryModel?: ModelConfig;
setCurrentPrimaryModel: (model: ModelConfig | undefined) => void;
currentSecondaryModel?: ModelConfig;
setCurrentSecondaryModel: (model: ModelConfig | undefined) => void;
}> = ({ currentPrimaryModel, currentSecondaryModel, setCurrentPrimaryModel, setCurrentSecondaryModel }) => {
const { models, error, isLoading } = useAvailableModels();

if (isLoading) {
Expand Down Expand Up @@ -71,11 +118,11 @@ const ModelSelect: React.FC<{ currentModel?: ModelConfig; setCurrentModel: (mode
{models
.filter((model) => model.category === "Primary")
.map((model, index) => (
<ModelButton
<PrimaryModelButton
modelConfig={model}
key={index}
active={model.modelId === currentModel?.modelId}
setCurrentModel={setCurrentModel}
active={model.modelId === currentPrimaryModel?.modelId}
setCurrentPrimaryModel={setCurrentPrimaryModel}
/>
))}
</div>
Expand All @@ -86,11 +133,11 @@ const ModelSelect: React.FC<{ currentModel?: ModelConfig; setCurrentModel: (mode
{models
.filter((model) => model.category === "Secondary")
.map((model, index) => (
<ModelButton
<SecondaryModelButton
modelConfig={model}
key={index}
active={model.modelId === currentModel?.modelId}
setCurrentModel={setCurrentModel}
active={model.modelId === currentSecondaryModel?.modelId}
setCurrentSecondaryModel={setCurrentSecondaryModel}
/>
))}
</div>
Expand Down
41 changes: 41 additions & 0 deletions frontend/src/functions/Filtering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,44 @@ export const filterValidModels = (experiment: ExperimentResult, models: ModelCon
.filter((model) => model.category === "Primary")
.filter((model) => Object.entries(filteredConcs).every(([key]) => model.validSubstances.includes(key)));
};

export function getValidParametersForSecondaryModel(
parameters: Record<string, any> | undefined,
validParams: string[] | undefined
) {
if (!parameters || !validParams) return {};
return Object.fromEntries(Object.entries(parameters).filter(([key]) => validParams.includes(key)));
}

export function filterInValidAndUndefinedSubstances(
concentrations: Record<string, number | undefined>,
validSubstances?: string[]
): Record<string, number> {
return Object.entries(concentrations)
.filter(
([substance, concentration]) =>
!!validSubstances?.includes(substance) && concentration !== undefined && Number(concentration) > 0
)
.reduce(
(acc, [substance, concentration]) => ({
...acc,
[substance]: Number(concentration),
}),
{} as Record<string, number>
);
}
/* Specific for Solubility CCS model - added after consultation with domain experts*/
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move model specific stuff to the backend?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes next pr

export function filterAcidWithLowConcentration(concentrations: Record<string, number>): Record<string, number> {
const acids = ["HNO3", "H2SO4"];
const presentAcids = acids.filter((acid) => concentrations[acid] !== undefined);
const updatedConcentrations = { ...concentrations };
if (presentAcids.length === 2) {
const [acid1, acid2] = presentAcids;
if (concentrations[acid1] >= concentrations[acid2]) {
delete updatedConcentrations[acid2];
} else {
delete updatedConcentrations[acid1];
}
}
return updatedConcentrations;
}
78 changes: 78 additions & 0 deletions frontend/src/hooks/useSecondaryModelQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useQuery } from "@tanstack/react-query";
import { startSimulation, getResultForSimulation, ResultIsPending } from "@/api/api";
import { ModelConfig } from "@/dto/FormConfig";
import { SimulationResults } from "@/dto/SimulationResults";
import {
filterAcidWithLowConcentration,
filterInValidAndUndefinedSubstances,
getValidParametersForSecondaryModel,
} from "@/functions/Filtering";

interface UseSecondaryModelQueryOptions {
primaryResults?: SimulationResults;
secondaryModel?: ModelConfig;
enabled?: boolean;
}

export const useSecondaryModelQuery = ({
primaryResults,
secondaryModel,
enabled = true,
}: UseSecondaryModelQueryOptions) => {
const {
data: secondarySimulationId,
isLoading: isStartingSecondary,
error: startError,
} = useQuery({
queryKey: ["start-secondary-simulation", secondaryModel?.modelId, primaryResults?.modelInput.modelId],
queryFn: async () => {
let validConcentrations = filterInValidAndUndefinedSubstances(
primaryResults?.finalConcentrations ?? {},
secondaryModel?.validSubstances
);

const validParameters = getValidParametersForSecondaryModel(
primaryResults?.modelInput.parameters,
Object.keys(secondaryModel?.parameters || {})
);
validConcentrations = filterAcidWithLowConcentration(validConcentrations);

if (Object.keys(validConcentrations).length === 0) {
throw new Error(
`No compatible concentrations found for ${secondaryModel?.displayName ?? "unknown model"}`
);
}

return await startSimulation({
modelId: secondaryModel!.modelId,
concentrations: validConcentrations,
parameters: validParameters ?? {},
});
},
enabled:
enabled && !!primaryResults?.finalConcentrations && !!secondaryModel && primaryResults.status === "done",
retry: 1000,
staleTime: 0,
});

const {
data: secondaryResults,
isLoading: isLoadingSecondaryResults,
error: resultsError,
} = useQuery({
queryKey: ["secondary-simulation-results", secondarySimulationId],
queryFn: () => getResultForSimulation(secondarySimulationId!),
enabled: !!secondarySimulationId,
retry: (_count, error) => error instanceof ResultIsPending,
retryDelay: () => 2000,
});

return {
secondaryResults,
isLoadingSecondary: isStartingSecondary || isLoadingSecondaryResults,
secondaryError: startError || resultsError,
hasSecondaryResults: !!secondaryResults,
secondarySimulationId,
};
};
export default useSecondaryModelQuery;
67 changes: 54 additions & 13 deletions frontend/src/pages/Models.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ import DownloadButton from "@/components/DownloadButton";
import { convertSimulationQueriesResultToTabulatedData, convertTabulatedDataToCSVFormat } from "@/functions/Formatting";
import { simulationHistory } from "@/hooks/useSimulationHistory.ts";
import { getModelInputStore } from "@/hooks/useModelInputStore";
import { useSecondaryModelQuery } from "@/hooks/useSecondaryModelQuery";

const Models: React.FC = () => {
const [currentModel, setCurrentModel] = useState<ModelConfig | undefined>(undefined);
const [currentPrimaryModel, setCurrentPrimaryModel] = useState<ModelConfig | undefined>(undefined);
const [currentSecondaryModel, setCurrentSecondaryModel] = useState<ModelConfig | undefined>(undefined);
const { models } = useAvailableModels();
const { simulationId } = useParams<{ simulationId?: string }>();
const navigate = useNavigate();
Expand All @@ -36,14 +38,16 @@ const Models: React.FC = () => {
},
});

const { data: simulationResults, isLoading } = useQuery({
const { data, isLoading } = useQuery({
queryKey: ["simulation", simulationId],
queryFn: () => getResultForSimulation(simulationId!),
enabled: simulationId !== undefined,
retry: (_count, error) => error instanceof ResultIsPending,
retryDelay: () => 2000,
});

let simulationResults = data;

useEffect(() => {
if (simulationId && !isLoading) {
simulationHistory.finalizeEntry(simulationId);
Expand All @@ -52,27 +56,56 @@ const Models: React.FC = () => {

useEffect(() => {
if (simulationResults && models.length > 0) {
const model = models.find((model) => model.modelId === simulationResults.modelInput.modelId);
const model = models.find((model) => model.modelId === simulationResults?.modelInput.modelId);

if (model) {
setCurrentModel(model);
getModelInputStore(model).getState().reset(simulationResults.modelInput);
if (model.category === "Primary") {
setCurrentPrimaryModel(model);
getModelInputStore(model).getState().reset(simulationResults.modelInput);
} else if (model.category === "Secondary") {
setCurrentSecondaryModel(model);
getModelInputStore(model).getState().reset(simulationResults.modelInput);
}
} else {
console.log(`Could not find model ${simulationResults.modelInput.modelId}`);
}
}
}, [simulationResults, models]);

const secondaryModelResults = useSecondaryModelQuery({
primaryResults: simulationResults,
secondaryModel: currentSecondaryModel,
enabled:
simulationResults !== undefined && currentSecondaryModel !== undefined && currentPrimaryModel !== undefined,
});

let inputsStep: ReactNode | null = null;

if (currentModel === undefined) {
if (currentPrimaryModel === undefined && currentSecondaryModel === undefined) {
inputsStep = <CenteredImage src={noModelImage} caption="No model selected" />;
} else {
inputsStep = <ModelInputs model={currentModel} onSubmit={setModelInput} />;
} else if (currentPrimaryModel !== undefined && currentSecondaryModel === undefined) {
inputsStep = <ModelInputs model={currentPrimaryModel} onSubmit={setModelInput} />;
} else if (currentPrimaryModel === undefined && currentSecondaryModel !== undefined) {
inputsStep = <ModelInputs model={currentSecondaryModel} onSubmit={setModelInput} />;
} else if (currentPrimaryModel !== undefined && currentSecondaryModel !== undefined) {
inputsStep = <ModelInputs model={currentPrimaryModel} onSubmit={setModelInput} />;

if (secondaryModelResults.hasSecondaryResults) {
simulationResults = {
...simulationResults,
status: simulationResults?.status ?? "done",
modelInput: simulationResults?.modelInput ?? { concentrations: {}, parameters: {}, modelId: "" },
finalConcentrations: simulationResults?.finalConcentrations ?? {},
panels: [
...(simulationResults?.panels ?? []),
...(secondaryModelResults.secondaryResults?.panels ?? []),
],
};
}
}

let resultsStep: ReactNode | null = null;
if (isLoading) {
if (isLoading || (currentSecondaryModel !== undefined && secondaryModelResults.isLoadingSecondary)) {
resultsStep = <Working />;
} else if (simulationResults === undefined) {
resultsStep = <NoResults />;
Expand Down Expand Up @@ -104,14 +137,22 @@ const Models: React.FC = () => {

return (
<MainContainer>
<Step step={1} title="Models" description="Select a model for the simulation" />
<ModelSelect currentModel={currentModel} setCurrentModel={setCurrentModel} />
<Step
step={1}
title="Models"
description="Select a model for simulation. Models can be run in a pipeline by selecting both primary and secondary."
/>
<ModelSelect
currentPrimaryModel={currentPrimaryModel}
setCurrentPrimaryModel={setCurrentPrimaryModel}
currentSecondaryModel={currentSecondaryModel}
setCurrentSecondaryModel={setCurrentSecondaryModel}
/>
<Step step={2} title="Inputs" />
{inputsStep}
<Step step={3} title="Results" />
{resultsStep}
{/* Padding (25% of the device height) */}
<div style={{ height: "25dvh" }}></div>
<div style={{ height: "25dvh" }} />
</MainContainer>
);
};
Expand Down
Loading