Skip to content

Commit c3f219b

Browse files
committed
Initial implementation of brain-atlas endpoints
See #168 for the specification. Briefly; a `BrainAtlas` is an named entity that has an asset `annotation.nrrd` attached to it. It is associated with a particular `BrainRegionHierarchy` In addition, `BrainAtlasRegion` gives metadata for all the regions within a `BrainAtlas`; * their volume in the `annotation.nrrd` if they are a leaf, -1 otherwise. * an attached asset with the .obj mesh
1 parent 9061703 commit c3f219b

18 files changed

+1904
-593
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import: ## Run the import on a database, assumes mba_hierarchy.json and out are
5353
@$(call load_env,run-local)
5454
@test -n "$(PROJECT_ID_IMPORT)" || (echo "Please set the variable PROJECT_ID_IMPORT"; exit 1)
5555
@test -n "$(VIRTUAL_LAB_ID_IMPORT)" || (echo "Please set the variable VIRTUAL_LAB_ID_IMPORT"; exit 1)
56-
docker compose up --wait db
56+
#docker compose up --wait db
5757
uv run -m alembic upgrade head
5858
uv run -m app.cli.import_data --seed 0 hierarchy $(HIERARCHY_NAME) mba_hierarchy.json
5959
uv run -m app.cli.import_data --seed 1 run ./out --virtual-lab-id $(VIRTUAL_LAB_ID_IMPORT) --project-id $(PROJECT_ID_IMPORT) --hierarchy-name $(HIERARCHY_NAME)

app/cli/brain_region_data.py

Lines changed: 1430 additions & 0 deletions
Large diffs are not rendered by default.

app/cli/curate.py

Lines changed: 2 additions & 450 deletions
Large diffs are not rendered by default.

app/cli/import_data.py

Lines changed: 90 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import datetime
22
import glob
3+
import hashlib
34
import json
45
import os
56
import random
@@ -17,6 +18,7 @@
1718
from tqdm import tqdm
1819

