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
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Default migration message

Revision ID: 8d1610d7c882
Revises: 634224e88212
Create Date: 2025-05-26 17:35:12.743948

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


from sqlalchemy import Text
import app.db.types

# revision identifiers, used by Alembic.
revision: str = "8d1610d7c882"
down_revision: Union[str, None] = "634224e88212"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint("unique_person_name_1", "person", type_="unique")
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_unique_constraint("unique_person_name_1", "person", ["given_name", "family_name"])
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""Default migration message

Revision ID: 1589bff44728
Revises: 8d1610d7c882
Create Date: 2025-05-26 22:23:51.082630

"""

from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
from alembic_postgresql_enum import TableReference

from sqlalchemy import Text
import app.db.types

# revision identifiers, used by Alembic.
revision: str = "1589bff44728"
down_revision: Union[str, None] = "8d1610d7c882"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"memodel_calibration_result",
sa.Column("id", sa.Uuid(), nullable=False),
sa.Column("holding_current", sa.Float(), nullable=False),
sa.Column("threshold_current", sa.Float(), nullable=False),
sa.Column("rin", sa.Float(), nullable=True),
sa.Column("calibrated_entity_id", sa.Uuid(), nullable=False),
sa.ForeignKeyConstraint(
["calibrated_entity_id"],
["memodel.id"],
name=op.f("fk_memodel_calibration_result_calibrated_entity_id_memodel"),
),
sa.ForeignKeyConstraint(
["id"], ["entity.id"], name=op.f("fk_memodel_calibration_result_id_entity")
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_memodel_calibration_result")),
)
op.create_index(
op.f("ix_memodel_calibration_result_calibrated_entity_id"),
"memodel_calibration_result",
["calibrated_entity_id"],
unique=False,
)
op.drop_column("memodel", "holding_current")
op.drop_column("memodel", "threshold_current")
op.sync_enum_values(
enum_schema="public",
enum_name="entitytype",
new_values=[
"analysis_software_source_code",
"brain_atlas",
"brain_atlas_region",
"emodel",
"cell_composition",
"memodel_calibration_result",
"experimental_bouton_density",
"experimental_neuron_density",
"experimental_synapses_per_connection",
"memodel",
"mesh",
"me_type_density",
"reconstruction_morphology",
"electrical_cell_recording",
"electrical_recording_stimulus",
"single_neuron_simulation",
"single_neuron_synaptome",
"single_neuron_synaptome_simulation",
"ion_channel_model",
"subject",
"validation_result",
],
affected_columns=[
TableReference(table_schema="public", table_name="entity", column_name="type")
],
enum_values_to_rename=[],
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.sync_enum_values(
enum_schema="public",
enum_name="entitytype",
new_values=[
"analysis_software_source_code",
"brain_atlas",
"brain_atlas_region",
"emodel",
"cell_composition",
"experimental_bouton_density",
"experimental_neuron_density",
"experimental_synapses_per_connection",
"memodel",
"mesh",
"me_type_density",
"reconstruction_morphology",
"electrical_cell_recording",
"electrical_recording_stimulus",
"single_neuron_simulation",
"single_neuron_synaptome",
"single_neuron_synaptome_simulation",
"ion_channel_model",
"subject",
"validation_result",
],
affected_columns=[
TableReference(table_schema="public", table_name="entity", column_name="type")
],
enum_values_to_rename=[],
)
op.add_column(
"memodel",
sa.Column(
"threshold_current",
sa.DOUBLE_PRECISION(precision=53),
autoincrement=False,
nullable=True,
),
)
op.add_column(
"memodel",
sa.Column(
"holding_current", sa.DOUBLE_PRECISION(precision=53), autoincrement=False, nullable=True
),
)
op.drop_index(
op.f("ix_memodel_calibration_result_calibrated_entity_id"),
table_name="memodel_calibration_result",
)
op.drop_table("memodel_calibration_result")
# ### end Alembic commands ###
17 changes: 14 additions & 3 deletions app/cli/import_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
Measurement,
MeasurementAnnotation,
MEModel,
MEModelCalibrationResult,
METypeDensity,
MTypeClass,
MTypeClassification,
Expand Down Expand Up @@ -901,13 +902,23 @@ def ingest(db, project_context, data_list, all_data_by_id, hierarchy_name: str):
strain_id=morphology.strain_id,
creation_date=createdAt,
update_date=updatedAt,
holding_current=data.get("holding_current"),
threshold_current=data.get("threshold_current"),
)

db.add(db_item)
db.flush()

db_calibration = MEModelCalibrationResult(
calibrated_entity_id=db_item.id,
holding_current=data.get("holding_current", 0),
threshold_current=data.get("threshold_current", 0),
authorized_project_id=project_context.project_id,
authorized_public=AUTHORIZED_PUBLIC,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
creation_date=createdAt,
update_date=updatedAt,
)
db.add(db_calibration)
db.flush()
utils.import_contribution(data, db_item.id, db)

for annotation in ensurelist(data.get("annotation", [])):
Expand Down
30 changes: 26 additions & 4 deletions app/db/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,6 @@ class Person(Agent):
"polymorphic_identity": __tablename__,
"polymorphic_load": "selectin",
}
__table_args__ = (UniqueConstraint("given_name", "family_name", name="unique_person_name_1"),)


class Organization(Agent):
Expand Down Expand Up @@ -483,13 +482,17 @@ class MEModel(
"ReconstructionMorphology", foreign_keys=[morphology_id], uselist=False
)

holding_current: Mapped[float | None]
threshold_current: Mapped[float | None]

emodel_id: Mapped[uuid.UUID] = mapped_column(ForeignKey(f"{EntityType.emodel}.id"))

emodel = relationship("EModel", foreign_keys=[emodel_id], uselist=False)

calibration_result = relationship(
"MEModelCalibrationResult",
uselist=False,
foreign_keys="MEModelCalibrationResult.calibrated_entity_id",
lazy="joined",
)

__mapper_args__ = {"polymorphic_identity": __tablename__} # noqa: RUF012


Expand Down Expand Up @@ -832,6 +835,25 @@ class ValidationResult(Entity):
}


class MEModelCalibrationResult(Entity):
__tablename__ = EntityType.memodel_calibration_result.value
id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), primary_key=True)
holding_current: Mapped[float]
threshold_current: Mapped[float]
Comment on lines +841 to +842
Copy link
Contributor

@g-bar g-bar May 27, 2025

Choose a reason for hiding this comment

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

The implication of this is that now a SingleNeuronSimulation should have a reference to both an MEModal and an MEModelCalibrationResult.

Not sure how it is for SingleNeuronSynaptome which also reference MEModel

Copy link
Contributor Author

Choose a reason for hiding this comment

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

hum, @AurelienJaquier should we have multiple calibration result per memodel or just one ?

Copy link
Contributor

Choose a reason for hiding this comment

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

We should have just one.

Copy link
Contributor

@g-bar g-bar May 28, 2025

Choose a reason for hiding this comment

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

If users create an MEModel, and run a simulation without running validation, then the resulting simulations use different currents (the dummy values in bluenaas). That's a different model than then one with a calibration result.

Is it possible to re-run validation? Is Calibration result itself immutable? If i re-run the validation service do I update the values in the existing CalibrationResult?, is it guaranteed not to result in different values and thus not 'invalidate' simulations ran using the previous values?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is Calibration result itself immutable?

Calibration result is immutable. However, we can replace it with a new calibration result. (recomputing the calibration)
Which means that it would be a different calibration result id. Therefore the simulation result would likely be different.

Provenance should track the memodel id + calibration result id used.

Wrt invalidation, we don't have that concept so far.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

if a change in a field of calibration result means the model will behave differently, then it should be a different entity.
Therefore, we would replace the previous calibration result entity, with a new one, with a different id.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@g-bar , I am trying to add an memodel to calibrationresult relationship:
calibration_result = relationship( "MEModelCalibrationResult", uselist=False, foreign_keys="MEModelCalibrationResult.calibrated_entity_id", lazy="joined", )

However this thing is giving me the following error:

from return response_schema_class.model_validate(row) in "router_read_one":
"Error extracting attribute: InvalidRequestError: 'MEModel.calibration_result' is not available due to lazy='raise' [type=get_attribute_error, input_value=<app.db.model.MEModel object at 0x12cbaaa80>, input_type=MEModel]"

I think you worked on that , would you have any clue ?

Copy link
Contributor

@g-bar g-bar May 28, 2025

Choose a reason for hiding this comment

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

Yes, we just had to add a joinedload to the MEModel response query. (See my latest commit).

Copy link
Contributor

Choose a reason for hiding this comment

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

Is calibration_result part of the immutability constraints of the MEModel based on the immutability proposal the is currently pending? i.e. will we need to create a new MEModel id if a new calibration result is created?

If the MEModel results differ when a calibration result is present, then it should be part of its immutability constraints. Otherwise, it will be quite difficult to track when a config (that points to entities like memodels for example), which is an input to an activity, has changed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is calibration_result part of the immutability constraints of the MEModel based on the immutability proposal the is currently pending? i.e. will we need to create a new MEModel id if a new calibration result is created?

no it will be a new calibration result. With respect to the current proposal, the previous calibration result would be "deprecated". The ME Model would stay the same.
Provenance should log the usage of the MEModel and the CalibrationResult.

rin: Mapped[float | None]
calibrated_entity_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("memodel.id"), index=True)
calibrated_entity: Mapped[Entity] = relationship(
"MEModel",
uselist=False,
foreign_keys=[calibrated_entity_id],
)

__mapper_args__ = { # noqa: RUF012
"polymorphic_identity": __tablename__,
"inherit_condition": id == Entity.id,
}


class Asset(Identifiable):
"""Asset table."""

Expand Down
1 change: 1 addition & 0 deletions app/db/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class EntityType(StrEnum):
brain_atlas_region = auto()
emodel = auto()
cell_composition = auto()
memodel_calibration_result = auto()
experimental_bouton_density = auto()
experimental_neuron_density = auto()
experimental_synapses_per_connection = auto()
Expand Down
27 changes: 27 additions & 0 deletions app/filters/memodel_calibration_result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import uuid
from typing import Annotated

from fastapi_filter import FilterDepends

from app.db.model import MEModelCalibrationResult
from app.filters.base import CustomFilter
from app.filters.common import EntityFilterMixin


class MEModelCalibrationResultFilter(
CustomFilter,
EntityFilterMixin,
):
passed: bool | None = None
calibrated_entity_id: uuid.UUID | None = None

order_by: list[str] = ["calibrated_entity_id"] # noqa: RUF012

class Constants(CustomFilter.Constants):
model = MEModelCalibrationResult
ordering_model_fields = ["calibrated_entity_id"] # noqa: RUF012


MEModelCalibrationResultFilterDep = Annotated[
MEModelCalibrationResultFilter, FilterDepends(MEModelCalibrationResultFilter)
]
2 changes: 2 additions & 0 deletions app/routers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
license,
measurement_annotation,
memodel,
memodel_calibration_result,
morphology,
mtype,
organization,
Expand Down Expand Up @@ -54,6 +55,7 @@
license.router,
measurement_annotation.router,
memodel.router,
memodel_calibration_result.router,
morphology.router,
mtype.router,
organization.router,
Expand Down
11 changes: 11 additions & 0 deletions app/routers/memodel_calibration_result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from fastapi import APIRouter

import app.service.memodel_calibration_result

router = APIRouter(
prefix="/memodel-calibration-result",
tags=["memodel-calibration-result"],
)
read_many = router.get("")(app.service.memodel_calibration_result.read_many)
read_one = router.get("/{id_}")(app.service.memodel_calibration_result.read_one)
create_one = router.post("")(app.service.memodel_calibration_result.create_one)
4 changes: 2 additions & 2 deletions app/schemas/me_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
)
from app.schemas.contribution import ContributionReadWithoutEntity
from app.schemas.emodel import EModelRead
from app.schemas.memodel_calibration_result import MEModelCalibrationResultRead
from app.schemas.morphology import ReconstructionMorphologyRead


Expand All @@ -25,8 +26,6 @@ class MEModelBase(BaseModel):
name: str
description: str
validation_status: ValidationStatus = ValidationStatus.created
holding_current: float | None = None
threshold_current: float | None = None


# To be used by entities who reference MEModel
Expand Down Expand Up @@ -59,3 +58,4 @@ class MEModelRead(
etypes: list[ETypeClassRead] | None
morphology: ReconstructionMorphologyRead
emodel: EModelRead
calibration_result: MEModelCalibrationResultRead | None
28 changes: 28 additions & 0 deletions app/schemas/memodel_calibration_result.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import uuid

from pydantic import BaseModel

from app.schemas.agent import CreatedByUpdatedByMixin
from app.schemas.base import (
CreationMixin,
IdentifiableMixin,
)


class MEModelCalibrationResultBase(BaseModel):
"""Base model for MEModel calibration results."""

holding_current: float
threshold_current: float
rin: float | None = None
calibrated_entity_id: uuid.UUID


class MEModelCalibrationResultRead(
MEModelCalibrationResultBase, CreationMixin, IdentifiableMixin, CreatedByUpdatedByMixin
):
"""Read model for MEModel calibration results, including entity metadata."""


class MEModelCalibrationResultCreate(MEModelCalibrationResultBase):
"""Create model for MEModel calibration results."""
3 changes: 3 additions & 0 deletions app/service/memodel.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
Contribution,
EModel,
MEModel,
MEModelCalibrationResult,
ReconstructionMorphology,
)
from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep
Expand Down Expand Up @@ -70,6 +71,8 @@ def _load(select: Select):
joinedload(MEModel.etypes),
joinedload(MEModel.created_by),
joinedload(MEModel.updated_by),
joinedload(MEModel.calibration_result).joinedload(MEModelCalibrationResult.created_by),
joinedload(MEModel.calibration_result).joinedload(MEModelCalibrationResult.updated_by),
raiseload("*"),
)

Expand Down
Loading