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
8 changes: 4 additions & 4 deletions src/api/small-scale-simulator/circuit/simulation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,22 @@ import type { WorkspaceContext } from '@/types/common';

type Params = {
ctx: WorkspaceContext;
simulationId: string;
simulationIds: string[];
signal?: AbortSignal;
};

export async function runSimulation({ ctx, simulationId, signal }: Params) {
export async function runBatch({ ctx, simulationIds, signal }: Params) {
const api = await smallScaleSimulatorApi();

return api.post<Response>(
'/circuit/simulation/run',
'/circuit/simulation/run-batch',
{
queryParams: { simulation_id: simulationId },
headers: {
...getEntityCoreContext(ctx).headers,
accept: 'application/x-ndjson',
'Content-Type': 'application/json',
},
body: { simulation_ids: simulationIds },
signal,
},
{ asRawResponse: true }
Expand Down
2 changes: 1 addition & 1 deletion src/api/small-scale-simulator/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Small circuit scale
export { runSimulation as runCircuitSimulation } from './circuit/simulation';
export { runBatch as runCircuitSimulationBatch } from './circuit/simulation';

// Single neuron scale
export { createModel as createSingleNeuronSynaptome } from './single-neuron/synaptome';
Expand Down
10 changes: 6 additions & 4 deletions src/features/small-microcircuit/_components/atoms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import { getCircuitSimulationResult } from '@/api/entitycore/queries/simulation/
import { TEntityTypeDict } from '@/api/entitycore/types';
import { ICircuit } from '@/api/entitycore/types/entities/circuit';
import { ICircuitSimulation } from '@/api/entitycore/types/entities/circuit-simulation';
import { ICircuitSimulationExecution } from '@/api/entitycore/types/entities/circuit-simulation-execution';
import {
CircuitSimulationExecutionStatus,
ICircuitSimulationExecution,
} from '@/api/entitycore/types/entities/circuit-simulation-execution';
import { ICircuitSimulationResult } from '@/api/entitycore/types/entities/circuit-simulation-result';
import { getLatestSimExecStatus } from '@/features/small-microcircuit/_components/utils';
import { SimExecStatusMap } from '@/features/small-microcircuit/types';
Expand Down Expand Up @@ -70,8 +73,7 @@ export const simExecStatusMapAtomFamily = atomFamilyWithExpiration(
const remoteStatusMap = await get(simExecRemoteStatusMapAtom);
const localStatusMap = get(localStatusMapAtom);

const simIds = Array.from(new Set([...remoteStatusMap.keys(), ...localStatusMap.keys()]));
const statusMap = simIds.reduce((map, simId) => {
const statusMap = simulationIds.reduce((map, simId) => {
const remoteStatus = remoteStatusMap.get(simId);
const localStatus = localStatusMap.get(simId);
// If both are set we take the latest possible one,
Expand All @@ -80,7 +82,7 @@ export const simExecStatusMapAtomFamily = atomFamilyWithExpiration(
const status =
remoteStatus && localStatus
? getLatestSimExecStatus(remoteStatus, localStatus)
: (localStatus ?? remoteStatus);
: (localStatus ?? remoteStatus ?? CircuitSimulationExecutionStatus.CREATED);
return map.set(simId, status);
}, new Map());

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { CircuitSimulationExecutionStatus } from '@/api/entitycore/types/entities/circuit-simulation-execution';
import { simulationStatusColorMap } from '@/features/small-microcircuit/constants';

export function SimulationStatusBadge({ status }: { status?: CircuitSimulationExecutionStatus }) {
const color = status ? statusColorMap[status] : '#fafafa';
const color = status ? simulationStatusColorMap[status] : '#fafafa';
const showSpinner = status && ['pending', 'running'].includes(status);

// TODO: move spinner outside of the module.
Expand Down Expand Up @@ -34,17 +35,10 @@ export function SimulationStatusBadge({ status }: { status?: CircuitSimulationEx

<span
style={{ borderColor: color, color }}
className="flex items-center rounded-xl border-1 px-4"
className="flex items-center rounded-xl border-1 px-4 capitalize transition-colors duration-300"
>
{status ?? ''}
</span>
</div>
);
}
const statusColorMap: Record<CircuitSimulationExecutionStatus, string> = {
created: '#434343',
pending: '#fa8c16',
running: '#1890ff',
done: '#389e0d',
error: '#f5222d',
};
9 changes: 9 additions & 0 deletions src/features/small-microcircuit/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { CircuitSimulationExecutionStatus } from '@/api/entitycore/types/entities/circuit-simulation-execution';

export const simulationStatusColorMap = {
[CircuitSimulationExecutionStatus.CREATED]: '#1890ff',
[CircuitSimulationExecutionStatus.PENDING]: '#fa8c16',
[CircuitSimulationExecutionStatus.RUNNING]: '#389e0d',
[CircuitSimulationExecutionStatus.ERROR]: '#f5222d',
[CircuitSimulationExecutionStatus.DONE]: '#002766',
};
197 changes: 141 additions & 56 deletions src/features/small-microcircuit/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Ajv, { AnySchema } from 'ajv';
import { atom, useAtomValue, useSetAtom } from 'jotai';
import { Fragment, Suspense, useEffect, useMemo, useRef, useState } from 'react';

import { Progress } from 'antd';
import { Checkbox, ConfigProvider } from 'antd';
import { match } from 'ts-pattern';
import {
simExecRemoteStatusMapAtomFamily,
Expand Down Expand Up @@ -36,12 +36,14 @@ import ApiError from '@/api/error';
import authFetch from '@/authFetch';
import { useAppNotification } from '@/components/notification';
import { ButtonCopyId } from '@/features/details-view/button-copy-id';
import { simulationStatusColorMap } from '@/features/small-microcircuit/constants';
import { useLastTruthyValue } from '@/hooks/hooks';
import { messages } from '@/i18n/en/simulation';
import { runSimulation } from '@/services/small-scale-simulator/circuit';
import { runSimulationBatch } from '@/services/small-scale-simulator/circuit';
import { MessageType } from '@/services/small-scale-simulator/types';
import { assertErrorMessage, classNames } from '@/util/utils';
import { getErrorMessage } from '@/utils/error';

import styles from './small-microcircuit.module.css';

export default function SimulationCampaignConfiguration({
Expand Down Expand Up @@ -358,8 +360,6 @@ function SimulationsTab({ campaignId, virtualLabId, projectId }: SimulationTabPr
const [selectedSimulation, setSelectedSimulation] = useState<null | ICircuitSimulation>(null);
const [selectedFile, setSelectedFile] = useState<File | undefined>(undefined);

const [progress, setProgress] = useState<number | null>(null);

const activeSimulationExecStatus = selectedSimulation && statusMap?.get(selectedSimulation.id);

useEffect(() => {
Expand All @@ -374,15 +374,17 @@ function SimulationsTab({ campaignId, virtualLabId, projectId }: SimulationTabPr
}, [simulations, statusMap]);

useEffect(() => {
// If there is only one simulation in the campaign - make it active on page load.
if (simulations.length === 1) setSelectedSimulation(simulations[0]);
// Select first simulation from the list
setSelectedSimulation(simulations[0]);
}, [simulations]);

useEffect(() => {
// Poll simulation statuses if there are active (running/pending) simulations
// and no active simulation request with the status streaming
if (simRequestInProgress) return;

// TODO Optimize the polling when there are multiple simulation requests

const hasActiveSimulations = statusMap
? Array.from(statusMap.values()).some((status) =>
[
Expand All @@ -403,33 +405,31 @@ function SimulationsTab({ campaignId, virtualLabId, projectId }: SimulationTabPr
const run = async () => {
setSimRequestInProgress(true);
try {
for (const simId of simExecSelectedSimulationIds) {
await runSimulation({
ctx: { virtualLabId, projectId },
simulationId: simulations[0].id,
onMessage: (message) => {
match(message)
.with({ message_type: MessageType.STATUS }, (msg) => {
// TODO: fix types
await runSimulationBatch({
ctx: { virtualLabId, projectId },
simulationIds: simExecSelectedSimulationIds,
onInit: () => {
setSimRequestInProgress(false);
setSimExecSelectedSimulationIds([]);
},
onMessage: (message) => {
match(message)
.with({ message_type: MessageType.STATUS }, (msg) => {
// TODO: fix types
const simId = msg.ctx?.simulation_id;
if (simId) {
updateStatusMap(
statusMap!.set(simId, msg.status as unknown as CircuitSimulationExecutionStatus)
);
}

if (msg.status === 'running' && msg.extra) {
const progressParsed = parseInt(msg.extra, 10);
if (Number.isFinite(progressParsed) && progressParsed > 0) {
setProgress(progressParsed);
}
}
if (msg.status !== 'done') return;

if (msg.status !== 'done') return;

notification.success({ message: `Simulation ${simulations[0].name} done` });
})
.otherwise(() => null);
},
});
}
notification.success({ message: `Simulation ${simulations[0].name} done` });
})
.otherwise(() => null);
},
});
} catch (error) {
const defaultMsg = messages.RunningSimulationDefaultError;

Expand All @@ -450,17 +450,30 @@ function SimulationsTab({ campaignId, virtualLabId, projectId }: SimulationTabPr

return (
<div className={styles.threeColumns}>
{/* List of simulations */}
<div className="flex flex-col gap-5 overflow-y-auto border-r border-gray-200 pr-4">
{simulations.map((simulation) => (
<SimulationListItem
key={simulation.id}
simulation={simulation}
execStatus={statusMap?.get(simulation.id)}
onSelect={() => {}}
progress={progress}
/>
))}
<div className="flex border-r border-gray-200 pr-4">
{/* List of simulations */}
<div className="flex flex-col justify-start gap-5 overflow-y-auto">
{simulations.map((simulation) => (
<SimulationListItem
key={simulation.id}
selected={selectedSimulation?.id === simulation.id}
simulation={simulation}
execStatus={statusMap?.get(simulation.id)}
onSelect={() => setSelectedSimulation(simulation)}
onSelectedForSimChange={(simulationId, selected) => {
if (selected) {
setSimExecSelectedSimulationIds((prev) => [...prev, simulationId]);
} else {
setSimExecSelectedSimulationIds((prev) =>
prev.filter((id) => id !== simulationId)
);
}
}}
selectedForSim={simExecSelectedSimulationIds.includes(simulation.id)}
selectionForSimDisabled={simRequestInProgress}
/>
))}
</div>
<button
className={classNames(
'min-h-[50] w-full cursor-pointer rounded-3xl p-2 text-white',
Expand All @@ -471,7 +484,10 @@ function SimulationsTab({ campaignId, virtualLabId, projectId }: SimulationTabPr
onClick={run}
disabled={simRequestInProgress || simExecSelectedSimulationIds.length === 0}
>
Launch simulations {launchSimBtnLabelPrefix}
<div className="flex justify-center gap-4">
<span className="pl-10">Launch simulations {launchSimBtnLabelPrefix}</span>
<div className="w-6">{simRequestInProgress && <LoadingOutlined />}</div>
</div>
</button>
</div>

Expand Down Expand Up @@ -502,25 +518,94 @@ type SimulationBlockProps = {
simulation: ICircuitSimulation;
execStatus?: CircuitSimulationExecutionStatus;
onSelect: (simulationId: string) => void;
progress: number | null;
selected?: boolean;
onSelectedForSimChange: (simulationId: string, selected: boolean) => void;
selectedForSim: boolean;
selectionForSimDisabled?: boolean;
};

function SimulationListItem({ simulation, execStatus, onSelect, progress }: SimulationBlockProps) {
function SimulationListItem({
simulation,
execStatus,
onSelect,
selected,
onSelectedForSimChange,
selectedForSim,
selectionForSimDisabled,
}: SimulationBlockProps) {
const color = simulationStatusColorMap[execStatus ?? CircuitSimulationExecutionStatus.CREATED];

return (
<button
type="button"
title={simulation.name}
className="h-20 w-full cursor-pointer rounded-lg bg-white p-4"
onClick={() => onSelect(simulation.id)}
>
<div className="flex items-center justify-between">
<div className="truncate overflow-hidden whitespace-nowrap">{simulation.name}</div>
<div className="ml-4 flex flex-shrink-0">
<SimulationStatusBadge status={execStatus} />
<RightOutlined className="ml-2 text-sm" />
</div>
<div className="flex-none bg-white">
<div
className="rounded-lg p-4 transition-colors duration-300"
style={{
border: selected ? `2px solid ${color}` : 'none',
backgroundColor: selected ? `${color}0f` : 'white', // 6% opacity for bg color
}}
>
<button
type="button"
title={simulation.name}
className="mb-2 h-18 w-full cursor-pointer"
onClick={() => onSelect(simulation.id)}
>
<div className="flex items-center justify-between">
<div className="font-bold">
{!execStatus || execStatus === CircuitSimulationExecutionStatus.CREATED ? (
<ConfigProvider theme={{ token: { colorPrimary: '#1890ff' } }}>
<Checkbox
className="mr-2 transition-colors duration-300"
disabled={selectionForSimDisabled}
onChange={(e) => onSelectedForSimChange(simulation.id, e.target.checked)}
checked={selectedForSim}
style={{ color }}
>
<span className="truncate overflow-hidden text-lg whitespace-nowrap transition-colors duration-300">
{simulation.name}
</span>
</Checkbox>
</ConfigProvider>
) : (
<span
style={{ color }}
className="truncate overflow-hidden text-lg whitespace-nowrap transition-colors duration-300"
>
{simulation.name}
</span>
)}
</div>
<div className="ml-4 flex flex-shrink-0">
<SimulationStatusBadge status={execStatus} />
<RightOutlined className="ml-2 text-sm" />
</div>
</div>
</button>

<ScanParams scanParams={simulation.scan_parameters} color={color} />
</div>
{progress && <Progress className="mt-2" size="small" percent={progress} status="active" />}
</button>
</div>
);
}

type SimulationScanParams = { [key: string]: string | number };

function ScanParams({ scanParams, color }: { scanParams: SimulationScanParams; color: string }) {
return (
<div className="grid grid-cols-2 gap-x-4 gap-y-2">
{Object.entries(scanParams).map(([key, value]) => (
<div key={key} className="overflow-x-hidden">
<div title={key} className="truncate text-ellipsis text-gray-400">
{key}
</div>
<div
className="truncate font-bold text-ellipsis transition-colors duration-300"
style={{ color }}
>
{value}
</div>
</div>
))}
</div>
);
}
Loading