1920
from app.cli import curate, utils
21+
from app.cli.brain_region_data import BRAIN_ATLAS_REGION_VOLUMES
2022
from app.cli.curation import electrical_cell_recording
2123
from app.cli.types import ContentType
2224
from app.cli.utils import (
@@ -31,6 +33,7 @@
3133
Annotation,
3234
Asset,
3335
BrainAtlas,
36+
BrainAtlasRegion,
3437
BrainRegion,
3538
BrainRegionHierarchy,
3639
CellComposition,
@@ -47,14 +50,14 @@
4750
Measurement,
4851
MeasurementAnnotation,
4952
MEModel,
50-
Mesh,
5153
METypeDensity,
5254
MTypeClass,
5355
MTypeClassification,
5456
Organization,
5557
Person,
5658
ReconstructionMorphology,
5759
SingleNeuronSimulation,
60+
Species,
5861
)
5962
from app.db.session import configure_database_session_manager
6063
from app.db.types import (
@@ -71,6 +74,9 @@
7174
from app.cli.curation import electrical_cell_recording, cell_composition
7275
from app.cli.types import ContentType
7376

77+
78+
BRAIN_ATLAS_NAME = "BlueBrain Atlas"
79+
7480
REQUIRED_PATH = click.Path(exists=True, readable=True, dir_okay=False, resolve_path=True)
7581
REQUIRED_PATH_DIR = click.Path(
7682
exists=True, readable=True, file_okay=False, dir_okay=True, resolve_path=True
@@ -555,47 +561,97 @@ def ingest(
555561
db.commit()
556562

557563

558-
class ImportBrainRegionMeshes(Import):
559-
name = "BrainRegionMeshes"
564+
class ImportBrainAtlas(Import):
565+
name = "BrainAtlas"
560566

561567
@staticmethod
562568
def is_correct_type(data):
563-
types = ensurelist(data["@type"])
564-
return "BrainParcellationMesh" in types
569+
# for reasons unknown, the annotations are tagged as v1.2.0
570+
# the contents of the annotations is the same as in
571+
# s3://openbluebrain/Model_Data/Brain_atlas/Mouse/resolution_25_um/version_1.1.0/Annotation_volume/annotation_ccfv3_l23split_barrelsplit_validated.nrrd
572+
# but their sha256 hashes differ
573+
return (
574+
utils.is_type(data, "BrainParcellationMesh")
575+
and "atlasRelease" in data
576+
and data["atlasRelease"].get("tag") == "v1.1.0"
577+
) or (
578+
utils.is_type(data, "BrainParcellationDataLayer")
579+
and "atlasRelease" in data
580+
and data["atlasRelease"].get("tag") == "v1.2.0"
581+
)
565582

566583
@staticmethod
567584
def ingest(db, project_context, data_list, all_data_by_id, hierarchy_name: str):
568-
for data in tqdm(data_list):
569-
if "atlasRelease" not in data or data["atlasRelease"].get("tag", "") != "v1.1.0":
570-
continue
585+
meshes, annotations = [], []
586+
for d in data_list:
587+
if utils.is_type(d, "BrainParcellationMesh"):
588+
meshes.append(d)
589+
else:
590+
annotations.append(d)
591+
592+
assert len(annotations) == 1
593+
brain_atlas = db.execute(
594+
sa.select(BrainAtlas).filter(BrainAtlas.name == BRAIN_ATLAS_NAME)
595+
).scalar_one_or_none()
596+
if brain_atlas is None:
597+
hierarchy = db.execute(
598+
sa.select(BrainRegionHierarchy).filter(BrainRegionHierarchy.name == hierarchy_name)
599+
).scalar_one()
600+
601+
mouse = db.execute(
602+
sa.select(Species).filter(Species.name == "Mus musculus")
603+
).scalar_one()
604+
605+
brain_atlas = BrainAtlas(
606+
name=BRAIN_ATLAS_NAME,
607+
description="version v1.1.0 from NEXUS",
608+
species_id=mouse.id,
609+
hierarchy_id=hierarchy.id,
610+
authorized_project_id=project_context.project_id,
611+
authorized_public=True,
612+
)
613+
db.add(brain_atlas)
614+
db.commit()
571615

572-
legacy_id = data["@id"]
573-
legacy_self = data["_self"]
574-
rm = utils._find_by_legacy_id(legacy_id, Mesh, db)
575-
if rm:
576-
continue
616+
utils.import_distribution(
617+
annotations[0], brain_atlas.id, EntityType.brain_atlas, db, project_context
618+
)
577619

578-
try:
579-
brain_region_id = utils.get_brain_region(data, hierarchy_name, db)
580-
except RuntimeError:
581-
L.exception("Cannot import mesh")
582-
continue
620+
for mesh in tqdm(meshes):
621+
brain_region_data = mesh["brainLocation"]["brainRegion"]
622+
brain_region_id = utils.get_brain_region_by_hier_id(
623+
brain_region_data, hierarchy_name, db
624+
)
583625

584-
createdAt, updatedAt = utils.get_created_and_updated(data)
626+
atlas_region = db.execute(
627+
sa.select(BrainAtlasRegion).filter(
628+
BrainAtlasRegion.brain_region_id == str(brain_region_id)
629+
)
630+
).scalar_one_or_none()
631+
632+
if atlas_region is None:
633+
annotation_id = curate.curate_brain_region(brain_region_data)["@id"]
634+
volume = -1
635+
636+
leaf_region = False
637+
if annotation_id in BRAIN_ATLAS_REGION_VOLUMES:
638+
volume = BRAIN_ATLAS_REGION_VOLUMES[annotation_id]
639+
leaf_region = True
640+
641+
atlas_region = BrainAtlasRegion(
642+
brain_atlas_id=brain_atlas.id,
643+
brain_region_id=brain_region_id,
644+
volume=volume,
645+
leaf_region=leaf_region,
646+
authorized_project_id=project_context.project_id,
647+
authorized_public=True,
648+
)
649+
db.add(atlas_region)
650+
db.commit()
585651

586-
db_item = Mesh(
587-
name=data["name"],
588-
description=data["description"],
589-
legacy_id=[legacy_id],
590-
legacy_self=[legacy_self],
591-
brain_region_id=brain_region_id,
592-
creation_date=createdAt,
593-
update_date=updatedAt,
594-
authorized_project_id=project_context.project_id,
595-
authorized_public=AUTHORIZED_PUBLIC,
652+
utils.import_distribution(
653+
mesh, atlas_region.id, EntityType.brain_atlas, db, project_context
596654
)
597-
db.add(db_item)
598-
db.commit()
599655

600656

601657
class ImportMorphologies(Import):
@@ -743,8 +799,6 @@ def ingest(db, project_context, data_list, all_data_by_id, hierarchy_name):
743799
brain_region_id = utils.get_brain_region(data, hierarchy_name, db)
744800

745801
license_id = utils.get_license_mixin(data, db)
746-
# species_id, strain_id = utils.get_species_mixin(data, db)
747-
748802
subject_id = utils.get_or_create_subject(data, project_context, db)
749803
createdAt, updatedAt = utils.get_created_and_updated(data)
750804

@@ -1036,50 +1090,6 @@ def ingest(
10361090
create_annotation(annotation, db_item.id, db)
10371091

10381092

1039-
class ImportBrainAtlas(Import):
1040-
name = "Brain Atlas"
1041-
1042-
@staticmethod
1043-
def is_correct_type(data):
1044-
return "BrainAtlasRelease" in ensurelist(data["@type"]) and data["@id"] == BRAIN_ATLAS_ID
1045-
1046-
@staticmethod
1047-
def ingest(
1048-
db: Session,
1049-
project_context: ProjectContext,
1050-
data_list: list[dict],
1051-
all_data_by_id: dict,
1052-
hierarchy_name: str,
1053-
):
1054-
for data in data_list:
1055-
legacy_id, legacy_self = data["@id"], data["_self"]
1056-
rm = utils._find_by_legacy_id(legacy_id, BrainAtlas, db)
1057-
if rm:
1058-
continue
1059-
1060-
created_by_id, updated_by_id = utils.get_agent_mixin(data, db)
1061-
createdAt, updatedAt = utils.get_created_and_updated(data)
1062-
species_id, strain_id = utils.get_species_mixin(data, db)
1063-
1064-
brain_region_id = utils.get_brain_region(data, hierarchy_name, db)
1065-
1066-
db_item = BrainAtlas(
1067-
legacy_id=[legacy_id],
1068-
legacy_self=[legacy_self],
1069-
name=data["name"],
1070-
description=data.get("description", ""),
1071-
brain_region_id=brain_region_id,
1072-
species_id=species_id,
1073-
strain_id=strain_id,
1074-
created_by_id=created_by_id,
1075-
updated_by_id=updated_by_id,
1076-
creation_date=createdAt,
1077-
update_date=updatedAt,
1078-
authorized_project_id=project_context.project_id,
1079-
authorized_public=AUTHORIZED_PUBLIC,
1080-
)
1081-
1082-
10831093
class ImportDistribution(Import):
10841094
name = "Distribution"
10851095

@@ -1098,8 +1108,7 @@ def ingest(
10981108
ignored: dict[tuple[dict], int] = Counter()
10991109
for data in tqdm(data_list):
11001110
legacy_id = data["@id"]
1101-
root = utils._find_by_legacy_id(legacy_id, Entity, db)
1102-
if root:
1111+
if root := utils._find_by_legacy_id(legacy_id, Entity, db):
11031112
utils.import_distribution(data, root.id, root.type, db, project_context)
11041113
else:
11051114
dt = data["@type"]
@@ -1339,9 +1348,7 @@ def _do_import(db, input_dir, project_context, hierarchy_name):
13391348
ImportAgent,
13401349
ImportMETypeDensity,
13411350
ImportCellComposition,
1342-
ImportBrainAtlas,
13431351
ImportAnalysisSoftwareSourceCode,
1344-
ImportBrainRegionMeshes,
13451352
ImportMorphologies,
13461353
ImportEModels,
13471354
ImportExperimentalNeuronDensities,
@@ -1350,6 +1357,7 @@ def _do_import(db, input_dir, project_context, hierarchy_name):
13501357
ImportMEModel,
13511358
ImportElectricalCellRecording,
13521359
ImportSingleNeuronSimulation,
1360+
ImportBrainAtlas,
13531361
ImportDistribution,
13541362
ImportNeuronMorphologyFeatureAnnotation,
13551363
]
@@ -1579,7 +1587,7 @@ def curate_files(input_digest_path, output_digest_path, out_dir, dry_run):
15791587
closing(configure_database_session_manager()) as database_session_manager,
15801588
database_session_manager.session() as db,
15811589
):
1582-
# Group assets by ntity_type/entity_id/content_type
1590+
# Group assets by entity_type/entity_id/content_type
15831591
assets_per_entity_type = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
15841592
for asset in db.query(Asset).all():
15851593
entity_type = asset.full_path.split("/")[4]

app/cli/utils.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,9 @@ def _find_by_legacy_id(legacy_id, db_type, db, _cache={}):
6060
return res
6161

6262

63-
def get_brain_region_by_hier_id(brain_region, hierarchy_name, db, _cache={}):
64-
brain_region = curate.curate_brain_region(brain_region)
65-
66-
brain_region_id = int(brain_region["@id"])
63+
def get_brain_region_by_annotation_id(brain_region_id: int, hierarchy_name: str, db, _cache={}):
6764
if (hierarchy_name, brain_region_id) in _cache:
6865
return _cache[hierarchy_name, brain_region_id]
69-
7066
br = db.execute(
7167
sa.select(BrainRegion)
7268
.join(BrainRegionHierarchy, BrainRegion.hierarchy_id == BrainRegionHierarchy.id)
@@ -77,13 +73,22 @@ def get_brain_region_by_hier_id(brain_region, hierarchy_name, db, _cache={}):
7773
).scalar_one_or_none()
7874

7975
if br is None:
80-
msg = f"({hierarchy_name}, {brain_region}) not found in database"
76+
msg = f"({hierarchy_name}, {brain_region_id}) not found in database"
8177
raise RuntimeError(msg)
8278

8379
_cache[hierarchy_name, brain_region_id] = br.id
80+
8481
return br.id
8582

8683

84+
def get_brain_region_by_hier_id(brain_region, hierarchy_name, db):
85+
brain_region = curate.curate_brain_region(brain_region)
86+
87+
brain_region_id = int(brain_region["@id"])
88+
89+
return get_brain_region_by_annotation_id(brain_region_id, hierarchy_name, db)
90+
91+
8792
def get_or_create_species(species, db, _cache={}):
8893
id_ = species["@id"]
8994
if id_ in _cache:

app/db/model.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -464,12 +464,6 @@ class EModel(
464464
__mapper_args__ = {"polymorphic_identity": __tablename__} # noqa: RUF012
465465

466466

467-
class Mesh(LocationMixin, NameDescriptionVectorMixin, Entity):
468-
__tablename__ = EntityType.mesh.value
469-
id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), primary_key=True)
470-
__mapper_args__ = {"polymorphic_identity": __tablename__} # noqa: RUF012
471-
472-
473467
class MEModel(
474468
MTypesMixin, ETypesMixin, SpeciesMixin, LocationMixin, NameDescriptionVectorMixin, Entity
475469
):
@@ -875,9 +869,30 @@ class METypeDensity(
875869
__mapper_args__ = {"polymorphic_identity": __tablename__} # noqa: RUF012
876870

877871

878-
class BrainAtlas(NameDescriptionVectorMixin, LocationMixin, SpeciesMixin, Entity):
872+
class BrainAtlas(NameDescriptionVectorMixin, SpeciesMixin, Entity):
879873
__tablename__ = EntityType.brain_atlas
874+
875+
id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), primary_key=True)
876+
877+
hierarchy_id: Mapped[uuid.UUID] = mapped_column(
878+
ForeignKey("brain_region_hierarchy.id"), index=True
879+
)
880+
881+
__mapper_args__ = {"polymorphic_identity": __tablename__} # noqa: RUF012
882+
883+
884+
class BrainAtlasRegion(Entity, LocationMixin):
885+
__tablename__ = EntityType.brain_atlas_region
886+
880887
id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), primary_key=True)
888+
889+
# only the volume for leaf nodes is saved; the consumer must calculate
890+
# volumes depending on which view of the hierarchy they are using
891+
volume: Mapped[float] = mapped_column(nullable=True)
892+
leaf_region: Mapped[bool] = mapped_column(default=False)
893+
894+
brain_atlas_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("brain_atlas.id"), index=True)
895+
881896
__mapper_args__ = {"polymorphic_identity": __tablename__} # noqa: RUF012
882897

883898

app/db/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ class EntityType(StrEnum):
5050

5151
analysis_software_source_code = auto()
5252
brain_atlas = auto()
53+
brain_atlas_region = auto()
5354
emodel = auto()
5455
cell_composition = auto()
5556
experimental_bouton_density = auto()

0 commit comments

Comments
 (0)