Skip to content
Open
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
@@ -0,0 +1,20 @@
'use client';

import { useParams } from 'next/navigation';

import ContributeConfig from '@/features/contribute';
import { WorkspaceContext } from '@/types/common';

type Params = WorkspaceContext & { circuit_id: string };

export default function ContributeConfiguration() {
const { virtualLabId, projectId } = useParams<Params>();

return (
<ContributeConfig
circuitId="ee3bc6d2-2953-4c23-8272-82dbeb321943"
virtualLabId={virtualLabId}
projectId={projectId}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ReactNode } from 'react';
import ContributeLayout from '@/components/explore-section/ContributeLayout';

export default function ContributeDataLayout({ children }: { children: ReactNode }) {
return <ContributeLayout>{children}</ContributeLayout>;
}
44 changes: 33 additions & 11 deletions src/components/entities-type-stats/interactive-navigation-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ function EntityTypeStats(props: StatsPanelProps) {
return match(selectedTab)
.with('experimental-data', () => (
<>
{Object.entries(ExperimentalEntitiesTileTypes).map(([key, value]) => {
const href = `${pathName}/${value?.explore.basePrefix}/${value.slug}`;
{Object.entries(ExperimentalEntitiesTileTypes).map(([key, value], index) => {
const baseHref = `${pathName}/${value?.explore.basePrefix}/${value.slug}`;
let records = '';
let isError = false;

Expand All @@ -74,16 +74,38 @@ function EntityTypeStats(props: StatsPanelProps) {
isError = !!error || typeof tmpResult === 'string';
}

// Find the last occurrence of '/interactive/' and insert '/add/' after it
const lastInteractiveIndex = pathName.lastIndexOf('/interactive');
let addHref = baseHref;
if (lastInteractiveIndex !== -1) {
const pathBeforeInteractive = pathName.substring(
0,
lastInteractiveIndex + '/interactive'.length
);
const pathAfterInteractive = baseHref.substring(baseHref.indexOf('/experimental')); // Assuming /experimental is always after /interactive
addHref = `${pathBeforeInteractive}/add${pathAfterInteractive}`;
}

return (
<EntityTypeCount
isError={isError}
key={`count-${key}`}
href={href}
title={value.title}
records={records}
type={value.type}
isLoading={isLoading}
/>
<div key={`count-container-${key}`} className="flex items-center">
<EntityTypeCount
isError={isError}
key={`count-${key}`}
href={baseHref}
title={value.title}
records={records}
type={value.type}
isLoading={isLoading}
/>
{index === 0 && (
<a
href={addHref} // Use the modified addHref here
className="ml-2 rounded-md bg-blue-600 px-3 py-1 text-sm font-medium text-white hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none"
>
add
</a>
)}
</div>
);
})}
</>
Expand Down
47 changes: 47 additions & 0 deletions src/components/explore-section/ContributeLayout/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// File: app/virtual-lab/explore/interactive/add/layout.tsx (or wherever your 'add' route lives)

'use client';

import { ReactNode } from 'react';
import { usePathname } from 'next/navigation';
import { ErrorBoundary } from 'react-error-boundary';
import SimpleErrorComponent from '@/components/GenericErrorFallback';
import BackToInteractiveExplorationBtn from '@/components/explore-section/BackToInteractiveExplorationBtn';

export default function ContributeLayout({ children }: { children: ReactNode }) {
const pathname = usePathname();

// Determine the href for the back button.
// Goal: If current is /virtual-lab/explore/interactive/add/experimental/morphology
// It should go back to /virtual-lab/explore/interactive
const splittedPathname = pathname.split('/');
let interactivePageHref = '/'; // Default fallback

const addIndex = splittedPathname.indexOf('add');

if (addIndex > -1) {
// If 'add' is found, take all segments up to (but not including) 'add'
interactivePageHref = splittedPathname.slice(0, addIndex).join('/');
} else {
// Fallback: If 'add' is not in the path (unexpected for this layout),
// go back one level from the current path.
interactivePageHref = splittedPathname.slice(0, splittedPathname.length - 1).join('/');
}

// Ensure the path starts with a '/' if it's not empty (e.g., if it's just 'virtual-lab/explore/interactive')
if (interactivePageHref === '') {
interactivePageHref = '/';
} else if (!interactivePageHref.startsWith('/')) {
interactivePageHref = '/' + interactivePageHref;
}

return (
<div className="bg-primary-9 flex h-screen w-full overflow-x-auto" id="contribute-layout">
<ErrorBoundary FallbackComponent={SimpleErrorComponent}>
<BackToInteractiveExplorationBtn href={interactivePageHref} />

<div className="bg-primary-9 grow text-white">{children}</div>
</ErrorBoundary>
</div>
);
}
75 changes: 7 additions & 68 deletions src/features/cell-composition/how-to.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# HOW-TO: Construct Brain Cell Composition

This short document explains how cell composition has been constructed, which builds a hierarchical representation of brain cell types and their densities across brain regions.
This short document explains how cell composition has been constructed,
which builds a hierarchical representation of brain cell types and their
densities across brain regions.

## Overview

The cell composition feature processes hierarchical brain region data along with cell type information to generate a unified view of neuron and glial cell distributions. It handles relationships between `brain_region`, `mTypes`, `eTypes`.
The cell composition feature processes hierarchical brain region data
along with cell type information to generate a unified view of neuron
and glial cell distributions. It handles relationships between
`brain_region`, `mTypes`, `eTypes`.

## Data Flow

Expand All @@ -22,69 +27,3 @@ flowchart TD
I --> K[return needed data]
J --> K
```

## How to process:

1. **Initialization**

- start with a brain region id (selected by the user)
- retrieve all leaf regions under that brain region (use `brainRegionHierarchyAtom`)
- create volume maps for each leaf region (using `brainRegionAtlasAtom`)

2. **Node Construction**

- for each leaf region:
- process mTypes and their child eTypes
- build tree nodes with cell counts and densities
- calculate composition data based on volumes

3. **Node Aggregation**

- merge nodes that represent the same mType/eType across different leaf regions
- sum cell counts across regions
- recalculate densities based on aggregated volumes
- create links between parent mTypes and child eTypes

4. **Final Calculations**

- calculate total neuron and glial cell counts
- determine total volume (not needed for now)
- compute the whole composition densities

## Implementation Details

### Composite IDs

The system uses composite IDs (`mTypeId__eTypeId`) to ensure that when the same eType appears under different mTypes, they are treated as distinct nodes. This preserves the hierarchical relationship.

```mermaid
graph TD
mType1 --> eType1_1[eType1 under mType1]
mType1 --> eType2_1[eType2 under mType1]
mType2 --> eType1_2[eType1 under mType2]
mType2 --> eType3[eType3]
```

### Volume and Density calculations:

- **cell count** = density × volume
- **aggregated density** = totalCellCount / TotalVolume
- **neuron count scale** = 1e-9

### Node merging:

When the same node (by composite ID) appears in multiple leaf regions:

1. merge the lists of associated leaf regions
2. sum the cell counts from all regions
3. recalculate densities based on the total volume of all associated regions
4. combine related nodes (for mTypes pointing to eTypes)

## Output:

The final output contains:

- **nodes**: Array of tree nodes representing mTypes and eTypes
- **links**: Connections between parent mTypes and child eTypes
- **totalVolume**: Combined volume of all leaf regions
- **totalComposition**: Overall neuron and glial cell densities and counts
170 changes: 170 additions & 0 deletions src/features/contribute/_components/atoms/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { atom } from 'jotai';
import { atomWithRefresh } from 'jotai/utils';
import isEqual from 'lodash/isEqual';

import { downloadAsset } from '@/api/entitycore/queries/assets';
import { getCircuit } from '@/api/entitycore/queries/model/circuit';
import { getCircuitSimulations } from '@/api/entitycore/queries/simulation/circuit-simulation';
import { getCircuitSimulationExecutions } from '@/api/entitycore/queries/simulation/circuit-simulation-execution';
import { getCircuitSimulationResult } from '@/api/entitycore/queries/simulation/circuit-simulation-result';
import { EntityTypeValue } 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 { ICircuitSimulationResult } from '@/api/entitycore/types/entities/circuit-simulation-result';
import { getLatestSimExecStatus } from '@/features/small-microcircuit/_components/utils';
import { SimExecStatusMap } from '@/features/small-microcircuit/types';
import { WorkspaceContext } from '@/types/common';
import { atomFamilyWithExpiration, readAtomFamilyWithExpiration } from '@/util/atoms';

const simExecBySimIdAtomFamily = readAtomFamilyWithExpiration(
({ simulationId, context }: { simulationId: string; context: WorkspaceContext }) =>
atom<Promise<ICircuitSimulationExecution>>(async () => {
const simulationExecutionFilters = { used__id: simulationId };
const res = await getCircuitSimulationExecutions({
filters: simulationExecutionFilters,
context,
});

return res.data[0];
}),
{
ttl: 120000, // 2 minutes
areEqual: isEqual,
}
);

export const simExecRemoteStatusMapAtomFamily = atomFamilyWithExpiration(
({ simulationIds, context }: { simulationIds: string[]; context: WorkspaceContext }) =>
atomWithRefresh<Promise<SimExecStatusMap>>(async () => {
const simulationExecutionFilters = { used__id__in: simulationIds.join(',') };
const res = await getCircuitSimulationExecutions({
filters: simulationExecutionFilters,
context,
});

return res.data.reduce(
(map, simExec) => map.set(simExec.used[0].id, simExec.status),
new Map()
);
}),
{
ttl: 120000, // 2 minutes
areEqual: isEqual,
}
);

type SimExecStatusMapAtomFamilyArg = { simulationIds: string[]; context: WorkspaceContext };

export const simExecStatusMapAtomFamily = atomFamilyWithExpiration(
({ simulationIds, context }: SimExecStatusMapAtomFamilyArg) => {
const simExecRemoteStatusMapAtom = simExecRemoteStatusMapAtomFamily({
simulationIds,
context,
});

const localStatusMapAtom = atom<SimExecStatusMap>(new Map());

return atom<Promise<SimExecStatusMap>, [SimExecStatusMap], void>(
async (get) => {
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 remoteStatus = remoteStatusMap.get(simId);
const localStatus = localStatusMap.get(simId);
// If both are set we take the latest possible one,
// because the status change in a particular sequence.
// See definition of getLatestSimExecStatus
const status =
remoteStatus && localStatus
? getLatestSimExecStatus(remoteStatus, localStatus)
: (localStatus ?? remoteStatus);
return map.set(simId, status);
}, new Map());

return statusMap;
},
(get, set, newStatusMap) => set(localStatusMapAtom, new Map(newStatusMap))
);
},
{
ttl: 120000, // 2 minutes
areEqual: isEqual,
}
);

export const simResultBySimIdAtomFamily = readAtomFamilyWithExpiration(
({ simulationId, context }: { simulationId: string; context: WorkspaceContext }) =>
atom<Promise<ICircuitSimulationResult>>(async (get) => {
const execution = await get(simExecBySimIdAtomFamily({ simulationId, context }));

if (!execution?.generated?.[0]) {
throw new Error('Simulation Result not found');
}

return getCircuitSimulationResult({ id: execution.generated[0].id, context });
}),
{
ttl: 120000, // 2 minutes
areEqual: isEqual,
}
);

export const simulationsByCampaignIdAtomFamily = readAtomFamilyWithExpiration(
({ campaignId, context }: { campaignId: string; context: WorkspaceContext }) =>
atom<Promise<ICircuitSimulation[]>>(async () => {
const filters = { simulation_campaign_id: campaignId };
const res = await getCircuitSimulations({ filters, context });

return res.data;
}),
{
ttl: 120000, // 2 minutes
areEqual: isEqual,
}
);

export const circuitAtomFamily = readAtomFamilyWithExpiration(
({ circuitId, context }: { circuitId: string; context: WorkspaceContext }) =>
atom<Promise<ICircuit>>(async () => {
return getCircuit({ id: circuitId, context });
}),
{
ttl: 120000, // 2 minutes
areEqual: isEqual,
}
);

export const fileAtomFamily = readAtomFamilyWithExpiration(
({
id,
entityId,
entityType,
assetPath,
context,
}: {
id: string;
entityId: string;
entityType: EntityTypeValue;
assetPath?: string;
context: WorkspaceContext;
}) =>
atom<Promise<ICircuit>>(async () => {
const res = await downloadAsset({
ctx: context,
entityId,
id,
entityType,
assetPath,
asRawResponse: true,
});

return res.json();
}),
{
ttl: 120000, // 2 minutes
areEqual: isEqual,
}
);
Loading
Loading