From 5146bc534e42c187007301069a1b6d6d26fbe45a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= <72930209+AurelienJaquier@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:16:22 +0100 Subject: [PATCH 01/13] Add ion channel model simulation campaign --- app/db/model.py | 96 +++++++-- app/db/types.py | 16 ++ .../ion_channel_model_simulation_campaign.py | 30 +++ app/routers/__init__.py | 2 + .../ion_channel_model_simulation_campaign.py | 20 ++ .../ion_channel_model_simulation_campaign.py | 54 +++++ .../ion_channel_model_simulation_campaign.py | 193 ++++++++++++++++++ tests/utils.py | 23 +++ 8 files changed, 418 insertions(+), 16 deletions(-) create mode 100644 app/filters/ion_channel_model_simulation_campaign.py create mode 100644 app/routers/ion_channel_model_simulation_campaign.py create mode 100644 app/schemas/ion_channel_model_simulation_campaign.py create mode 100644 app/service/ion_channel_model_simulation_campaign.py diff --git a/app/db/model.py b/app/db/model.py index a1d7a9f0..c4903236 100644 --- a/app/db/model.py +++ b/app/db/model.py @@ -1248,6 +1248,80 @@ class IonChannelModelToEModel(Base): ) +class IonChannelModelToIonChannelModelSimulationCampaign(Base): + __tablename__ = "ion_channel_model__ion_channel_model_simulation_campaign" + + ion_channel_model_id: Mapped[uuid.UUID] = mapped_column( + ForeignKey(f"{EntityType.ion_channel_model}.id", ondelete="CASCADE"), primary_key=True + ) + ion_channel_model_simulation_campaign_id: Mapped[uuid.UUID] = mapped_column( + ForeignKey(f"{EntityType.ion_channel_model_simulation_campaign}.id", ondelete="CASCADE"), + primary_key=True, + ) + + +class SimulationCampaignBase( + NameDescriptionVectorMixin, + Entity, +): + """Represents a simulation campaign entity in the database. + + A simulation campaign represents the specification of a set of simulations. + + it has an asset which is the simulation campaign configuration file. + + Attributes: + id (uuid.UUID): Primary key for the simulation campaign, referencing the entity ID. + """ + + id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), primary_key=True) + + simulations = relationship( + "Simulation", + uselist=True, + back_populates="simulation_campaign", + foreign_keys="Simulation.simulation_campaign_id", + ) + scan_parameters: Mapped[JSON_DICT] = mapped_column( + default={}, + nullable=False, + server_default="{}", + ) + + +class IonChannelModelSimulationCampaign( + SimulationCampaignBase, +): + """Represents an ion channel model simulation campaign entity in the database. + + An ion channel model simulation campaign represents the specification of a set of + ion channel model simulation tasks. + + It has an asset which is the ion channel model simulation campaign configuration file. + + Attributes: + id (uuid.UUID): Primary key for the ion channel model simulation campaign, + referencing the entity ID. + """ + + __tablename__ = EntityType.ion_channel_model_simulation_campaign.value + + ion_channel_models: Mapped[list["IonChannelModel"]] = relationship( + "IonChannelModel", + primaryjoin=( + "IonChannelModelSimulationCampaign.id == " + "IonChannelModelToIonChannelModelSimulationCampaign." + "ion_channel_model_simulation_campaign_id" + ), + secondary="ion_channel_model__ion_channel_model_simulation_campaign", + ) + + __mapper_args__ = { # noqa: RUF012 + "polymorphic_identity": __tablename__, + "inherit_condition": id == Entity.id, + } + + class IonChannelRecordingToIonChannelModelingCampaign(Base): __tablename__ = "ion_channel_recording__ion_channel_modeling_campaign" @@ -1503,21 +1577,8 @@ class SimulationCampaign( """ __tablename__ = EntityType.simulation_campaign.value - id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), primary_key=True) - entity_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), index=True) - simulations = relationship( - "Simulation", - uselist=True, - back_populates="simulation_campaign", - foreign_keys="Simulation.simulation_campaign_id", - ) - scan_parameters: Mapped[JSON_DICT] = mapped_column( - default={}, - nullable=False, - server_default="{}", - ) __mapper_args__ = { # noqa: RUF012 "polymorphic_identity": __tablename__, "inherit_condition": id == Entity.id, @@ -1546,16 +1607,19 @@ class Simulation(Entity, NameDescriptionVectorMixin): simulation_campaign_id: Mapped[uuid.UUID] = mapped_column( ForeignKey("simulation_campaign.id"), index=True ) - simulation_campaign: Mapped[SimulationCampaign] = relationship( - "SimulationCampaign", + simulation_campaign: Mapped[SimulationCampaignBase] = relationship( + "SimulationCampaignBase", uselist=False, foreign_keys=[simulation_campaign_id], ) - entity_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), index=True) + entity_id: Mapped[uuid.UUID | None] = mapped_column( + ForeignKey("entity.id"), index=True, nullable=True + ) entity: Mapped[Entity] = relationship( "Entity", uselist=False, foreign_keys=[entity_id], + nullable=True, ) number_neurons: Mapped[int] = mapped_column(BigInteger) scan_parameters: Mapped[JSON_DICT] = mapped_column( diff --git a/app/db/types.py b/app/db/types.py index 1cd7a3f2..d78c4720 100644 --- a/app/db/types.py +++ b/app/db/types.py @@ -785,6 +785,22 @@ class LabelRequirements(BaseModel): ) ], }, + EntityType.ion_channel_model_simulation_campaign: { + AssetLabel.campaign_generation_config: [ + LabelRequirements( + content_type=ContentType.json, + is_directory=False, + description="Campaign configuration.", + ) + ], + AssetLabel.campaign_summary: [ + LabelRequirements( + content_type=ContentType.json, + is_directory=False, + description="Summary of generated campaign listing all created simulation configs.", + ) + ], + }, EntityType.simulation_campaign: { AssetLabel.campaign_generation_config: [ LabelRequirements( diff --git a/app/filters/ion_channel_model_simulation_campaign.py b/app/filters/ion_channel_model_simulation_campaign.py new file mode 100644 index 00000000..0733e670 --- /dev/null +++ b/app/filters/ion_channel_model_simulation_campaign.py @@ -0,0 +1,30 @@ +from typing import Annotated + +from fastapi_filter import with_prefix + +from app.db.model import IonChannelModelSimulationCampaign +from app.dependencies.filter import FilterDepends +from app.filters.base import CustomFilter +from app.filters.common import ILikeSearchFilterMixin, NameFilterMixin +from app.filters.entity import EntityFilterMixin +from app.filters.simulation import NestedSimulationFilter + + +class IonChannelModelSimulationCampaignFilter( + CustomFilter, EntityFilterMixin, NameFilterMixin, ILikeSearchFilterMixin +): + simulation: Annotated[ + NestedSimulationFilter | None, + FilterDepends(with_prefix("simulation", NestedSimulationFilter)), + ] = None + + order_by: list[str] = ["-creation_date"] # noqa: RUF012 + + class Constants(CustomFilter.Constants): + model = IonChannelModelSimulationCampaign + ordering_model_fields = ["creation_date", "update_date", "name"] # noqa: RUF012 + + +IonChannelModelSimulationCampaignFilterDep = Annotated[ + IonChannelModelSimulationCampaignFilter, FilterDepends(IonChannelModelSimulationCampaignFilter) +] diff --git a/app/routers/__init__.py b/app/routers/__init__.py index a62c2b03..91c545df 100644 --- a/app/routers/__init__.py +++ b/app/routers/__init__.py @@ -40,6 +40,7 @@ external_url, ion_channel, ion_channel_model, + ion_channel_model_simulation_campaign, ion_channel_modeling_campaign, ion_channel_modeling_config, ion_channel_modeling_config_generation, @@ -124,6 +125,7 @@ external_url.router, ion_channel.router, ion_channel_model.router, + ion_channel_model_simulation_campaign.router, ion_channel_modeling_campaign.router, ion_channel_modeling_config.router, ion_channel_modeling_config_generation.router, diff --git a/app/routers/ion_channel_model_simulation_campaign.py b/app/routers/ion_channel_model_simulation_campaign.py new file mode 100644 index 00000000..2a3a35a9 --- /dev/null +++ b/app/routers/ion_channel_model_simulation_campaign.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter + +import app.service.ion_channel_model_simulation_campaign +from app.routers.admin import router as admin_router + +ROUTE = "ion-channel-model-simulation-campaign" +router = APIRouter(prefix=f"/{ROUTE}", tags=[ROUTE]) + +read_many = router.get("")(app.service.ion_channel_model_simulation_campaign.read_many) +read_one = router.get("/{id_}")(app.service.ion_channel_model_simulation_campaign.read_one) +create_one = router.post("")(app.service.ion_channel_model_simulation_campaign.create_one) +update_one = router.patch("/{id_}")(app.service.ion_channel_model_simulation_campaign.update_one) +delete_one = router.delete("/{id_}")(app.service.ion_channel_model_simulation_campaign.delete_one) + +admin_read_one = admin_router.get(f"/{ROUTE}/{{id_}}")( + app.service.ion_channel_model_simulation_campaign.admin_read_one +) +admin_update_one = admin_router.patch(f"/{ROUTE}/{{id_}}")( + app.service.ion_channel_model_simulation_campaign.admin_update_one +) diff --git a/app/schemas/ion_channel_model_simulation_campaign.py b/app/schemas/ion_channel_model_simulation_campaign.py new file mode 100644 index 00000000..9ff78821 --- /dev/null +++ b/app/schemas/ion_channel_model_simulation_campaign.py @@ -0,0 +1,54 @@ +from pydantic import BaseModel, ConfigDict + +from app.db.types import JSON_DICT +from app.schemas.agent import CreatedByUpdatedByMixin +from app.schemas.asset import AssetsMixin +from app.schemas.base import ( + AuthorizationMixin, + AuthorizationOptionalPublicMixin, + CreationMixin, + EntityTypeMixin, + IdentifiableMixin, + NameDescriptionMixin, +) +from app.schemas.contribution import ContributionReadWithoutEntityMixin +from app.schemas.ion_channel import NestedIonChannelRead +from app.schemas.utils import make_update_schema + + +class IonChannelModelSimulationCampaignBase(BaseModel, NameDescriptionMixin): + model_config = ConfigDict(from_attributes=True) + scan_parameters: JSON_DICT + + +class IonChannelModelSimulationCampaignCreate( + IonChannelModelSimulationCampaignBase, AuthorizationOptionalPublicMixin +): + pass + + +IonChannelModelSimulationCampaignUserUpdate = make_update_schema( + IonChannelModelSimulationCampaignCreate, "IonChannelModelSimulationCampaignUserUpdate" +) # pyright: ignore [reportInvalidTypeForm] +IonChannelModelSimulationCampaignAdminUpdate = make_update_schema( + IonChannelModelSimulationCampaignCreate, + "IonChannelModelSimulationCampaignAdminUpdate", + excluded_fields=set(), +) # pyright : ignore [reportInvalidTypeForm] + + +class NestedIonChannelModelSimulationCampaignRead( + IonChannelModelSimulationCampaignBase, EntityTypeMixin, IdentifiableMixin +): + pass + + +class IonChannelModelSimulationCampaignRead( + NestedIonChannelModelSimulationCampaignRead, + AssetsMixin, + CreatedByUpdatedByMixin, + CreationMixin, + AuthorizationMixin, + ContributionReadWithoutEntityMixin, +): + ion_channel_models: list[NestedIonChannelRead] diff --git a/app/service/ion_channel_model_simulation_campaign.py b/app/service/ion_channel_model_simulation_campaign.py new file mode 100644 index 00000000..0f7a4874 --- /dev/null +++ b/app/service/ion_channel_model_simulation_campaign.py @@ -0,0 +1,193 @@ +import uuid +from typing import TYPE_CHECKING + +import sqlalchemy as sa +from sqlalchemy.orm import aliased, joinedload, raiseload, selectinload + +from app.db.model import ( + Agent, + IonChannelModelSimulationCampaign, + Simulation, + Person, +) +from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep +from app.dependencies.common import ( + FacetsDep, + PaginationQuery, + SearchDep, +) +from app.dependencies.db import SessionDep +from app.filters.ion_channel_model_simulation_campaign import IonChannelModelSimulationCampaignFilterDep +from app.queries.common import ( + router_create_one, + router_read_many, + router_read_one, + router_update_one, + router_user_delete_one, +) +from app.queries.factory import query_params_factory +from app.schemas.ion_channel_model_simulation_campaign import ( + IonChannelModelSimulationCampaignAdminUpdate, + IonChannelModelSimulationCampaignCreate, + IonChannelModelSimulationCampaignRead, + IonChannelModelSimulationCampaignUserUpdate, +) +from app.schemas.routers import DeleteResponse +from app.schemas.types import ListResponse + +if TYPE_CHECKING: + from app.filters.base import Aliases + + +def _load(query: sa.Select): + return query.options( + joinedload(IonChannelModelSimulationCampaign.created_by), + joinedload(IonChannelModelSimulationCampaign.updated_by), + selectinload(IonChannelModelSimulationCampaign.assets), + selectinload(IonChannelModelSimulationCampaign.contributions), + selectinload(IonChannelModelSimulationCampaign.simulations), + selectinload(IonChannelModelSimulationCampaign.ion_channel_models), + raiseload("*"), + ) + + +def read_one( + user_context: UserContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> IonChannelModelSimulationCampaignRead: + return router_read_one( + db=db, + id_=id_, + db_model_class=IonChannelModelSimulationCampaign, + user_context=user_context, + response_schema_class=IonChannelModelSimulationCampaignRead, + apply_operations=_load, + ) + + +def admin_read_one( + db: SessionDep, + id_: uuid.UUID, +) -> IonChannelModelSimulationCampaignRead: + return router_read_one( + db=db, + id_=id_, + db_model_class=IonChannelModelSimulationCampaign, + user_context=None, + response_schema_class=IonChannelModelSimulationCampaignRead, + apply_operations=_load, + ) + + +def create_one( + db: SessionDep, + json_model: IonChannelModelSimulationCampaignCreate, + user_context: UserContextWithProjectIdDep, +) -> IonChannelModelSimulationCampaignRead: + return router_create_one( + db=db, + json_model=json_model, + user_context=user_context, + db_model_class=IonChannelModelSimulationCampaign, + response_schema_class=IonChannelModelSimulationCampaignRead, + apply_operations=_load, + ) + + +def update_one( + user_context: UserContextDep, + db: SessionDep, + id_: uuid.UUID, + json_model: IonChannelModelSimulationCampaignUserUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> IonChannelModelSimulationCampaignRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=IonChannelModelSimulationCampaign, + user_context=user_context, + json_model=json_model, + response_schema_class=IonChannelModelSimulationCampaignRead, + apply_operations=_load, + ) + + +def admin_update_one( + db: SessionDep, + id_: uuid.UUID, + json_model: IonChannelModelSimulationCampaignAdminUpdate, # pyright: ignore [reportInvalidTypeForm] +) -> IonChannelModelSimulationCampaignRead: + return router_update_one( + id_=id_, + db=db, + db_model_class=IonChannelModelSimulationCampaign, + user_context=None, + json_model=json_model, + response_schema_class=IonChannelModelSimulationCampaignRead, + apply_operations=_load, + ) + + +def read_many( + user_context: UserContextDep, + db: SessionDep, + pagination_request: PaginationQuery, + filter_model: IonChannelModelSimulationCampaignFilterDep, + with_search: SearchDep, + facets: FacetsDep, +) -> ListResponse[IonChannelModelSimulationCampaignRead]: + agent_alias = aliased(Agent, flat=True) + created_by_alias = aliased(Person, flat=True) + updated_by_alias = aliased(Person, flat=True) + simulation_alias = aliased(Simulation, flat=True) + aliases: Aliases = { + Agent: { + "contribution": agent_alias, + }, + Person: { + "created_by": created_by_alias, + "updated_by": updated_by_alias, + }, + Simulation: simulation_alias, + } + facet_keys = filter_keys = [ + "created_by", + "updated_by", + "contribution", + "simulation", + ] + name_to_facet_query_params, filter_joins = query_params_factory( + db_model_class=IonChannelModelSimulationCampaign, + facet_keys=facet_keys, + filter_keys=filter_keys, + aliases=aliases, + ) + return router_read_many( + db=db, + filter_model=filter_model, + db_model_class=IonChannelModelSimulationCampaign, + with_search=with_search, + with_in_brain_region=None, + facets=facets, + name_to_facet_query_params=name_to_facet_query_params, + apply_filter_query_operations=None, + apply_data_query_operations=_load, + aliases=aliases, + pagination_request=pagination_request, + response_schema_class=IonChannelModelSimulationCampaignRead, + authorized_project_id=user_context.project_id, + filter_joins=filter_joins, + ) + + +def delete_one( + user_context: UserContextDep, + db: SessionDep, + id_: uuid.UUID, +) -> DeleteResponse: + return router_user_delete_one( + id_=id_, + db=db, + db_model_class=IonChannelModelSimulationCampaign, + user_context=user_context, + ) diff --git a/tests/utils.py b/tests/utils.py index f022964a..ef2f8832 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -33,6 +33,7 @@ ETypeClass, ETypeClassification, IonChannelModelingCampaign, + IonChannelModelSimulationCampaign, IonChannelRecording, MeasurementAnnotation, MTypeClass, @@ -95,6 +96,7 @@ ElectricalCellRecording: "/electrical-cell-recording", IonChannelRecording: "/ion-channel-recording", IonChannelModelingCampaign: "/ion-channel-modeling-campaign", + IonChannelModelSimulationCampaign: "/ion-channel-model-simulation-campaign", CircuitExtractionCampaign: "/circuit-extraction-campaign", SkeletonizationCampaign: "/skeletonization-campaign", SkeletonizationConfig: "/skeletonization-config", @@ -250,6 +252,27 @@ def create_ion_channel_modeling_campaign_id( return response.json()["id"] +def create_ion_channel_model_simulation_campaign_id( + client, + name="Test Ion Channel Model Simulation Campaign Name", + description="Test Ion Channel Model Simulation Campaign Description", + *, + authorized_public: bool = False, +): + response = client.post( + ROUTES[IonChannelModelSimulationCampaign], + json={ + "name": name, + "description": description, + "authorized_public": authorized_public, + "scan_parameters": {"foo": "bar"}, + }, + ) + + assert response.status_code == 200 + return response.json()["id"] + + def create_skeletonization_campaign_id( client, name="Test Skeletonization Campaign Name", From 7d5da6c3f3601533ff0fa2459bbdaaca2355d7d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= <72930209+AurelienJaquier@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:17:39 +0100 Subject: [PATCH 02/13] lint fix --- app/service/ion_channel_model_simulation_campaign.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/service/ion_channel_model_simulation_campaign.py b/app/service/ion_channel_model_simulation_campaign.py index 0f7a4874..39acc315 100644 --- a/app/service/ion_channel_model_simulation_campaign.py +++ b/app/service/ion_channel_model_simulation_campaign.py @@ -7,8 +7,8 @@ from app.db.model import ( Agent, IonChannelModelSimulationCampaign, - Simulation, Person, + Simulation, ) from app.dependencies.auth import UserContextDep, UserContextWithProjectIdDep from app.dependencies.common import ( @@ -17,7 +17,9 @@ SearchDep, ) from app.dependencies.db import SessionDep -from app.filters.ion_channel_model_simulation_campaign import IonChannelModelSimulationCampaignFilterDep +from app.filters.ion_channel_model_simulation_campaign import ( + IonChannelModelSimulationCampaignFilterDep, +) from app.queries.common import ( router_create_one, router_read_many, From 51eae7c13667716ff225b6ec1fd9a07b0efbbef9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= <72930209+AurelienJaquier@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:30:32 +0100 Subject: [PATCH 03/13] add ion channel model simulation campaign to EntityType --- app/db/model.py | 3 +-- app/db/types.py | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/db/model.py b/app/db/model.py index c4903236..42d491c1 100644 --- a/app/db/model.py +++ b/app/db/model.py @@ -1563,8 +1563,7 @@ class CellComposition(NameDescriptionVectorMixin, LocationMixin, SpeciesMixin, E class SimulationCampaign( - NameDescriptionVectorMixin, - Entity, + SimulationCampaignBase, ): """Represents a simulation campaign entity in the database. diff --git a/app/db/types.py b/app/db/types.py index d78c4720..9d293516 100644 --- a/app/db/types.py +++ b/app/db/types.py @@ -116,6 +116,7 @@ class EntityType(StrEnum): experimental_synapses_per_connection = auto() external_url = auto() ion_channel_model = auto() + ion_channel_model_simulation = auto() ion_channel_modeling_campaign = auto() ion_channel_modeling_config = auto() ion_channel_recording = auto() From 0455613f630cb6508c76ec23f9804d6aaaf68cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= <72930209+AurelienJaquier@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:32:48 +0100 Subject: [PATCH 04/13] fix typo --- app/db/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/db/types.py b/app/db/types.py index 9d293516..26575f66 100644 --- a/app/db/types.py +++ b/app/db/types.py @@ -116,7 +116,7 @@ class EntityType(StrEnum): experimental_synapses_per_connection = auto() external_url = auto() ion_channel_model = auto() - ion_channel_model_simulation = auto() + ion_channel_model_simulation_campaign = auto() ion_channel_modeling_campaign = auto() ion_channel_modeling_config = auto() ion_channel_recording = auto() From 0a4b6d5338a1244ec6aa6747163bd2cac767d470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= <72930209+AurelienJaquier@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:37:22 +0100 Subject: [PATCH 05/13] move id out of SimulationCampaignBase --- app/db/model.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/db/model.py b/app/db/model.py index 42d491c1..2cf54aa3 100644 --- a/app/db/model.py +++ b/app/db/model.py @@ -1269,13 +1269,8 @@ class SimulationCampaignBase( A simulation campaign represents the specification of a set of simulations. it has an asset which is the simulation campaign configuration file. - - Attributes: - id (uuid.UUID): Primary key for the simulation campaign, referencing the entity ID. """ - id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), primary_key=True) - simulations = relationship( "Simulation", uselist=True, @@ -1305,6 +1300,7 @@ class IonChannelModelSimulationCampaign( """ __tablename__ = EntityType.ion_channel_model_simulation_campaign.value + id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), primary_key=True) ion_channel_models: Mapped[list["IonChannelModel"]] = relationship( "IonChannelModel", @@ -1576,6 +1572,8 @@ class SimulationCampaign( """ __tablename__ = EntityType.simulation_campaign.value + id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), primary_key=True) + entity_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), index=True) __mapper_args__ = { # noqa: RUF012 From da8f035d4597e83d9135ad2872cb8830b11dd4bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= <72930209+AurelienJaquier@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:55:27 +0100 Subject: [PATCH 06/13] make SimulationCampaignBase abstract --- app/db/model.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/db/model.py b/app/db/model.py index 2cf54aa3..122244b7 100644 --- a/app/db/model.py +++ b/app/db/model.py @@ -1270,6 +1270,7 @@ class SimulationCampaignBase( it has an asset which is the simulation campaign configuration file. """ + __abstract__ = True simulations = relationship( "Simulation", From 14c822d556dea2f7d7c91885e76ec2115c446dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= <72930209+AurelienJaquier@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:58:27 +0100 Subject: [PATCH 07/13] lint fix --- app/db/model.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/db/model.py b/app/db/model.py index 122244b7..35d777ef 100644 --- a/app/db/model.py +++ b/app/db/model.py @@ -1270,6 +1270,7 @@ class SimulationCampaignBase( it has an asset which is the simulation campaign configuration file. """ + __abstract__ = True simulations = relationship( From 6e32bae523dcfd1c0b74f2e988dbeec8870871a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= <72930209+AurelienJaquier@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:12:32 +0100 Subject: [PATCH 08/13] use declared_attr --- app/db/model.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/app/db/model.py b/app/db/model.py index 35d777ef..0b406aaf 100644 --- a/app/db/model.py +++ b/app/db/model.py @@ -1273,17 +1273,22 @@ class SimulationCampaignBase( __abstract__ = True - simulations = relationship( - "Simulation", - uselist=True, - back_populates="simulation_campaign", - foreign_keys="Simulation.simulation_campaign_id", - ) - scan_parameters: Mapped[JSON_DICT] = mapped_column( - default={}, - nullable=False, - server_default="{}", - ) + @declared_attr + def simulations(cls): + return relationship( + "Simulation", + uselist=True, + back_populates="simulation_campaign", + foreign_keys="Simulation.simulation_campaign_id", + ) + + @declared_attr + def scan_parameters(cls) -> Mapped[JSON_DICT]: + return mapped_column( + default={}, + nullable=False, + server_default="{}", + ) class IonChannelModelSimulationCampaign( From 1174df419e1386defcca4101360a946aa75d1ba1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= <72930209+AurelienJaquier@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:14:08 +0100 Subject: [PATCH 09/13] also use classmethod --- app/db/model.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/db/model.py b/app/db/model.py index 0b406aaf..88f16e81 100644 --- a/app/db/model.py +++ b/app/db/model.py @@ -1274,6 +1274,7 @@ class SimulationCampaignBase( __abstract__ = True @declared_attr + @classmethod def simulations(cls): return relationship( "Simulation", @@ -1283,6 +1284,7 @@ def simulations(cls): ) @declared_attr + @classmethod def scan_parameters(cls) -> Mapped[JSON_DICT]: return mapped_column( default={}, From ce1adbd05156f3bb18db48bc6dcc315eb179edb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= <72930209+AurelienJaquier@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:20:49 +0100 Subject: [PATCH 10/13] fix relationship --- app/db/model.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/db/model.py b/app/db/model.py index 88f16e81..5512c25f 100644 --- a/app/db/model.py +++ b/app/db/model.py @@ -1625,7 +1625,6 @@ class Simulation(Entity, NameDescriptionVectorMixin): "Entity", uselist=False, foreign_keys=[entity_id], - nullable=True, ) number_neurons: Mapped[int] = mapped_column(BigInteger) scan_parameters: Mapped[JSON_DICT] = mapped_column( From 2b427207307ee9ed61bbc69c5a157577dffd0f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= <72930209+AurelienJaquier@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:53:10 +0100 Subject: [PATCH 11/13] polymorphic SimulationCampaignBase --- app/db/model.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/db/model.py b/app/db/model.py index 5512c25f..21f72e56 100644 --- a/app/db/model.py +++ b/app/db/model.py @@ -1291,6 +1291,10 @@ def scan_parameters(cls) -> Mapped[JSON_DICT]: nullable=False, server_default="{}", ) + + __mapper_args__ = { + "polymorphic_on": type, + } class IonChannelModelSimulationCampaign( @@ -1611,7 +1615,7 @@ class Simulation(Entity, NameDescriptionVectorMixin): __tablename__ = EntityType.simulation.value id: Mapped[uuid.UUID] = mapped_column(ForeignKey("entity.id"), primary_key=True) simulation_campaign_id: Mapped[uuid.UUID] = mapped_column( - ForeignKey("simulation_campaign.id"), index=True + ForeignKey("simulation_campaign_base.id"), index=True ) simulation_campaign: Mapped[SimulationCampaignBase] = relationship( "SimulationCampaignBase", From 8c6d1b2abe200e0569b491ff1911001dc2d9b56b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= <72930209+AurelienJaquier@users.noreply.github.com> Date: Wed, 25 Feb 2026 11:58:20 +0100 Subject: [PATCH 12/13] lint fix --- app/db/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/db/model.py b/app/db/model.py index 21f72e56..e49f71ec 100644 --- a/app/db/model.py +++ b/app/db/model.py @@ -1291,7 +1291,7 @@ def scan_parameters(cls) -> Mapped[JSON_DICT]: nullable=False, server_default="{}", ) - + __mapper_args__ = { "polymorphic_on": type, } From 354b9d25bcc995d6fe7c40ee5939390ce5edb7bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien?= <72930209+AurelienJaquier@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:02:03 +0100 Subject: [PATCH 13/13] lint fix --- app/db/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/db/model.py b/app/db/model.py index e49f71ec..017af5ea 100644 --- a/app/db/model.py +++ b/app/db/model.py @@ -1292,7 +1292,7 @@ def scan_parameters(cls) -> Mapped[JSON_DICT]: server_default="{}", ) - __mapper_args__ = { + __mapper_args__ = { # noqa: RUF012 "polymorphic_on": type, }