From eba6406bedf037dc1e43733d52164ea76866ffdc Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Mon, 30 Mar 2026 09:20:31 -0700 Subject: [PATCH 01/45] adds acquisition mapping --- pyproject.toml | 2 +- .../data_contract/_dataset.py | 12 +- uv.lock | 60 +++++- .../README.md | 0 .../examples/coupled_baiting_curriculum.py | 0 .../pyproject.toml | 0 .../__init__.py | 0 .../cli.py | 0 .../coupled_baiting/__init__.py | 0 .../coupled_baiting/curriculum.py | 0 .../coupled_baiting/stages.py | 0 .../metrics.py | 2 +- .../utils.py | 0 .../tests/test_coupled_baiting.py | 0 .../tests/test_metrics.py | 0 .../README.md | 0 .../pyproject.toml | 75 ++++++++ .../__init__.py | 0 .../acquisition.py | 131 +++++++++++++ .../rig.py | 173 ++++++++++++++++++ 20 files changed, 446 insertions(+), 9 deletions(-) rename {src => workspace}/aind_behavior_dynamic_foraging_curricula/README.md (100%) rename {src => workspace}/aind_behavior_dynamic_foraging_curricula/examples/coupled_baiting_curriculum.py (100%) rename {src => workspace}/aind_behavior_dynamic_foraging_curricula/pyproject.toml (100%) rename {src => workspace}/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/__init__.py (100%) rename {src => workspace}/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/cli.py (100%) rename {src => workspace}/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/__init__.py (100%) rename {src => workspace}/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/curriculum.py (100%) rename {src => workspace}/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/stages.py (100%) rename {src => workspace}/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/metrics.py (98%) rename {src => workspace}/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/utils.py (100%) rename {src => workspace}/aind_behavior_dynamic_foraging_curricula/tests/test_coupled_baiting.py (100%) rename {src => workspace}/aind_behavior_dynamic_foraging_curricula/tests/test_metrics.py (100%) create mode 100644 workspace/aind_behavior_dynamic_foraging_metadata_mapper/README.md create mode 100644 workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml create mode 100644 workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py create mode 100644 workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py create mode 100644 workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/rig.py diff --git a/pyproject.toml b/pyproject.toml index 20e4ca1d..9e725b2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ ] [tool.uv.workspace] -members = ["src/aind_behavior_dynamic_foraging_curricula"] +members = ["workspace/*"] [project.urls] Documentation = "https://allenneuraldynamics.github.io/Aind.Behavior.DynamicForaging/" diff --git a/src/aind_behavior_dynamic_foraging/data_contract/_dataset.py b/src/aind_behavior_dynamic_foraging/data_contract/_dataset.py index 3f2e2381..4bba0ad2 100644 --- a/src/aind_behavior_dynamic_foraging/data_contract/_dataset.py +++ b/src/aind_behavior_dynamic_foraging/data_contract/_dataset.py @@ -1,6 +1,6 @@ from pathlib import Path -from aind_behavior_curriculum import TrainerState +from aind_behavior_curriculum import TrainerState, Metrics from aind_behavior_services.session import Session from contraqctor.contract import Dataset, DataStreamCollection from contraqctor.contract.camera import Camera @@ -61,10 +61,18 @@ def make_dataset( data_streams=[ Json( name="PreviousMetrics", - reader_params=Json.make_params( + reader_params=PydanticModel.make_params( + model=Metrics, path=root_path / "behavior/previous_metrics.json", ), ), + PydanticModel( + name="Metrics", + reader_params=PydanticModel.make_params( + model=Metrics, + path=root_path / "behavior/metrics.json", + ), + ), PydanticModel( name="TrainerState", reader_params=PydanticModel.make_params( diff --git a/uv.lock b/uv.lock index b8b3b56a..d8832a09 100644 --- a/uv.lock +++ b/uv.lock @@ -17,6 +17,7 @@ resolution-markers = [ members = [ "aind-behavior-dynamic-foraging", "aind-behavior-dynamic-foraging-curricula", + "aind-behavior-dynamic-foraging-metadata-mapper", ] [[package]] @@ -103,7 +104,7 @@ docs = [ [[package]] name = "aind-behavior-dynamic-foraging-curricula" version = "0.2.1" -source = { editable = "src/aind_behavior_dynamic_foraging_curricula" } +source = { editable = "workspace/aind_behavior_dynamic_foraging_curricula" } dependencies = [ { name = "aind-behavior-curriculum" }, { name = "aind-behavior-dynamic-foraging" }, @@ -149,6 +150,55 @@ docs = [ { name = "ruff" }, ] +[[package]] +name = "aind-behavior-dynamic-foraging-metadata-mapper" +version = "0.0.1" +source = { editable = "workspace/aind_behavior_dynamic_foraging_metadata_mapper" } +dependencies = [ + { name = "aind-behavior-dynamic-foraging" }, + { name = "aind-data-schema" }, + { name = "numpy" }, + { name = "pydantic-settings" }, +] + +[package.dev-dependencies] +dev = [ + { name = "codespell" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] +docs = [ + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extra = ["python"] }, + { name = "pymdown-extensions" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "aind-behavior-dynamic-foraging", editable = "." }, + { name = "aind-data-schema", specifier = ">=2.6.0" }, + { name = "numpy", specifier = ">=2.4.2" }, + { name = "pydantic-settings" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "codespell" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] +docs = [ + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "mkdocstrings", extras = ["python"] }, + { name = "pymdown-extensions" }, + { name = "ruff" }, +] + [[package]] name = "aind-behavior-services" version = "0.13.5" @@ -1642,9 +1692,9 @@ name = "msal" version = "1.35.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cryptography" }, - { name = "pyjwt", extra = ["crypto"] }, - { name = "requests" }, + { name = "cryptography", marker = "sys_platform == 'win32'" }, + { name = "pyjwt", extra = ["crypto"], marker = "sys_platform == 'win32'" }, + { name = "requests", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3c/aa/5a646093ac218e4a329391d5a31e5092a89db7d2ef1637a90b82cd0b6f94/msal-1.35.1.tar.gz", hash = "sha256:70cac18ab80a053bff86219ba64cfe3da1f307c74b009e2da57ef040eb1b5656", size = 165658, upload-time = "2026-03-04T23:38:51.812Z" } wheels = [ @@ -2195,7 +2245,7 @@ wheels = [ [package.optional-dependencies] crypto = [ - { name = "cryptography" }, + { name = "cryptography", marker = "sys_platform == 'win32'" }, ] [[package]] diff --git a/src/aind_behavior_dynamic_foraging_curricula/README.md b/workspace/aind_behavior_dynamic_foraging_curricula/README.md similarity index 100% rename from src/aind_behavior_dynamic_foraging_curricula/README.md rename to workspace/aind_behavior_dynamic_foraging_curricula/README.md diff --git a/src/aind_behavior_dynamic_foraging_curricula/examples/coupled_baiting_curriculum.py b/workspace/aind_behavior_dynamic_foraging_curricula/examples/coupled_baiting_curriculum.py similarity index 100% rename from src/aind_behavior_dynamic_foraging_curricula/examples/coupled_baiting_curriculum.py rename to workspace/aind_behavior_dynamic_foraging_curricula/examples/coupled_baiting_curriculum.py diff --git a/src/aind_behavior_dynamic_foraging_curricula/pyproject.toml b/workspace/aind_behavior_dynamic_foraging_curricula/pyproject.toml similarity index 100% rename from src/aind_behavior_dynamic_foraging_curricula/pyproject.toml rename to workspace/aind_behavior_dynamic_foraging_curricula/pyproject.toml diff --git a/src/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/__init__.py b/workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/__init__.py similarity index 100% rename from src/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/__init__.py rename to workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/__init__.py diff --git a/src/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/cli.py b/workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/cli.py similarity index 100% rename from src/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/cli.py rename to workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/cli.py diff --git a/src/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/__init__.py b/workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/__init__.py similarity index 100% rename from src/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/__init__.py rename to workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/__init__.py diff --git a/src/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/curriculum.py b/workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/curriculum.py similarity index 100% rename from src/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/curriculum.py rename to workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/curriculum.py diff --git a/src/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/stages.py b/workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/stages.py similarity index 100% rename from src/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/stages.py rename to workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/stages.py diff --git a/src/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/metrics.py b/workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/metrics.py similarity index 98% rename from src/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/metrics.py rename to workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/metrics.py index 7f0a7aeb..908e586c 100644 --- a/src/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/metrics.py +++ b/workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/metrics.py @@ -70,7 +70,7 @@ def metrics_from_dataset( logger.debug(f"Calculated foraging efficiency as {foraging_efficiency}") try: - prev_metrics = DynamicForagingMetrics(**dataset["Behavior"]["PreviousMetrics"].data) + prev_metrics = DynamicForagingMetrics.model_validate(dataset["Behavior"]["PreviousMetrics"].data) prev_stage = prev_metrics.stage_name except FileNotFoundError: logger.info("No previous metrics found.") diff --git a/src/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/utils.py b/workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/utils.py similarity index 100% rename from src/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/utils.py rename to workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/utils.py diff --git a/src/aind_behavior_dynamic_foraging_curricula/tests/test_coupled_baiting.py b/workspace/aind_behavior_dynamic_foraging_curricula/tests/test_coupled_baiting.py similarity index 100% rename from src/aind_behavior_dynamic_foraging_curricula/tests/test_coupled_baiting.py rename to workspace/aind_behavior_dynamic_foraging_curricula/tests/test_coupled_baiting.py diff --git a/src/aind_behavior_dynamic_foraging_curricula/tests/test_metrics.py b/workspace/aind_behavior_dynamic_foraging_curricula/tests/test_metrics.py similarity index 100% rename from src/aind_behavior_dynamic_foraging_curricula/tests/test_metrics.py rename to workspace/aind_behavior_dynamic_foraging_curricula/tests/test_metrics.py diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/README.md b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/README.md new file mode 100644 index 00000000..e69de29b diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml new file mode 100644 index 00000000..132e3b67 --- /dev/null +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml @@ -0,0 +1,75 @@ +[build-system] +requires = ["uv_build>=0.8.22"] +build-backend = "uv_build" + +[project] +name = "aind-behavior-dynamic-foraging-metadata-mapper" +description = "A library of mapping for the Dynamic Foraging task." +authors = [ + {name = "Bruno Cruz", email = "bruno.cruz@alleninstitute.org"}, + {name = "Micah Woodard", email = "micah.woodard@alleninstitute.org"} + ] +license = "MIT" +version = "0.0.1" +requires-python = ">=3.11" +classifiers = [ + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Operating System :: Microsoft :: Windows", +] +readme = {file = "README.md", content-type = "text/markdown"} + +dependencies = [ + "numpy>=2.4.2", + "pydantic-settings", + "aind-behavior-dynamic-foraging==0.0.2rc24", + "aind-data-schema>=2.6.0", +] + +[tool.uv.sources] +aind-behavior-dynamic-foraging = { workspace = true } + +[dependency-groups] + +dev = [ + 'ruff', + 'pytest', + 'pytest-cov', + 'codespell', +] + +docs = [ + 'mkdocs', + 'mkdocs-material', + 'mkdocstrings[python]', + 'pymdown-extensions', + 'ruff', +] + +[tool.uv] +default-groups = ['dev'] + +[tool.ruff] +line-length = 120 +target-version = 'py311' + +[tool.ruff.lint] +extend-select = ['Q', 'RUF100', 'C90', 'I'] +extend-ignore = [] +mccabe = { max-complexity = 14 } +pydocstyle = { convention = 'google' } + +[tool.codespell] +skip = '.git,*.pdf,*.svg,uv.lock' +ignore-words-list = 'nd' + +[tool.pytest.ini_options] +addopts = "--strict-markers --tb=short --cov=src --cov-report=term-missing --cov-fail-under=70" +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] + +[project.scripts] +mapper = "aind_behavior_dynamic_foraging_metadata_mapper.cli:main" diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py new file mode 100644 index 00000000..2fc22fbc --- /dev/null +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py @@ -0,0 +1,131 @@ +import os +from datetime import datetime, timezone + +from aind_behavior_dynamic_foraging.data_contract import dataset as df_foraging_dataset +from aind_behavior_dynamic_foraging.rig import AindDynamicForagingRig +from aind_behavior_services.session import Session +from aind_data_schema.components.configs import TriggerType +from aind_data_schema.components.connections import Connection +from aind_data_schema.components.identifiers import Software +from aind_data_schema.core.acquisition import ( + Acquisition, + Code, + DataStream, + DetectorConfig, + StimulusEpoch, + StimulusModality, + PerformanceMetrics +) +from aind_data_schema_models.modalities import Modality + + +def acqusition_from_dataset( + data_directory: os.PathLike, +) -> Acquisition: + """ + Create acquisition model for completed session. + + Args: + data_directory (os.PathLike): + Path to the directory containing the dataset to analyze. This + directory is expected to include all required behavioral data files. + + Returns: + Acquisition: + Acquisition model for session + + Raises: + FileNotFoundError: + If the specified data directory or required files do not exist. + + ValueError: + If the dataset is malformed or missing required fields for + computing metrics. + """ + + dataset = df_foraging_dataset(data_directory) + software_events = dataset["Behavior"]["SoftwareEvents"] + software_events.load_all() + + input_schemas = dataset["Behavior"]["InputSchemas"] + + # extract info from session model + session = Session.model_validate(input_schemas["Session"].data) + acquisition_start_time = session.date + subject_id = session.subject + experimenter = session.experimenter + notes = session.notes + + # extract info from rig model + rig = AindDynamicForagingRig.model_validate(input_schemas["Rig"].data) + + instrument_id = os.getenv("aibs_comp_id", "unknown") + acquisition_end_time = datetime.now(tz=timezone.utc) + + # populate camera data stream + cam_configs = [] + active_devices = ["BehaviorBoard"] + connections = [] + for name, camera in rig.triggered_camera_controller.cameras.items(): + cam_configs.append( + DetectorConfig( + device_name=name, + exposure_time=camera.exposure, + trigger_type=TriggerType.EXTERNAL, + crop_offset_x=camera.region_of_interest.x, + crop_offset_y=camera.region_of_interest.y, + crop_width=camera.region_of_interest.width, + crop_height=camera.region_of_interest.height, + ) + ) + # TODO: compression + active_devices.append(name) + connections.append(Connection(source_device="BehaviorBoard", target_device=name)) + + data_stream = DataStream( + stream_start_time=acquisition_start_time, + stream_end_time=acquisition_end_time, + modalities=[Modality.BEHAVIOR_VIDEOS], + code=[ + Code( + url=r"https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging/blob/feat-adding-curriculum/src/aind_behavior_dynamic_foraging/rig.py", + parameters=rig.model_dump(), + core_dependency=Software(name="bonsai"), + ) + ], + active_devices=active_devices, + configurations=cam_configs, + connections=connections, + ) + + # populate behavior epoch + metrics = dataset["Behavior"]["PreviousMetrics"].data + trainer_state = dataset["Behavior"]["TrainerState"].data + performance_metrics = PerformanceMetrics(output_parameters=metrics) + + stimulus_epoch = StimulusEpoch( + stimulus_start_time=acquisition_start_time, + stimulus_end_time=acquisition_end_time, + stimulus_name="GoCue", + code=Code( + url=r"https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging/tree/feat-adding-curriculum", + parameters=input_schemas["TaskLogic"].data.model_dump(), + core_dependency=Software(name="bonsai") + ), + stimulus_modalities=[StimulusModality.AUDITORY], + performance_metrics=performance_metrics, + curriculum_status = trainer_state.stage.name + ) + + + return Acquisition( + subject_id=subject_id, + instrument_id=instrument_id, + experimenters=experimenter, + acquisition_start_time=acquisition_start_time, + acquisition_end_time=acquisition_end_time, + acquisition_type="DynamicForaging", + notes=notes, + data_streams=[data_stream], + stimulus_epochs=[stimulus_epoch], + ) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/rig.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/rig.py new file mode 100644 index 00000000..132aa8f9 --- /dev/null +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/rig.py @@ -0,0 +1,173 @@ +from datetime import date +import os + +from aind_data_schema.core.instrument import Instrument +from aind_behavior_dynamic_foraging.rig import AindDynamicForagingRig +from aind_data_schema.components.devices import ( + Camera, + CameraAssembly, + CameraTarget, + HarpDevice, + MotorizedStage, + Speaker, + Computer, +) +from aind_data_schema.components.connections import Connection +from aind_data_schema.components.coordinates import CoordinateSystem +from aind_data_schema_models.modalities import Modality +from aind_data_schema_models.organizations import Organization + + +def instrument_from_dataset( + data_directory: os.PathLike, +) -> Instrument: + """ + Create acquisition model for completed session. + + Args: + data_directory (os.PathLike): + Path to the directory containing the dataset to analyze. This + directory is expected to include all required behavioral data files. + + Returns: + Acquisition: + Acquisition model for session + + Raises: + FileNotFoundError: + If the specified data directory or required files do not exist. + + ValueError: + If the dataset is malformed or missing required fields for + computing metrics. + """ + + rig = AindDynamicForagingRig.model_validate(input_schemas["Rig"].data) + + components = [] + connections = [] + + # --- Triggered cameras wrapped in CameraAssembly (required for BEHAVIOR_VIDEOS) --- + for name, cam in rig.triggered_camera_controller.cameras.items(): + camera = Camera( + name=name, + serial_number=cam.serial_number, + # manufacturer=Organization.FLIR, # TODO + # model="Blackfly S BFS-U3-16S2M", # TODO + ) + assembly = CameraAssembly( + name=f"{name}Assembly", + camera=camera, + target=CameraTarget.BODY, # TODO: adjust per camera (FACE, SIDE, etc.) + # lens=Lens(...), # TODO if needed + ) + components.append(assembly) + + # --- Monitoring cameras (optional) --- + if rig.monitoring_camera_controller: + for name, cam in rig.monitoring_camera_controller.cameras.items(): + camera = Camera( + name=name, + serial_number=getattr(cam, "serial_number", None), + # manufacturer=..., # TODO + ) + assembly = CameraAssembly( + name=f"{name}Assembly", + camera=camera, + target=CameraTarget.OTHER, # TODO: requires notes on Instrument + ) + components.append(assembly) + + # --- Harp behavior board --- + components.append( + HarpDevice( + name="BehaviorBoard", + serial_number=rig.harp_behavior.serial_number, + who_am_i=rig.harp_behavior.who_am_i, + port_name=rig.harp_behavior.port_name, + # manufacturer=Organization.HARP_TECH, # TODO + ) + ) + + # --- Harp clock generator --- + components.append( + HarpDevice( + name="ClockGenerator", + serial_number=rig.harp_clock_generator.serial_number, + who_am_i=rig.harp_clock_generator.who_am_i, + port_name=rig.harp_clock_generator.port_name, + is_clock_generator=True, + ) + ) + + # --- Harp sound card --- + components.append( + HarpDevice( + name="SoundCard", + serial_number=rig.harp_sound_card.serial_number, + who_am_i=rig.harp_sound_card.who_am_i, + port_name=rig.harp_sound_card.port_name, + ) + ) + + # --- Optional harp devices --- + if rig.harp_lickometer_left: + components.append(HarpDevice( + name="LickometerLeft", + serial_number=rig.harp_lickometer_left.serial_number, + who_am_i=rig.harp_lickometer_left.who_am_i, + port_name=rig.harp_lickometer_left.port_name, + )) + if rig.harp_lickometer_right: + components.append(HarpDevice( + name="LickometerRight", + serial_number=rig.harp_lickometer_right.serial_number, + who_am_i=rig.harp_lickometer_right.who_am_i, + port_name=rig.harp_lickometer_right.port_name, + )) + if rig.harp_sniff_detector: + components.append(HarpDevice( + name="SniffDetector", + serial_number=rig.harp_sniff_detector.serial_number, + who_am_i=rig.harp_sniff_detector.who_am_i, + port_name=rig.harp_sniff_detector.port_name, + )) + if rig.harp_environment_sensor: + components.append(HarpDevice( + name="EnvironmentSensor", + serial_number=rig.harp_environment_sensor.serial_number, + who_am_i=rig.harp_environment_sensor.who_am_i, + port_name=rig.harp_environment_sensor.port_name, + )) + + # --- Manipulator (no dedicated type, use MotorizedStage) --- + components.append( + MotorizedStage( + name="Manipulator", + serial_number=rig.manipulator.serial_number, + # manufacturer=..., # TODO + # model="AindManipulator", # TODO + ) + ) + + # --- Connections: BehaviorBoard triggers cameras --- + for name in rig.triggered_camera_controller.cameras: + connections.append(Connection( + source_device="BehaviorBoard", + target_device=name, + )) + + return Instrument( + instrument_id=instrument_id, + modification_date=date.today(), # TODO: use actual last-modified date + modalities=[Modality.BEHAVIOR_VIDEOS], # TODO: add others if applicable + coordinate_system=CoordinateSystem( + name="RigCoordinateSystem", # TODO: fill in properly + # origin=..., + # axes=..., + ), + components=components, + connections=connections, + # location="447", # TODO: room/lab location + # notes="...", # Required if any CameraTarget.OTHER is used + ) \ No newline at end of file From 441ad1113ebc6ea73d5d2723f8dd7be664e22f5c Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Mon, 30 Mar 2026 10:16:48 -0700 Subject: [PATCH 02/45] adds rig schema --- .../acquisition.py | 9 +- .../rig.py | 170 +++++++++--------- 2 files changed, 88 insertions(+), 91 deletions(-) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py index 2fc22fbc..3a2d78fb 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py @@ -12,9 +12,9 @@ Code, DataStream, DetectorConfig, + PerformanceMetrics, StimulusEpoch, StimulusModality, - PerformanceMetrics ) from aind_data_schema_models.modalities import Modality @@ -69,7 +69,7 @@ def acqusition_from_dataset( for name, camera in rig.triggered_camera_controller.cameras.items(): cam_configs.append( DetectorConfig( - device_name=name, + device_name=name, exposure_time=camera.exposure, trigger_type=TriggerType.EXTERNAL, crop_offset_x=camera.region_of_interest.x, @@ -110,14 +110,13 @@ def acqusition_from_dataset( code=Code( url=r"https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging/tree/feat-adding-curriculum", parameters=input_schemas["TaskLogic"].data.model_dump(), - core_dependency=Software(name="bonsai") + core_dependency=Software(name="bonsai"), ), stimulus_modalities=[StimulusModality.AUDITORY], performance_metrics=performance_metrics, - curriculum_status = trainer_state.stage.name + curriculum_status=trainer_state.stage.name, ) - return Acquisition( subject_id=subject_id, instrument_id=instrument_id, diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/rig.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/rig.py index 132aa8f9..8e8887cd 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/rig.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/rig.py @@ -1,19 +1,23 @@ -from datetime import date import os +from datetime import date -from aind_data_schema.core.instrument import Instrument +from aind_behavior_dynamic_foraging.data_contract import dataset as df_foraging_dataset from aind_behavior_dynamic_foraging.rig import AindDynamicForagingRig +from aind_data_schema.components.connections import Connection +from aind_data_schema.components.coordinates import Axis, AxisName, CoordinateSystem, Direction, Origin from aind_data_schema.components.devices import ( + AnatomicalRelative, Camera, CameraAssembly, CameraTarget, + DataInterface, HarpDevice, + HarpDeviceType, + Lens, MotorizedStage, - Speaker, - Computer, + SizeUnit, ) -from aind_data_schema.components.connections import Connection -from aind_data_schema.components.coordinates import CoordinateSystem +from aind_data_schema.core.instrument import Instrument from aind_data_schema_models.modalities import Modality from aind_data_schema_models.organizations import Organization @@ -41,133 +45,127 @@ def instrument_from_dataset( If the dataset is malformed or missing required fields for computing metrics. """ - + + dataset = df_foraging_dataset(data_directory) + input_schemas = dataset["Behavior"]["InputSchemas"] rig = AindDynamicForagingRig.model_validate(input_schemas["Rig"].data) - + components = [] connections = [] - # --- Triggered cameras wrapped in CameraAssembly (required for BEHAVIOR_VIDEOS) --- + # cameras for name, cam in rig.triggered_camera_controller.cameras.items(): camera = Camera( name=name, serial_number=cam.serial_number, - # manufacturer=Organization.FLIR, # TODO - # model="Blackfly S BFS-U3-16S2M", # TODO + manufacturer=Organization.SPINNAKER, + data_interface=DataInterface.COAX, ) assembly = CameraAssembly( name=f"{name}Assembly", camera=camera, - target=CameraTarget.BODY, # TODO: adjust per camera (FACE, SIDE, etc.) - # lens=Lens(...), # TODO if needed + target=CameraTarget.BODY if "Body" in name else CameraTarget.FACE, + lens=Lens(name="Lens A", manufacturer=Organization.FUJINON), + relative_position=[AnatomicalRelative.RIGHT if "Body" in name else AnatomicalRelative.SUPERIOR], ) components.append(assembly) - # --- Monitoring cameras (optional) --- - if rig.monitoring_camera_controller: - for name, cam in rig.monitoring_camera_controller.cameras.items(): - camera = Camera( - name=name, - serial_number=getattr(cam, "serial_number", None), - # manufacturer=..., # TODO - ) - assembly = CameraAssembly( - name=f"{name}Assembly", - camera=camera, - target=CameraTarget.OTHER, # TODO: requires notes on Instrument - ) - components.append(assembly) - - # --- Harp behavior board --- + # behavior board components.append( HarpDevice( name="BehaviorBoard", + harp_device_type=HarpDeviceType.BEHAVIOR, serial_number=rig.harp_behavior.serial_number, - who_am_i=rig.harp_behavior.who_am_i, - port_name=rig.harp_behavior.port_name, - # manufacturer=Organization.HARP_TECH, # TODO + manufacturer=Organization.CHAMPALIMAUD, + is_clock_generator=False, ) ) - # --- Harp clock generator --- + # clock generator components.append( HarpDevice( name="ClockGenerator", + harp_device_type=HarpDeviceType.WHITERABBIT, serial_number=rig.harp_clock_generator.serial_number, - who_am_i=rig.harp_clock_generator.who_am_i, - port_name=rig.harp_clock_generator.port_name, is_clock_generator=True, ) ) - # --- Harp sound card --- + # sound card components.append( HarpDevice( name="SoundCard", + harp_device_type=HarpDeviceType.SOUNDCARD, serial_number=rig.harp_sound_card.serial_number, - who_am_i=rig.harp_sound_card.who_am_i, - port_name=rig.harp_sound_card.port_name, + manufacturer=Organization.CHAMPALIMAUD, + is_clock_generator=False, ) ) - # --- Optional harp devices --- + # optional harp devices if rig.harp_lickometer_left: - components.append(HarpDevice( - name="LickometerLeft", - serial_number=rig.harp_lickometer_left.serial_number, - who_am_i=rig.harp_lickometer_left.who_am_i, - port_name=rig.harp_lickometer_left.port_name, - )) + components.append( + HarpDevice( + name="LickometerLeft", + harp_device_type=HarpDeviceType.LICKETYSPLIT, + serial_number=rig.harp_lickometer_left.serial_number, + is_clock_generator=False, + ) + ) if rig.harp_lickometer_right: - components.append(HarpDevice( - name="LickometerRight", - serial_number=rig.harp_lickometer_right.serial_number, - who_am_i=rig.harp_lickometer_right.who_am_i, - port_name=rig.harp_lickometer_right.port_name, - )) + components.append( + HarpDevice( + name="LickometerRight", + serial_number=rig.harp_lickometer_right.serial_number, + harp_device_type=HarpDeviceType.LICKETYSPLIT, + is_clock_generator=False, + ) + ) if rig.harp_sniff_detector: - components.append(HarpDevice( - name="SniffDetector", - serial_number=rig.harp_sniff_detector.serial_number, - who_am_i=rig.harp_sniff_detector.who_am_i, - port_name=rig.harp_sniff_detector.port_name, - )) + components.append( + HarpDevice( + name="SniffDetector", + harp_device_type=HarpDeviceType.SNIFFDETECTOR, + serial_number=rig.harp_sniff_detector.serial_number, + is_clock_generator=False, + ) + ) if rig.harp_environment_sensor: - components.append(HarpDevice( - name="EnvironmentSensor", - serial_number=rig.harp_environment_sensor.serial_number, - who_am_i=rig.harp_environment_sensor.who_am_i, - port_name=rig.harp_environment_sensor.port_name, - )) - - # --- Manipulator (no dedicated type, use MotorizedStage) --- - components.append( - MotorizedStage( - name="Manipulator", - serial_number=rig.manipulator.serial_number, - # manufacturer=..., # TODO - # model="AindManipulator", # TODO + components.append( + HarpDevice( + name="EnvironmentSensor", + harp_device_type=HarpDeviceType.ENVIRONMENTSENSOR, + serial_number=rig.harp_environment_sensor.serial_number, + is_clock_generator=False, + ) ) - ) - # --- Connections: BehaviorBoard triggers cameras --- + # manipulator + components.append(MotorizedStage(name="Manipulator", serial_number=rig.manipulator.serial_number, travel=0.0)) + + # connections for name in rig.triggered_camera_controller.cameras: - connections.append(Connection( - source_device="BehaviorBoard", - target_device=name, - )) + connections.append( + Connection( + source_device="BehaviorBoard", + target_device=name, + ) + ) return Instrument( - instrument_id=instrument_id, - modification_date=date.today(), # TODO: use actual last-modified date - modalities=[Modality.BEHAVIOR_VIDEOS], # TODO: add others if applicable + instrument_id=rig.rig_name, + modification_date=date.today(), + modalities=[Modality.BEHAVIOR, Modality.BEHAVIOR_VIDEOS], coordinate_system=CoordinateSystem( - name="RigCoordinateSystem", # TODO: fill in properly - # origin=..., - # axes=..., + name="RigCoordinateSystem", + origin=Origin.ORIGIN, + axes=[ + Axis(name=AxisName.X, direction=Direction.LR), + Axis(name=AxisName.Y, direction=Direction.FB), + Axis(name=AxisName.Z, direction=Direction.DU), + ], + axis_unit=SizeUnit.MM, ), components=components, connections=connections, - # location="447", # TODO: room/lab location - # notes="...", # Required if any CameraTarget.OTHER is used - ) \ No newline at end of file + ) From a32a842a87af7515c5d203e19aa59707b1e3fddd Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Mon, 30 Mar 2026 10:26:48 -0700 Subject: [PATCH 03/45] adds data description --- .../data_description.py | 60 +++++++++++++++++++ .../rig.py | 6 +- 2 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py new file mode 100644 index 00000000..db8952e6 --- /dev/null +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py @@ -0,0 +1,60 @@ +import os + +from aind_behavior_dynamic_foraging.data_contract import dataset as df_foraging_dataset +from aind_behavior_services.session import Session +from aind_data_schema.components.identifiers import Person +from aind_data_schema.core.data_description import DataDescription, Funding +from aind_data_schema_models.data_name_patterns import DataLevel, Group +from aind_data_schema_models.modalities import Modality +from aind_data_schema_models.organizations import Organization + + +def data_description_from_dataset( + data_directory: os.PathLike, +) -> DataDescription: + """ + Create acquisition model for completed session. + + Args: + data_directory (os.PathLike): + Path to the directory containing the dataset to analyze. This + directory is expected to include all required behavioral data files. + + Returns: + DataDescription: + DataDescription model for session + + Raises: + FileNotFoundError: + If the specified data directory or required files do not exist. + + ValueError: + If the dataset is malformed or missing required fields for + computing metrics. + """ + + dataset = df_foraging_dataset(data_directory) + software_events = dataset["Behavior"]["SoftwareEvents"] + software_events.load_all() + + input_schemas = dataset["Behavior"]["InputSchemas"] + session = Session.model_validate(input_schemas["Session"].data) + + return DataDescription( + subject_id=session.subject, + creation_time=session.date, + institution=Organization.AIND, + funding_source=[ + Funding( + funder=Organization.AI, + ) + ], + data_level=DataLevel.RAW, + investigators=[Person(name=session.experimenter[0])], + project_name="DynamicForaging", + modalities=[ + Modality.BEHAVIOR, + Modality.BEHAVIOR_VIDEOS, + ], + group=Group.BEHAVIOR, + ) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/rig.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/rig.py index 8e8887cd..0d226bb0 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/rig.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/rig.py @@ -26,7 +26,7 @@ def instrument_from_dataset( data_directory: os.PathLike, ) -> Instrument: """ - Create acquisition model for completed session. + Create Instrument model for completed session. Args: data_directory (os.PathLike): @@ -34,8 +34,8 @@ def instrument_from_dataset( directory is expected to include all required behavioral data files. Returns: - Acquisition: - Acquisition model for session + Instrument: + Instrument model for session Raises: FileNotFoundError: From 3fef59b8860b120b7bd0ce96e30142f49e243503 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Mon, 30 Mar 2026 10:56:54 -0700 Subject: [PATCH 04/45] updates __init__ --- .../aind_behavior_dynamic_foraging_metadata_mapper/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py index e69de29b..6fbccdb6 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py @@ -0,0 +1,3 @@ +from rig import instrument_from_dataset +from acquisition import acqusition_from_dataset +from data_description import data_description_from_dataset \ No newline at end of file From 062b9178dc531738f55a219494c8a6570c1539ff Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Mon, 30 Mar 2026 11:34:57 -0700 Subject: [PATCH 05/45] fixes init --- .../__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py index 6fbccdb6..0425c725 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py @@ -1,3 +1,3 @@ -from rig import instrument_from_dataset -from acquisition import acqusition_from_dataset -from data_description import data_description_from_dataset \ No newline at end of file +from .rig import instrument_from_dataset +from .acquisition import acqusition_from_dataset +from .data_description import data_description_from_dataset \ No newline at end of file From 6dbe0f0a4873c42f5610a7cb3895f9c0a880c94e Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Tue, 31 Mar 2026 13:12:56 -0700 Subject: [PATCH 06/45] adds cli for mapping --- uv.lock | 48 +++++++++++++++++++ .../pyproject.toml | 5 +- .../__init__.py | 2 +- .../acquisition.py | 11 ++++- .../data_description.py | 12 +++-- .../{rig.py => instrument.py} | 11 +++-- 6 files changed, 79 insertions(+), 10 deletions(-) rename workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/{rig.py => instrument.py} (97%) diff --git a/uv.lock b/uv.lock index d8832a09..c776f3d6 100644 --- a/uv.lock +++ b/uv.lock @@ -157,6 +157,7 @@ source = { editable = "workspace/aind_behavior_dynamic_foraging_metadata_mapper" dependencies = [ { name = "aind-behavior-dynamic-foraging" }, { name = "aind-data-schema" }, + { name = "cyclopts" }, { name = "numpy" }, { name = "pydantic-settings" }, ] @@ -180,6 +181,7 @@ docs = [ requires-dist = [ { name = "aind-behavior-dynamic-foraging", editable = "." }, { name = "aind-data-schema", specifier = ">=2.6.0" }, + { name = "cyclopts", specifier = ">=4.10.0" }, { name = "numpy", specifier = ">=2.4.2" }, { name = "pydantic-settings" }, ] @@ -375,6 +377,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/42/b9/f8d6fa329ab25128b7e98fd83a3cb34d9db5b059a9847eddb840a0af45dd/argon2_cffi_bindings-25.1.0-cp39-abi3-win_arm64.whl", hash = "sha256:b0fdbcf513833809c882823f98dc2f931cf659d9a1429616ac3adebb49f5db94", size = 27149, upload-time = "2025-07-30T10:01:59.329Z" }, ] +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + [[package]] name = "autodoc-pydantic" version = "2.2.0" @@ -915,6 +926,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, ] +[[package]] +name = "cyclopts" +version = "4.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "docstring-parser" }, + { name = "rich" }, + { name = "rich-rst" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6c/c4/2ce2ca1451487dc7d59f09334c3fa1182c46cfcf0a2d5f19f9b26d53ac74/cyclopts-4.10.1.tar.gz", hash = "sha256:ad4e4bb90576412d32276b14a76f55d43353753d16217f2c3cd5bdceba7f15a0", size = 166623, upload-time = "2026-03-23T14:43:01.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/2261922126b2e50c601fe22d7ff5194e0a4d50e654836260c0665e24d862/cyclopts-4.10.1-py3-none-any.whl", hash = "sha256:35f37257139380a386d9fe4475e1e7c87ca7795765ef4f31abba579fcfcb6ecd", size = 204331, upload-time = "2026-03-23T14:43:02.625Z" }, +] + [[package]] name = "dnspython" version = "2.8.0" @@ -924,6 +950,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" }, ] +[[package]] +name = "docstring-parser" +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, +] + [[package]] name = "docutils" version = "0.22.4" @@ -2462,6 +2497,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, ] +[[package]] +name = "rich-rst" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "rich" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/6d/a506aaa4a9eaa945ed8ab2b7347859f53593864289853c5d6d62b77246e0/rich_rst-1.3.2.tar.gz", hash = "sha256:a1196fdddf1e364b02ec68a05e8ff8f6914fee10fbca2e6b6735f166bb0da8d4", size = 14936, upload-time = "2025-10-14T16:49:45.332Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/2f/b4530fbf948867702d0a3f27de4a6aab1d156f406d72852ab902c4d04de9/rich_rst-1.3.2-py3-none-any.whl", hash = "sha256:a99b4907cbe118cf9d18b0b44de272efa61f15117c61e39ebdc431baf5df722a", size = 12567, upload-time = "2025-10-14T16:49:42.953Z" }, +] + [[package]] name = "roman-numerals" version = "4.1.0" diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml index 132e3b67..3932060f 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "pydantic-settings", "aind-behavior-dynamic-foraging==0.0.2rc24", "aind-data-schema>=2.6.0", + "cyclopts>=4.10.0" ] [tool.uv.sources] @@ -72,4 +73,6 @@ python_classes = ["Test*"] python_functions = ["test_*"] [project.scripts] -mapper = "aind_behavior_dynamic_foraging_metadata_mapper.cli:main" +acquisition = "aind_behavior_dynamic_foraging_metadata_mapper.acquisition:app" +instrument = "aind_behavior_dynamic_foraging_metadata_mapper.instrument:app" +data_description = "aind_behavior_dynamic_foraging_metadata_mapper.data_description:app" diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py index 0425c725..2a12ebe9 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py @@ -1,3 +1,3 @@ -from .rig import instrument_from_dataset +from .instrument import instrument_from_dataset from .acquisition import acqusition_from_dataset from .data_description import data_description_from_dataset \ No newline at end of file diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py index 3a2d78fb..f68a017e 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py @@ -1,5 +1,7 @@ import os +from pathlib import Path from datetime import datetime, timezone +from cyclopts import App from aind_behavior_dynamic_foraging.data_contract import dataset as df_foraging_dataset from aind_behavior_dynamic_foraging.rig import AindDynamicForagingRig @@ -18,9 +20,11 @@ ) from aind_data_schema_models.modalities import Modality +app = App() +@app.default def acqusition_from_dataset( - data_directory: os.PathLike, + data_directory: Path, ) -> Acquisition: """ Create acquisition model for completed session. @@ -117,7 +121,7 @@ def acqusition_from_dataset( curriculum_status=trainer_state.stage.name, ) - return Acquisition( + acq = Acquisition( subject_id=subject_id, instrument_id=instrument_id, experimenters=experimenter, @@ -128,3 +132,6 @@ def acqusition_from_dataset( data_streams=[data_stream], stimulus_epochs=[stimulus_epoch], ) + + print(acq) + return acq diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py index db8952e6..c0d521ce 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py @@ -1,4 +1,5 @@ -import os +from pathlib import Path +from cyclopts import App from aind_behavior_dynamic_foraging.data_contract import dataset as df_foraging_dataset from aind_behavior_services.session import Session @@ -8,9 +9,11 @@ from aind_data_schema_models.modalities import Modality from aind_data_schema_models.organizations import Organization +app = App() +@app.default def data_description_from_dataset( - data_directory: os.PathLike, + data_directory: Path, ) -> DataDescription: """ Create acquisition model for completed session. @@ -40,7 +43,7 @@ def data_description_from_dataset( input_schemas = dataset["Behavior"]["InputSchemas"] session = Session.model_validate(input_schemas["Session"].data) - return DataDescription( + data_description = DataDescription( subject_id=session.subject, creation_time=session.date, institution=Organization.AIND, @@ -58,3 +61,6 @@ def data_description_from_dataset( ], group=Group.BEHAVIOR, ) + + print(data_description) + return data_description \ No newline at end of file diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/rig.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py similarity index 97% rename from workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/rig.py rename to workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py index 0d226bb0..c91ecffd 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/rig.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py @@ -1,5 +1,6 @@ -import os +from pathlib import Path from datetime import date +from cyclopts import App from aind_behavior_dynamic_foraging.data_contract import dataset as df_foraging_dataset from aind_behavior_dynamic_foraging.rig import AindDynamicForagingRig @@ -21,9 +22,11 @@ from aind_data_schema_models.modalities import Modality from aind_data_schema_models.organizations import Organization +app = App() +@app.default def instrument_from_dataset( - data_directory: os.PathLike, + data_directory: Path, ) -> Instrument: """ Create Instrument model for completed session. @@ -152,7 +155,7 @@ def instrument_from_dataset( ) ) - return Instrument( + inst = Instrument( instrument_id=rig.rig_name, modification_date=date.today(), modalities=[Modality.BEHAVIOR, Modality.BEHAVIOR_VIDEOS], @@ -169,3 +172,5 @@ def instrument_from_dataset( components=components, connections=connections, ) + print(inst) + return inst From 79dc6f9385cccea88b8796275a67bd081e34475a Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 1 Apr 2026 08:16:59 -0700 Subject: [PATCH 07/45] refrences current metrics in mapping --- .../acquisition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py index f68a017e..b9026998 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py @@ -103,7 +103,7 @@ def acqusition_from_dataset( ) # populate behavior epoch - metrics = dataset["Behavior"]["PreviousMetrics"].data + metrics = dataset["Behavior"]["Metrics"].data trainer_state = dataset["Behavior"]["TrainerState"].data performance_metrics = PerformanceMetrics(output_parameters=metrics) From 9e031634def96a7fb74f631fbbffff7dedd0e70b Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 1 Apr 2026 08:30:35 -0700 Subject: [PATCH 08/45] dumps metrics --- .../acquisition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py index b9026998..cec218d5 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py @@ -105,7 +105,7 @@ def acqusition_from_dataset( # populate behavior epoch metrics = dataset["Behavior"]["Metrics"].data trainer_state = dataset["Behavior"]["TrainerState"].data - performance_metrics = PerformanceMetrics(output_parameters=metrics) + performance_metrics = PerformanceMetrics(output_parameters=metrics.model_dump()) stimulus_epoch = StimulusEpoch( stimulus_start_time=acquisition_start_time, From 963292f26b494e33cf51880cc43b52db43415e19 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 1 Apr 2026 08:38:41 -0700 Subject: [PATCH 09/45] prints json --- .../acquisition.py | 2 +- .../data_description.py | 2 +- .../instrument.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py index cec218d5..02c448a3 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py @@ -133,5 +133,5 @@ def acqusition_from_dataset( stimulus_epochs=[stimulus_epoch], ) - print(acq) + print(acq.model_dump_json(indent=3)) return acq diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py index c0d521ce..adb903d4 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py @@ -62,5 +62,5 @@ def data_description_from_dataset( group=Group.BEHAVIOR, ) - print(data_description) + print(data_description.model_dump_json(indent=3)) return data_description \ No newline at end of file diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py index c91ecffd..3ab6c0a7 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py @@ -172,5 +172,5 @@ def instrument_from_dataset( components=components, connections=connections, ) - print(inst) + print(inst.model_dump_json(indent=3)) return inst From 062aa2fb683a3d50eee5e3531c96b3c12a0b1946 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 1 Apr 2026 08:49:21 -0700 Subject: [PATCH 10/45] removes return --- .../acquisition.py | 1 - .../data_description.py | 1 - .../aind_behavior_dynamic_foraging_metadata_mapper/instrument.py | 1 - 3 files changed, 3 deletions(-) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py index 02c448a3..16db7d5b 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py @@ -134,4 +134,3 @@ def acqusition_from_dataset( ) print(acq.model_dump_json(indent=3)) - return acq diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py index adb903d4..f4d5b76c 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py @@ -63,4 +63,3 @@ def data_description_from_dataset( ) print(data_description.model_dump_json(indent=3)) - return data_description \ No newline at end of file diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py index 3ab6c0a7..03453fb0 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py @@ -173,4 +173,3 @@ def instrument_from_dataset( connections=connections, ) print(inst.model_dump_json(indent=3)) - return inst From 1f5a2fc589ce94e5a7e75b8de22918bf8df23df0 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 1 Apr 2026 09:33:51 -0700 Subject: [PATCH 11/45] lints --- .../data_contract/_dataset.py | 2 +- .../__init__.py | 10 ++++++++-- .../acquisition.py | 7 ++++--- .../data_description.py | 3 ++- .../instrument.py | 7 ++++--- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/aind_behavior_dynamic_foraging/data_contract/_dataset.py b/src/aind_behavior_dynamic_foraging/data_contract/_dataset.py index 4bba0ad2..b88f99b5 100644 --- a/src/aind_behavior_dynamic_foraging/data_contract/_dataset.py +++ b/src/aind_behavior_dynamic_foraging/data_contract/_dataset.py @@ -1,6 +1,6 @@ from pathlib import Path -from aind_behavior_curriculum import TrainerState, Metrics +from aind_behavior_curriculum import Metrics, TrainerState from aind_behavior_services.session import Session from contraqctor.contract import Dataset, DataStreamCollection from contraqctor.contract.camera import Camera diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py index 2a12ebe9..6cc6967a 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py @@ -1,3 +1,9 @@ -from .instrument import instrument_from_dataset from .acquisition import acqusition_from_dataset -from .data_description import data_description_from_dataset \ No newline at end of file +from .data_description import data_description_from_dataset +from .instrument import instrument_from_dataset + +__all__ = [ + "acqusition_from_dataset", + "data_description_from_dataset", + "instrument_from_dataset", +] diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py index 16db7d5b..fa663184 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py @@ -1,7 +1,6 @@ import os -from pathlib import Path from datetime import datetime, timezone -from cyclopts import App +from pathlib import Path from aind_behavior_dynamic_foraging.data_contract import dataset as df_foraging_dataset from aind_behavior_dynamic_foraging.rig import AindDynamicForagingRig @@ -19,9 +18,11 @@ StimulusModality, ) from aind_data_schema_models.modalities import Modality +from cyclopts import App app = App() + @app.default def acqusition_from_dataset( data_directory: Path, @@ -121,7 +122,7 @@ def acqusition_from_dataset( curriculum_status=trainer_state.stage.name, ) - acq = Acquisition( + acq = Acquisition( subject_id=subject_id, instrument_id=instrument_id, experimenters=experimenter, diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py index f4d5b76c..13fd7edd 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py @@ -1,5 +1,4 @@ from pathlib import Path -from cyclopts import App from aind_behavior_dynamic_foraging.data_contract import dataset as df_foraging_dataset from aind_behavior_services.session import Session @@ -8,9 +7,11 @@ from aind_data_schema_models.data_name_patterns import DataLevel, Group from aind_data_schema_models.modalities import Modality from aind_data_schema_models.organizations import Organization +from cyclopts import App app = App() + @app.default def data_description_from_dataset( data_directory: Path, diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py index 03453fb0..8f25c05d 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py @@ -1,6 +1,5 @@ -from pathlib import Path from datetime import date -from cyclopts import App +from pathlib import Path from aind_behavior_dynamic_foraging.data_contract import dataset as df_foraging_dataset from aind_behavior_dynamic_foraging.rig import AindDynamicForagingRig @@ -21,9 +20,11 @@ from aind_data_schema.core.instrument import Instrument from aind_data_schema_models.modalities import Modality from aind_data_schema_models.organizations import Organization +from cyclopts import App app = App() + @app.default def instrument_from_dataset( data_directory: Path, @@ -155,7 +156,7 @@ def instrument_from_dataset( ) ) - inst = Instrument( + inst = Instrument( instrument_id=rig.rig_name, modification_date=date.today(), modalities=[Modality.BEHAVIOR, Modality.BEHAVIOR_VIDEOS], From ba96b5f8798280f9a3fde1bb5a84d783a5d266dc Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Thu, 2 Apr 2026 07:47:15 -0700 Subject: [PATCH 12/45] adds logging --- scripts/walk_through_session.py | 8 ++++---- src/Extensions/bonsai.py | 2 ++ .../trial_generators/coupled_trial_generator.py | 7 +++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/scripts/walk_through_session.py b/scripts/walk_through_session.py index f6288ac5..d460a6d4 100644 --- a/scripts/walk_through_session.py +++ b/scripts/walk_through_session.py @@ -2,7 +2,7 @@ import os from aind_behavior_dynamic_foraging.data_contract import dataset as df_foraging_dataset -from aind_behavior_dynamic_foraging.task_logic.trial_generators.warmup_trial_generator import WarmupTrialGeneratorSpec +from aind_behavior_dynamic_foraging.task_logic.trial_generators import WarmupTrialGeneratorSpec, CoupledTrialGeneratorSpec from aind_behavior_dynamic_foraging.task_logic.trial_models import TrialOutcome logging.basicConfig( @@ -17,10 +17,10 @@ def walk_through_session(data_directory: os.PathLike): software_events.load_all() trial_outcomes = software_events["TrialOutcome"].data["data"].iloc - warmup_trial_generator = WarmupTrialGeneratorSpec().create_generator() + trial_generator = CoupledTrialGeneratorSpec().create_generator() for i, outcome in enumerate(trial_outcomes): - warmup_trial_generator.update(TrialOutcome.model_validate(outcome)) - trial = warmup_trial_generator.next() + trial_generator.update(TrialOutcome.model_validate(outcome)) + trial = trial_generator.next() if not trial: print(f"Session finished at trial {i}") diff --git a/src/Extensions/bonsai.py b/src/Extensions/bonsai.py index 5fab0e0f..e3d80618 100644 --- a/src/Extensions/bonsai.py +++ b/src/Extensions/bonsai.py @@ -1,4 +1,5 @@ from typing import TYPE_CHECKING +import logging from pydantic import TypeAdapter @@ -7,6 +8,7 @@ if TYPE_CHECKING: from aind_behavior_dynamic_foraging.task_logic.trial_generators._base import ITrialGenerator +logging.basicConfig() def resolve_generator(spec: TrialGeneratorSpec | str) -> "ITrialGenerator": """Resolves and creates the trial generator instance based on the task logic's trial generator model.""" diff --git a/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/coupled_trial_generator.py b/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/coupled_trial_generator.py index 93c37dbc..b098669a 100644 --- a/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/coupled_trial_generator.py +++ b/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/coupled_trial_generator.py @@ -134,6 +134,13 @@ def _are_end_conditions_met(self) -> bool: logger.debug("Maximum trial count exceeded.") return True + logger.debug( + "Trial generation end conditions are not met: " + f"total trials={len(self.is_right_choice_history)}, " + f"time elapsed={time_elapsed}," + f"ignored trial={choice_history[-win:].count(None)}," + ) + return False def update(self, outcome: TrialOutcome | str) -> None: From 2f0a44615ff3f20e1ed28fa8fa0f94800aef402a Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Thu, 2 Apr 2026 08:00:24 -0700 Subject: [PATCH 13/45] converts end condition time to seconds --- .../coupled_baiting/stages.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/stages.py b/workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/stages.py index 66ee583e..6050287d 100644 --- a/workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/stages.py +++ b/workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/stages.py @@ -66,8 +66,8 @@ def make_s_stage_1_warmup(): CoupledTrialGeneratorSpec( trial_generation_end_parameters=CoupledTrialGenerationEndConditions( max_trial=1000, - max_time=75, - min_time=30, + max_time=4500, + min_time=1800, ignore_win=20000, ignore_ratio_threshold=1, ), @@ -113,8 +113,8 @@ def make_s_stage_1(): trial_generator=CoupledTrialGeneratorSpec( trial_generation_end_parameters=CoupledTrialGenerationEndConditions( max_trial=1000, - max_time=75, - min_time=30, + max_time=4500, + min_time=1800, ignore_win=20000, ignore_ratio_threshold=1, ), @@ -158,8 +158,8 @@ def make_s_stage_2(): trial_generator=CoupledTrialGeneratorSpec( trial_generation_end_parameters=CoupledTrialGenerationEndConditions( max_trial=1000, - max_time=75, - min_time=30, + max_time=4500, + min_time=1800, ignore_win=30, ignore_ratio_threshold=0.83, ), @@ -203,8 +203,8 @@ def make_s_stage_3(): trial_generator=CoupledTrialGeneratorSpec( trial_generation_end_parameters=CoupledTrialGenerationEndConditions( max_trial=1000, - max_time=75, - min_time=30, + max_time=4500, + min_time=1800, ignore_win=30, ignore_ratio_threshold=0.83, ), @@ -248,8 +248,8 @@ def make_s_stage_final(): trial_generator=CoupledTrialGeneratorSpec( trial_generation_end_parameters=CoupledTrialGenerationEndConditions( max_trial=1000, - max_time=75, - min_time=30, + max_time=4500, + min_time=1800, ignore_win=30, ignore_ratio_threshold=0.83, ), @@ -289,8 +289,8 @@ def make_s_stage_graduated(): trial_generator=CoupledTrialGeneratorSpec( trial_generation_end_parameters=CoupledTrialGenerationEndConditions( max_trial=1000, - max_time=75, - min_time=30, + max_time=4500, + min_time=1800, ignore_win=30, ignore_ratio_threshold=0.83, ), From 382ae85884e0b1658330c6fbe68f59eca32595eb Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Thu, 2 Apr 2026 08:01:32 -0700 Subject: [PATCH 14/45] streams logs to stdout --- src/Extensions/bonsai.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Extensions/bonsai.py b/src/Extensions/bonsai.py index e3d80618..7d5cae55 100644 --- a/src/Extensions/bonsai.py +++ b/src/Extensions/bonsai.py @@ -1,5 +1,6 @@ from typing import TYPE_CHECKING import logging +import sys from pydantic import TypeAdapter @@ -8,7 +9,7 @@ if TYPE_CHECKING: from aind_behavior_dynamic_foraging.task_logic.trial_generators._base import ITrialGenerator -logging.basicConfig() +logging.basicConfig(stream=sys.stdout) def resolve_generator(spec: TrialGeneratorSpec | str) -> "ITrialGenerator": """Resolves and creates the trial generator instance based on the task logic's trial generator model.""" From f160fbdf6b3cabf9b4f9a4e4840000327eda5c44 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Thu, 2 Apr 2026 08:18:46 -0700 Subject: [PATCH 15/45] moves logging before import --- scripts/walk_through_session.py | 4 +++- src/Extensions/bonsai.py | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/scripts/walk_through_session.py b/scripts/walk_through_session.py index d460a6d4..fae67eec 100644 --- a/scripts/walk_through_session.py +++ b/scripts/walk_through_session.py @@ -2,7 +2,9 @@ import os from aind_behavior_dynamic_foraging.data_contract import dataset as df_foraging_dataset -from aind_behavior_dynamic_foraging.task_logic.trial_generators import WarmupTrialGeneratorSpec, CoupledTrialGeneratorSpec +from aind_behavior_dynamic_foraging.task_logic.trial_generators import ( + CoupledTrialGeneratorSpec, +) from aind_behavior_dynamic_foraging.task_logic.trial_models import TrialOutcome logging.basicConfig( diff --git a/src/Extensions/bonsai.py b/src/Extensions/bonsai.py index 7d5cae55..2009618b 100644 --- a/src/Extensions/bonsai.py +++ b/src/Extensions/bonsai.py @@ -1,15 +1,16 @@ -from typing import TYPE_CHECKING import logging import sys +from typing import TYPE_CHECKING from pydantic import TypeAdapter -from aind_behavior_dynamic_foraging.task_logic import TrialGeneratorSpec +logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) + +from aind_behavior_dynamic_foraging.task_logic import TrialGeneratorSpec # noqa if TYPE_CHECKING: from aind_behavior_dynamic_foraging.task_logic.trial_generators._base import ITrialGenerator -logging.basicConfig(stream=sys.stdout) def resolve_generator(spec: TrialGeneratorSpec | str) -> "ITrialGenerator": """Resolves and creates the trial generator instance based on the task logic's trial generator model.""" From f1fff6d5c9901806231affc0e16013dd81cfd98f Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Thu, 2 Apr 2026 14:18:11 -0700 Subject: [PATCH 16/45] log fixes --- .../trial_generators/block_based_trial_generator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/block_based_trial_generator.py b/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/block_based_trial_generator.py index 0112c955..809bb837 100644 --- a/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/block_based_trial_generator.py +++ b/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/block_based_trial_generator.py @@ -168,7 +168,7 @@ def next(self) -> Trial | None: p_reward_left = 1 if is_left_baited else p_reward_left is_right_baited = self.block.p_right_reward > random_numbers[1] or self.is_right_baited - logger.debug(f"Right baited: {is_left_baited}") + logger.debug(f"Right baited: {is_right_baited}") p_reward_right = 1 if is_right_baited else p_reward_right return Trial( @@ -195,7 +195,7 @@ def _generate_next_block( reward_pairs: list[list[float, float]], base_reward_sum: float, block_len: Union[UniformDistribution, ExponentialDistribution], - current_block: Optional[None] = None, + current_block: Optional[Block] = None, ) -> Block: """Generates the next block, avoiding repeating the current block's side bias. From f7502b139b65d9a0c66ec672fb87548b41e0c55f Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Tue, 7 Apr 2026 10:35:56 -0700 Subject: [PATCH 17/45] push fixes for baiting --- .../trial_generators/block_based_trial_generator.py | 12 ++++++------ .../metrics.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/block_based_trial_generator.py b/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/block_based_trial_generator.py index 809bb837..3d37de9e 100644 --- a/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/block_based_trial_generator.py +++ b/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/block_based_trial_generator.py @@ -163,13 +163,13 @@ def next(self) -> Trial | None: if self.spec.is_baiting: random_numbers = np.random.random(2) - is_left_baited = self.block.p_left_reward > random_numbers[0] or self.is_left_baited - logger.debug(f"Left baited: {is_left_baited}") - p_reward_left = 1 if is_left_baited else p_reward_left + self.is_left_baited = self.block.p_left_reward > random_numbers[0] or self.is_left_baited + logger.debug(f"Left baited: {self.is_left_baited}") + p_reward_left = 1 if self.is_left_baited else p_reward_left - is_right_baited = self.block.p_right_reward > random_numbers[1] or self.is_right_baited - logger.debug(f"Right baited: {is_right_baited}") - p_reward_right = 1 if is_right_baited else p_reward_right + self.is_right_baited = self.block.p_right_reward > random_numbers[1] or self.is_right_baited + logger.debug(f"Right baited: {self.is_right_baited}") + p_reward_right = 1 if self.is_right_baited else p_reward_right return Trial( p_reward_left=p_reward_left, diff --git a/workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/metrics.py b/workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/metrics.py index 995d81a8..942926fc 100644 --- a/workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/metrics.py +++ b/workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/metrics.py @@ -138,7 +138,7 @@ def compute_foraging_efficiency( if not is_baiting: logger.debug("Calculated non baiting foraging efficiency.") - optimal_rewards_per_session = np.nanmean(np.max([p_right_reward], axis=0)) * len(p_left_reward) + optimal_rewards_per_session = np.nanmean(np.max([p_right_reward, p_left_reward], axis=0)) * len(p_left_reward) else: logger.debug("Calculated baiting foraging efficiency.") p_max = np.maximum(p_left_reward, p_right_reward) From 37f9c84bdc72f0d0e0f33284b9311d3c16905849 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 8 Apr 2026 10:25:01 -0700 Subject: [PATCH 18/45] add cli --- .../data_contract/utils.py | 34 +++ .../acquisition.py | 228 ++++++++++++------ .../cli.py | 45 ++++ .../data_description.py | 3 +- .../instrument.py | 3 +- 5 files changed, 241 insertions(+), 72 deletions(-) create mode 100644 src/aind_behavior_dynamic_foraging/data_contract/utils.py create mode 100644 workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py diff --git a/src/aind_behavior_dynamic_foraging/data_contract/utils.py b/src/aind_behavior_dynamic_foraging/data_contract/utils.py new file mode 100644 index 00000000..ab7819d8 --- /dev/null +++ b/src/aind_behavior_dynamic_foraging/data_contract/utils.py @@ -0,0 +1,34 @@ +import os +from typing import Optional + +from aind_behavior_dynamic_foraging.data_contract import dataset +from aind_behavior_dynamic_foraging.task_logic import AindDynamicForagingTaskLogic + + +def calculate_consumed_water(session_path: os.PathLike) -> Optional[float]: + """Calculate the total volume of water consumed during a session. + + Args: + session_path (os.PathLike): Path to the session directory. + + Returns: + Optional[float]: Total volume of water consumed in milliliters, or None if unavailable. + """ + + trial_outcomes = dataset(session_path)["Behavior"]["SoftwareEvents"]["TrialOutcome"].load().data["data"] + is_right_choice = [to["is_right_choice"] for to in trial_outcomes] + is_rewarded = [to["is_rewarded"] for to in trial_outcomes] + + task_logic_data = dataset(session_path)["Behavior"]["InputSchemas"]["TaskLogic"].load().data + task_logic = AindDynamicForagingTaskLogic.model_validate(task_logic_data) + right_reward_size = task_logic.task_parameters.reward_size.right_value_volume + left_reward_size = task_logic.task_parameters.reward_size.left_value_volume + + total = 0 + for choice, rewarded in zip(is_right_choice, is_rewarded): + if rewarded: + if choice is True: + total += right_reward_size * 1e-3 + if choice is False: + total += left_reward_size * 1e-3 + return total diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py index fa663184..25ab9366 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py @@ -1,31 +1,48 @@ +import logging import os +import sys from datetime import datetime, timezone from pathlib import Path +from typing import List, Optional +import git from aind_behavior_dynamic_foraging.data_contract import dataset as df_foraging_dataset +from aind_behavior_dynamic_foraging.data_contract.utils import calculate_consumed_water from aind_behavior_dynamic_foraging.rig import AindDynamicForagingRig +from aind_behavior_dynamic_foraging.task_logic import AindDynamicForagingTaskLogic +from aind_behavior_services.rig import Device as AbsDevice +from aind_behavior_services.rig import cameras as abs_camera +from aind_behavior_services.rig import water_valve as abs_water_valve from aind_behavior_services.session import Session +from aind_behavior_services.utils import get_fields_of_type, utcnow from aind_data_schema.components.configs import TriggerType from aind_data_schema.components.connections import Connection from aind_data_schema.components.identifiers import Software +from aind_data_schema.components.measurements import CalibrationFit, FitType, GenericModel, VolumeCalibration from aind_data_schema.core.acquisition import ( Acquisition, + AcquisitionSubjectDetails, Code, DataStream, DetectorConfig, + GenericModel, PerformanceMetrics, StimulusEpoch, StimulusModality, ) +from aind_data_schema_models import units from aind_data_schema_models.modalities import Modality +from clabe.data_mapper import helpers as data_mapper_helpers from cyclopts import App +logger = logging.getLogger(__name__) + app = App() @app.default def acqusition_from_dataset( - data_directory: Path, + data_directory: Path, repo_path: os.PathLike, end_time: Optional[datetime] = None ) -> Acquisition: """ Create acquisition model for completed session. @@ -35,6 +52,12 @@ def acqusition_from_dataset( Path to the directory containing the dataset to analyze. This directory is expected to include all required behavioral data files. + repo_path (os.PathLike): + Path to github repository. + + end_time: Optional[datetime]: + End time of acquisition. If None, current time will be used. + Returns: Acquisition: Acquisition model for session @@ -47,61 +70,46 @@ def acqusition_from_dataset( If the dataset is malformed or missing required fields for computing metrics. """ - dataset = df_foraging_dataset(data_directory) - software_events = dataset["Behavior"]["SoftwareEvents"] - software_events.load_all() - input_schemas = dataset["Behavior"]["InputSchemas"] - - # extract info from session model - session = Session.model_validate(input_schemas["Session"].data) - acquisition_start_time = session.date - subject_id = session.subject - experimenter = session.experimenter - notes = session.notes - - # extract info from rig model - rig = AindDynamicForagingRig.model_validate(input_schemas["Rig"].data) - - instrument_id = os.getenv("aibs_comp_id", "unknown") - acquisition_end_time = datetime.now(tz=timezone.utc) - - # populate camera data stream - cam_configs = [] - active_devices = ["BehaviorBoard"] - connections = [] - for name, camera in rig.triggered_camera_controller.cameras.items(): - cam_configs.append( - DetectorConfig( - device_name=name, - exposure_time=camera.exposure, - trigger_type=TriggerType.EXTERNAL, - crop_offset_x=camera.region_of_interest.x, - crop_offset_y=camera.region_of_interest.y, - crop_width=camera.region_of_interest.width, - crop_height=camera.region_of_interest.height, - ) + session_model = Session.model_validate(input_schemas["Session"].data) + rig_model = AindDynamicForagingRig.model_validate(input_schemas["Rig"].data) + task_logic_model = AindDynamicForagingTaskLogic.model_validate(input_schemas["TaskLogic"].data) + repository = git.Repo(repo_path) + + if end_time is None: + logger.warning("Session end time is not set. Using current time as end time.") + acquisition_end_time = datetime.now(tz=timezone.utc) + + bonsai_code = _get_bonsai_as_code(repository) + python_code = _get_python_as_code(repository) + + cameras = data_mapper_helpers.get_cameras(rig_model, exclude_without_video_writer=True) + camera_configs = [_get_cameras_config(k, v, repository) for k, v in cameras.items()] + + # construct data stream + modalities: list[Modality] = [getattr(Modality, "BEHAVIOR")] + if len(camera_configs) > 0: + modalities.append(getattr(Modality, "BEHAVIOR_VIDEOS")) + modalities = list(set(modalities)) + + active_devices = [ + _device[0] + for _device in get_fields_of_type(rig_model, AbsDevice, stop_recursion_on_type=False) + if _device[0] is not None and not isinstance(_device[1], abs_camera.CameraController) + ] + + data_streams = [ + DataStream( + stream_start_time=session_model.date, + stream_end_time=acquisition_end_time, + code=[bonsai_code, python_code], + active_devices=active_devices, + modalities=modalities, + configurations=camera_configs, + notes=session_model.notes, ) - # TODO: compression - active_devices.append(name) - connections.append(Connection(source_device="BehaviorBoard", target_device=name)) - - data_stream = DataStream( - stream_start_time=acquisition_start_time, - stream_end_time=acquisition_end_time, - modalities=[Modality.BEHAVIOR_VIDEOS], - code=[ - Code( - url=r"https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging/blob/feat-adding-curriculum/src/aind_behavior_dynamic_foraging/rig.py", - parameters=rig.model_dump(), - core_dependency=Software(name="bonsai"), - ) - ], - active_devices=active_devices, - configurations=cam_configs, - connections=connections, - ) + ] # populate behavior epoch metrics = dataset["Behavior"]["Metrics"].data @@ -109,29 +117,113 @@ def acqusition_from_dataset( performance_metrics = PerformanceMetrics(output_parameters=metrics.model_dump()) stimulus_epoch = StimulusEpoch( - stimulus_start_time=acquisition_start_time, + stimulus_start_time=session_model.date, stimulus_end_time=acquisition_end_time, stimulus_name="GoCue", - code=Code( - url=r"https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging/tree/feat-adding-curriculum", - parameters=input_schemas["TaskLogic"].data.model_dump(), - core_dependency=Software(name="bonsai"), - ), + code=bonsai_code, stimulus_modalities=[StimulusModality.AUDITORY], performance_metrics=performance_metrics, curriculum_status=trainer_state.stage.name, ) - acq = Acquisition( - subject_id=subject_id, - instrument_id=instrument_id, - experimenters=experimenter, - acquisition_start_time=acquisition_start_time, + # Construct aind-data-schema session + return Acquisition( + subject_id=session_model.subject, + subject_details=_get_subject_details(data_directory), + instrument_id=rig_model.rig_name, acquisition_end_time=acquisition_end_time, - acquisition_type="DynamicForaging", - notes=notes, - data_streams=[data_stream], + acquisition_start_time=session_model.date, + experimenters=session_model.experimenter, + acquisition_type=session_model.experiment or task_logic_model.name, + coordinate_system=None, + data_streams=data_streams, + calibrations=_get_water_calibration(rig_model), stimulus_epochs=[stimulus_epoch], ) - print(acq.model_dump_json(indent=3)) + +def _get_subject_details(data_directory: os.PathLike) -> AcquisitionSubjectDetails: + return AcquisitionSubjectDetails( + mouse_platform_name="tube", + reward_consumed_total=calculate_consumed_water(data_directory), + reward_consumed_unit=units.VolumeUnit.ML, + ) + + +def _get_water_calibration(rig_model: AindDynamicForagingRig) -> List[VolumeCalibration]: + + water_calibrations = get_fields_of_type(rig_model, abs_water_valve.WaterValveCalibration) + vol_cal = [] + for device_name, water_calibration in water_calibrations: + c = water_calibration + vol_cal.append( + VolumeCalibration( + device_name=device_name, + calibration_date=water_calibration.date if water_calibration.date else utcnow(), + input=list(c.interval_average.keys()), + output=list(c.interval_average.values()), + input_unit=units.TimeUnit.S, + output_unit=units.VolumeUnit.ML, + fit=CalibrationFit( + fit_type=FitType.LINEAR, + fit_parameters=GenericModel.model_validate(c.model_dump()), + ), + ) + ) + return vol_cal + + +def _get_cameras_config(name: str, camera: abs_camera.CameraTypes, repository: git.Repo) -> List[DetectorConfig]: + + if isinstance(camera.video_writer, abs_camera.VideoWriterFfmpeg): + compression = Code( + url="https://ffmpeg.org/", + name="FFMPEG", + parameters=GenericModel.model_validate(camera.video_writer.model_dump()), + ) + elif isinstance(camera.video_writer, abs_camera.VideoWriterOpenCv): + bonsai = _get_bonsai_as_code(repository) + bonsai.parameters = GenericModel.model_validate(camera.video_writer.model_dump()) + compression = bonsai + else: + raise ValueError("Camera does not have a valid video writer configured.") + + camera = DetectorConfig( + device_name=name, + exposure_time=getattr(camera, "exposure", -1), + exposure_time_unit=units.TimeUnit.US, + trigger_type=TriggerType.EXTERNAL, + compression=compression(camera.video_writer), + ) + + cameras = data_mapper_helpers.get_cameras(AindDynamicForagingTaskLogic, exclude_without_video_writer=True) + + return list(map(camera, cameras.keys(), cameras.values())) + +def _get_bonsai_as_code(repository: git.Repo) -> Code: + bonsai_folder = Path(Path(repository.working_tree_dir) / "bonsai" / "bonsai.exe").parent + bonsai_env = data_mapper_helpers.snapshot_bonsai_environment(bonsai_folder / "bonsai.config") + bonsai_version = bonsai_env.get("Bonsai", "unknown") + assert isinstance(repository, git.Repo) + + return Code( + url=repository.remote().url, + name="Aind.Behavior.DynamicForaging", + version=repository.head.commit.hexsha, + language="Bonsai", + language_version=bonsai_version, + ) + + +def _get_python_as_code(repository: git.Repo) -> Code: + v = sys.version_info + semver = f"{v.major}.{v.minor}.{v.micro}" + if v.releaselevel != "final": + semver += f"-{v.releaselevel}.{v.serial}" + return Code( + url=repository.remote().url, + name="aind-behavior-vr-foraging", + version=repository.head.commit.hexsha, + language="Python", + language_version=semver, + ) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py new file mode 100644 index 00000000..0cfa5ba1 --- /dev/null +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py @@ -0,0 +1,45 @@ +import logging +import os +import typing as t +from pathlib import Path + +from pydantic import AwareDatetime, Field +from pydantic_settings import BaseSettings + +logger = logging.getLogger(__name__) + + +class DataMapperCli(BaseSettings, cli_kebab_case=True): + data_path: os.PathLike = Field(description="Path to the session data directory.") + repo_path: os.PathLike = Field( + default=Path("."), description="Path to the repository. By default it will use the current directory." + ) + session_end_time: AwareDatetime | None = Field( + default=None, + description="End time of the session in ISO format. If not provided, will use the time the data mapping is run.", + ) + suffix: t.Optional[str] = Field(default="dynamicforaging", description="Suffix to append to the output filenames.") + + def cli_cmd(self): + """Generate aind-data-schema metadata for the Dynamic Foraging dataset located at the specified path.""" + from .acquisition import acqusition_from_dataset + from .instrument import instrument_from_dataset + from .data_description import data_description_from_dataset + + acquisition = acqusition_from_dataset( + data_directory=Path(self.data_path), + repo_path=Path(self.repo_path), + end_time=self.session_end_time, + ) + + instrument = instrument_from_dataset(data_directory=Path(self.data_path)) + data_description = data_description_from_dataset(data_directory=Path(self.data_path)) + + acquisition.write_standard_file(output_directory=Path(self.data_path), filename_suffix=self.suffix) + instrument.write_standard_file(output_directory=Path(self.data_path), filename_suffix=self.suffix) + data_description.write_standard_file(output_directory=Path(self.data_path), filename_suffix=self.suffix) + + logger.info( + "Mapping completed! Saved acquisition.json, instrument.json, data_description.json to %s", + self.data_path, + ) \ No newline at end of file diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py index 13fd7edd..fe5a4ce0 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py @@ -44,7 +44,7 @@ def data_description_from_dataset( input_schemas = dataset["Behavior"]["InputSchemas"] session = Session.model_validate(input_schemas["Session"].data) - data_description = DataDescription( + return DataDescription( subject_id=session.subject, creation_time=session.date, institution=Organization.AIND, @@ -63,4 +63,3 @@ def data_description_from_dataset( group=Group.BEHAVIOR, ) - print(data_description.model_dump_json(indent=3)) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py index 8f25c05d..bb4d4f05 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py @@ -156,7 +156,7 @@ def instrument_from_dataset( ) ) - inst = Instrument( + return Instrument( instrument_id=rig.rig_name, modification_date=date.today(), modalities=[Modality.BEHAVIOR, Modality.BEHAVIOR_VIDEOS], @@ -173,4 +173,3 @@ def instrument_from_dataset( components=components, connections=connections, ) - print(inst.model_dump_json(indent=3)) From 690982db57107954fe587884a46153aa12a41db1 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 8 Apr 2026 10:39:41 -0700 Subject: [PATCH 19/45] adds project script --- .../pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml index 3932060f..88bf85e8 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml @@ -76,3 +76,4 @@ python_functions = ["test_*"] acquisition = "aind_behavior_dynamic_foraging_metadata_mapper.acquisition:app" instrument = "aind_behavior_dynamic_foraging_metadata_mapper.instrument:app" data_description = "aind_behavior_dynamic_foraging_metadata_mapper.data_description:app" +mapper = "aind_behavior_dynamic_foraging_metadata_mapper.cli:main" From c9ee84ac87e68362e99f501652ef2145625f2bfb Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 8 Apr 2026 10:41:36 -0700 Subject: [PATCH 20/45] updates args --- .../cli.py | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py index 0cfa5ba1..f8b64d58 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py @@ -4,13 +4,13 @@ from pathlib import Path from pydantic import AwareDatetime, Field -from pydantic_settings import BaseSettings +from pydantic_settings import BaseSettings, CliApp logger = logging.getLogger(__name__) class DataMapperCli(BaseSettings, cli_kebab_case=True): - data_path: os.PathLike = Field(description="Path to the session data directory.") + data_directory: os.PathLike = Field(description="Path to the session data directory.") repo_path: os.PathLike = Field( default=Path("."), description="Path to the repository. By default it will use the current directory." ) @@ -27,19 +27,26 @@ def cli_cmd(self): from .data_description import data_description_from_dataset acquisition = acqusition_from_dataset( - data_directory=Path(self.data_path), + data_directory=Path(self.data_directory), repo_path=Path(self.repo_path), end_time=self.session_end_time, ) - instrument = instrument_from_dataset(data_directory=Path(self.data_path)) - data_description = data_description_from_dataset(data_directory=Path(self.data_path)) + instrument = instrument_from_dataset(data_directory=Path(self.data_directory)) + data_description = data_description_from_dataset(data_directory=Path(self.data_directory)) - acquisition.write_standard_file(output_directory=Path(self.data_path), filename_suffix=self.suffix) - instrument.write_standard_file(output_directory=Path(self.data_path), filename_suffix=self.suffix) - data_description.write_standard_file(output_directory=Path(self.data_path), filename_suffix=self.suffix) + acquisition.write_standard_file(output_directory=Path(self.data_directory), filename_suffix=self.suffix) + instrument.write_standard_file(output_directory=Path(self.data_directory), filename_suffix=self.suffix) + data_description.write_standard_file(output_directory=Path(self.data_directory), filename_suffix=self.suffix) logger.info( "Mapping completed! Saved acquisition.json, instrument.json, data_description.json to %s", - self.data_path, - ) \ No newline at end of file + self.data_directory, + ) + +def main(): + CliApp.run(DataMapperCli) + + +if __name__ == "__main__": + main() \ No newline at end of file From e68fe44d98402722913bfbe501ba199952558be0 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 8 Apr 2026 13:11:31 -0700 Subject: [PATCH 21/45] removes suffix --- .../src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py index f8b64d58..fd2ffaeb 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py @@ -18,7 +18,7 @@ class DataMapperCli(BaseSettings, cli_kebab_case=True): default=None, description="End time of the session in ISO format. If not provided, will use the time the data mapping is run.", ) - suffix: t.Optional[str] = Field(default="dynamicforaging", description="Suffix to append to the output filenames.") + suffix: t.Optional[str] = Field(default="", description="Suffix to append to the output filenames.") def cli_cmd(self): """Generate aind-data-schema metadata for the Dynamic Foraging dataset located at the specified path.""" From a107aa8536276c1f1fe2d1fdd415c5a74f9c8aae Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Thu, 9 Apr 2026 07:48:33 -0700 Subject: [PATCH 22/45] lints --- .../acquisition.py | 4 +--- .../aind_behavior_dynamic_foraging_metadata_mapper/cli.py | 7 ++++--- .../data_description.py | 1 - 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py index 25ab9366..64eb3dd8 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py @@ -16,8 +16,6 @@ from aind_behavior_services.session import Session from aind_behavior_services.utils import get_fields_of_type, utcnow from aind_data_schema.components.configs import TriggerType -from aind_data_schema.components.connections import Connection -from aind_data_schema.components.identifiers import Software from aind_data_schema.components.measurements import CalibrationFit, FitType, GenericModel, VolumeCalibration from aind_data_schema.core.acquisition import ( Acquisition, @@ -25,7 +23,6 @@ Code, DataStream, DetectorConfig, - GenericModel, PerformanceMetrics, StimulusEpoch, StimulusModality, @@ -200,6 +197,7 @@ def _get_cameras_config(name: str, camera: abs_camera.CameraTypes, repository: g return list(map(camera, cameras.keys(), cameras.values())) + def _get_bonsai_as_code(repository: git.Repo) -> Code: bonsai_folder = Path(Path(repository.working_tree_dir) / "bonsai" / "bonsai.exe").parent bonsai_env = data_mapper_helpers.snapshot_bonsai_environment(bonsai_folder / "bonsai.config") diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py index fd2ffaeb..04f3bf28 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py @@ -23,8 +23,8 @@ class DataMapperCli(BaseSettings, cli_kebab_case=True): def cli_cmd(self): """Generate aind-data-schema metadata for the Dynamic Foraging dataset located at the specified path.""" from .acquisition import acqusition_from_dataset - from .instrument import instrument_from_dataset from .data_description import data_description_from_dataset + from .instrument import instrument_from_dataset acquisition = acqusition_from_dataset( data_directory=Path(self.data_directory), @@ -43,10 +43,11 @@ def cli_cmd(self): "Mapping completed! Saved acquisition.json, instrument.json, data_description.json to %s", self.data_directory, ) - + + def main(): CliApp.run(DataMapperCli) if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py index fe5a4ce0..64ffd278 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py @@ -62,4 +62,3 @@ def data_description_from_dataset( ], group=Group.BEHAVIOR, ) - From 6ef9ed33ae48d3a790305e6d1013df1f330ed99c Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Mon, 13 Apr 2026 12:50:16 -0700 Subject: [PATCH 23/45] removes data description --- .../pyproject.toml | 1 - .../__init__.py | 2 - .../acquisition.py | 2 +- .../cli.py | 6 +- .../data_description.py | 64 ------------------- 5 files changed, 2 insertions(+), 73 deletions(-) delete mode 100644 workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml index 88bf85e8..7571e5d8 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml @@ -75,5 +75,4 @@ python_functions = ["test_*"] [project.scripts] acquisition = "aind_behavior_dynamic_foraging_metadata_mapper.acquisition:app" instrument = "aind_behavior_dynamic_foraging_metadata_mapper.instrument:app" -data_description = "aind_behavior_dynamic_foraging_metadata_mapper.data_description:app" mapper = "aind_behavior_dynamic_foraging_metadata_mapper.cli:main" diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py index 6cc6967a..938adb62 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py @@ -1,9 +1,7 @@ from .acquisition import acqusition_from_dataset -from .data_description import data_description_from_dataset from .instrument import instrument_from_dataset __all__ = [ "acqusition_from_dataset", - "data_description_from_dataset", "instrument_from_dataset", ] diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py index 64eb3dd8..b64b5dd0 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py @@ -220,7 +220,7 @@ def _get_python_as_code(repository: git.Repo) -> Code: semver += f"-{v.releaselevel}.{v.serial}" return Code( url=repository.remote().url, - name="aind-behavior-vr-foraging", + name="aind-behavior-dynamic-foraging", version=repository.head.commit.hexsha, language="Python", language_version=semver, diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py index 04f3bf28..42176523 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py @@ -23,7 +23,6 @@ class DataMapperCli(BaseSettings, cli_kebab_case=True): def cli_cmd(self): """Generate aind-data-schema metadata for the Dynamic Foraging dataset located at the specified path.""" from .acquisition import acqusition_from_dataset - from .data_description import data_description_from_dataset from .instrument import instrument_from_dataset acquisition = acqusition_from_dataset( @@ -31,16 +30,13 @@ def cli_cmd(self): repo_path=Path(self.repo_path), end_time=self.session_end_time, ) - instrument = instrument_from_dataset(data_directory=Path(self.data_directory)) - data_description = data_description_from_dataset(data_directory=Path(self.data_directory)) acquisition.write_standard_file(output_directory=Path(self.data_directory), filename_suffix=self.suffix) instrument.write_standard_file(output_directory=Path(self.data_directory), filename_suffix=self.suffix) - data_description.write_standard_file(output_directory=Path(self.data_directory), filename_suffix=self.suffix) logger.info( - "Mapping completed! Saved acquisition.json, instrument.json, data_description.json to %s", + "Mapping completed! Saved acquisition.json, instrument.json to %s", self.data_directory, ) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py deleted file mode 100644 index 64ffd278..00000000 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/data_description.py +++ /dev/null @@ -1,64 +0,0 @@ -from pathlib import Path - -from aind_behavior_dynamic_foraging.data_contract import dataset as df_foraging_dataset -from aind_behavior_services.session import Session -from aind_data_schema.components.identifiers import Person -from aind_data_schema.core.data_description import DataDescription, Funding -from aind_data_schema_models.data_name_patterns import DataLevel, Group -from aind_data_schema_models.modalities import Modality -from aind_data_schema_models.organizations import Organization -from cyclopts import App - -app = App() - - -@app.default -def data_description_from_dataset( - data_directory: Path, -) -> DataDescription: - """ - Create acquisition model for completed session. - - Args: - data_directory (os.PathLike): - Path to the directory containing the dataset to analyze. This - directory is expected to include all required behavioral data files. - - Returns: - DataDescription: - DataDescription model for session - - Raises: - FileNotFoundError: - If the specified data directory or required files do not exist. - - ValueError: - If the dataset is malformed or missing required fields for - computing metrics. - """ - - dataset = df_foraging_dataset(data_directory) - software_events = dataset["Behavior"]["SoftwareEvents"] - software_events.load_all() - - input_schemas = dataset["Behavior"]["InputSchemas"] - session = Session.model_validate(input_schemas["Session"].data) - - return DataDescription( - subject_id=session.subject, - creation_time=session.date, - institution=Organization.AIND, - funding_source=[ - Funding( - funder=Organization.AI, - ) - ], - data_level=DataLevel.RAW, - investigators=[Person(name=session.experimenter[0])], - project_name="DynamicForaging", - modalities=[ - Modality.BEHAVIOR, - Modality.BEHAVIOR_VIDEOS, - ], - group=Group.BEHAVIOR, - ) From 0a7e2f48f509ca63628c3ff571d16dcc337d9983 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Thu, 16 Apr 2026 10:43:15 -0700 Subject: [PATCH 24/45] configures logger to json and removes cluttered log mesages --- src/Extensions/bonsai.py | 2 +- .../task_logic/trial_generators/coupled_trial_generator.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Extensions/bonsai.py b/src/Extensions/bonsai.py index 2009618b..1780e753 100644 --- a/src/Extensions/bonsai.py +++ b/src/Extensions/bonsai.py @@ -4,7 +4,7 @@ from pydantic import TypeAdapter -logging.basicConfig(stream=sys.stdout, level=logging.DEBUG) +logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, format='{"level": %(levelno)d, "msg": "%(message)s"}',) from aind_behavior_dynamic_foraging.task_logic import TrialGeneratorSpec # noqa diff --git a/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/coupled_trial_generator.py b/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/coupled_trial_generator.py index b098669a..34ba90fe 100644 --- a/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/coupled_trial_generator.py +++ b/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/coupled_trial_generator.py @@ -154,8 +154,6 @@ def update(self, outcome: TrialOutcome | str) -> None: outcome: The TrialOutcome from the most recently completed trial. """ - logger.info(f"Updating coupled trial generator with trial outcome of {outcome}") - if isinstance(outcome, str): outcome = TrialOutcome.model_validate_json(outcome) From 2c411f97f81a59db73f7a7fe48009c9eec6781d4 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Thu, 16 Apr 2026 10:43:46 -0700 Subject: [PATCH 25/45] lints --- src/Extensions/bonsai.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Extensions/bonsai.py b/src/Extensions/bonsai.py index 1780e753..ceddd4d9 100644 --- a/src/Extensions/bonsai.py +++ b/src/Extensions/bonsai.py @@ -4,7 +4,11 @@ from pydantic import TypeAdapter -logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, format='{"level": %(levelno)d, "msg": "%(message)s"}',) +logging.basicConfig( + stream=sys.stdout, + level=logging.DEBUG, + format='{"level": %(levelno)d, "msg": "%(message)s"}', +) from aind_behavior_dynamic_foraging.task_logic import TrialGeneratorSpec # noqa From 96ea485e6992567cd3ccf7c78c70a72c9145fcc4 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Thu, 16 Apr 2026 10:49:28 -0700 Subject: [PATCH 26/45] adds name to log schema --- src/Extensions/bonsai.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Extensions/bonsai.py b/src/Extensions/bonsai.py index ceddd4d9..6c0c2811 100644 --- a/src/Extensions/bonsai.py +++ b/src/Extensions/bonsai.py @@ -7,7 +7,7 @@ logging.basicConfig( stream=sys.stdout, level=logging.DEBUG, - format='{"level": %(levelno)d, "msg": "%(message)s"}', + format='{"name": "%(name)s", "level": %(levelno)d, "msg": "%(message)s"}', ) from aind_behavior_dynamic_foraging.task_logic import TrialGeneratorSpec # noqa From 4a305d136841e92ffc0d4c4dc7e51a43eafd8fb0 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Fri, 17 Apr 2026 08:54:00 -0700 Subject: [PATCH 27/45] cleans up baiting logic --- .../trial_generators/block_based_trial_generator.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/block_based_trial_generator.py b/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/block_based_trial_generator.py index 3d37de9e..2e093b7c 100644 --- a/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/block_based_trial_generator.py +++ b/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/block_based_trial_generator.py @@ -157,23 +157,18 @@ def next(self) -> Trial | None: iti = draw_sample(self.spec.inter_trial_interval_duration) quiescent = draw_sample(self.spec.quiescent_duration) - p_reward_left = self.block.p_left_reward - p_reward_right = self.block.p_right_reward - if self.spec.is_baiting: random_numbers = np.random.random(2) self.is_left_baited = self.block.p_left_reward > random_numbers[0] or self.is_left_baited logger.debug(f"Left baited: {self.is_left_baited}") - p_reward_left = 1 if self.is_left_baited else p_reward_left self.is_right_baited = self.block.p_right_reward > random_numbers[1] or self.is_right_baited logger.debug(f"Right baited: {self.is_right_baited}") - p_reward_right = 1 if self.is_right_baited else p_reward_right return Trial( - p_reward_left=p_reward_left, - p_reward_right=p_reward_right, + p_reward_left=1 if self.is_left_baited else self.block.p_left_reward, + p_reward_right=1 if self.is_right_baited else self.block.p_right_reward, reward_consumption_duration=self.spec.reward_consumption_duration, response_deadline_duration=self.spec.response_duration, quiescence_period_duration=quiescent, From 80080af286f39a8fc647cef3adf18741eb3f6185 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Fri, 17 Apr 2026 09:24:06 -0700 Subject: [PATCH 28/45] removes test --- .../test_block_based_trial_generator.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/trial_generators/test_block_based_trial_generator.py b/tests/trial_generators/test_block_based_trial_generator.py index 0b5daaec..0943e386 100644 --- a/tests/trial_generators/test_block_based_trial_generator.py +++ b/tests/trial_generators/test_block_based_trial_generator.py @@ -115,18 +115,6 @@ def test_next_returns_correct_reward_probs(self): self.assertEqual(trial.p_reward_left, self.generator.block.p_left_reward) self.assertEqual(trial.p_reward_right, self.generator.block.p_right_reward) - #### Test unbaited #### - - def test_baiting_disabled_reward_prob_unchanged(self): - """Without baiting, reward probs should equal block probs exactly.""" - self.generator.block = Block(p_right_reward=0.8, p_left_reward=0.2, min_length=10) - self.generator.is_left_baited = True - self.generator.is_right_baited = True - trial = self.generator.next() - - self.assertEqual(trial.p_reward_right, 0.8) - self.assertEqual(trial.p_reward_left, 0.2) - class TestBlockBaseBaitingTrialGenerator(unittest.TestCase): def setUp(self): From 7632a11cce6722f6e04f1a66eef081d32f9cb6c9 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 29 Apr 2026 08:49:47 -0700 Subject: [PATCH 29/45] bumps version --- .../aind_behavior_dynamic_foraging_curricula/pyproject.toml | 2 +- .../pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/workspace/aind_behavior_dynamic_foraging_curricula/pyproject.toml b/workspace/aind_behavior_dynamic_foraging_curricula/pyproject.toml index 386167eb..7e4af570 100644 --- a/workspace/aind_behavior_dynamic_foraging_curricula/pyproject.toml +++ b/workspace/aind_behavior_dynamic_foraging_curricula/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "aind-behavior-curriculum >= 0.0.38", "numpy>=2.4.2", "pydantic-settings", - "aind-behavior-dynamic-foraging==0.0.2rc24" + "aind-behavior-dynamic-foraging==0.0.2rc30" ] [tool.uv.sources] diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml index 7571e5d8..a8e13238 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml @@ -23,7 +23,7 @@ readme = {file = "README.md", content-type = "text/markdown"} dependencies = [ "numpy>=2.4.2", "pydantic-settings", - "aind-behavior-dynamic-foraging==0.0.2rc24", + "aind-behavior-dynamic-foraging==0.0.2rc30", "aind-data-schema>=2.6.0", "cyclopts>=4.10.0" ] From f40221551f89a346f3bf9ea683e3174464b0d395 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Fri, 1 May 2026 08:24:16 -0700 Subject: [PATCH 30/45] lints --- scripts/walk_through_session.py | 3 +-- .../coupled_trial_generator.py | 2 +- .../tests/test_metrics.py | 12 ------------ 3 files changed, 2 insertions(+), 15 deletions(-) diff --git a/scripts/walk_through_session.py b/scripts/walk_through_session.py index 7ec1e181..fef9cceb 100644 --- a/scripts/walk_through_session.py +++ b/scripts/walk_through_session.py @@ -1,11 +1,10 @@ import logging import os +from aind_behavior_dynamic_foraging.data_contract import dataset as df_foraging_dataset from aind_behavior_dynamic_foraging.task_logic.trial_generators.coupled_trial_generators.coupled_warmup_trial_generator import ( CoupledWarmupTrialGeneratorSpec, ) - -from aind_behavior_dynamic_foraging.data_contract import dataset as df_foraging_dataset from aind_behavior_dynamic_foraging.task_logic.trial_models import TrialOutcome logging.basicConfig( diff --git a/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/coupled_trial_generators/coupled_trial_generator.py b/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/coupled_trial_generators/coupled_trial_generator.py index 32c6bc1c..c25f715d 100644 --- a/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/coupled_trial_generators/coupled_trial_generator.py +++ b/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/coupled_trial_generators/coupled_trial_generator.py @@ -299,4 +299,4 @@ def _is_block_switch_allowed(self) -> bool: # - minimum reward requirement is reached # - behavior is stable - return block_length_ok and reward_ok and behavior_ok \ No newline at end of file + return block_length_ok and reward_ok and behavior_ok diff --git a/workspace/aind_behavior_dynamic_foraging_curricula/tests/test_metrics.py b/workspace/aind_behavior_dynamic_foraging_curricula/tests/test_metrics.py index 71f62784..c8ec16b7 100644 --- a/workspace/aind_behavior_dynamic_foraging_curricula/tests/test_metrics.py +++ b/workspace/aind_behavior_dynamic_foraging_curricula/tests/test_metrics.py @@ -110,17 +110,5 @@ def test_previous_metrics_accumulate(self): self.assertEqual(len(result.foraging_efficiency_per_session), 2) self.assertEqual(len(result.unignored_trials_per_session), 2) - def test_foraging_efficiency_is_finite_and_positive(self): - trials = [ - _make_trial(True, True, 0.7, 0.3), - _make_trial(True, False, 0.7, 0.3), - _make_trial(False, True, 0.7, 0.3), - ] - with _patch_dataset(trials): - result = metrics_from_dataset(self.tmp_path) - self.assertGreater(result.foraging_efficiency_per_session[-1], 0) - self.assertTrue(np.isfinite(result.foraging_efficiency_per_session[-1])) - - if __name__ == "__main__": unittest.main() From 2509705a29d60fcc9633dcd927c3baa1e4e82734 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Fri, 1 May 2026 08:38:47 -0700 Subject: [PATCH 31/45] lints --- .../tests/test_metrics.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/workspace/aind_behavior_dynamic_foraging_curricula/tests/test_metrics.py b/workspace/aind_behavior_dynamic_foraging_curricula/tests/test_metrics.py index c8ec16b7..ed46e6ae 100644 --- a/workspace/aind_behavior_dynamic_foraging_curricula/tests/test_metrics.py +++ b/workspace/aind_behavior_dynamic_foraging_curricula/tests/test_metrics.py @@ -3,8 +3,6 @@ from typing import Optional from unittest.mock import MagicMock, PropertyMock, patch -import numpy as np - from aind_behavior_dynamic_foraging_curricula.metrics import ( metrics_from_dataset, ) @@ -110,5 +108,6 @@ def test_previous_metrics_accumulate(self): self.assertEqual(len(result.foraging_efficiency_per_session), 2) self.assertEqual(len(result.unignored_trials_per_session), 2) + if __name__ == "__main__": unittest.main() From e8681118c00201d92d70c139878eb8c9541e1bda Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Tue, 16 Jun 2026 10:20:43 -0700 Subject: [PATCH 32/45] refactors to classes to match vr --- .../pyproject.toml | 2 - .../__init__.py | 8 +- .../acquisition.py | 286 +++++++++-------- .../cli.py | 30 +- .../instrument.py | 294 ++++++++++-------- 5 files changed, 342 insertions(+), 278 deletions(-) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml index a8e13238..d0ba0510 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml @@ -73,6 +73,4 @@ python_classes = ["Test*"] python_functions = ["test_*"] [project.scripts] -acquisition = "aind_behavior_dynamic_foraging_metadata_mapper.acquisition:app" -instrument = "aind_behavior_dynamic_foraging_metadata_mapper.instrument:app" mapper = "aind_behavior_dynamic_foraging_metadata_mapper.cli:main" diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py index 938adb62..ca0ea40a 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/__init__.py @@ -1,7 +1,7 @@ -from .acquisition import acqusition_from_dataset -from .instrument import instrument_from_dataset +from .acquisition import AindAcquisitionDataMapper +from .instrument import AindInstrumentDataMapper __all__ = [ - "acqusition_from_dataset", - "instrument_from_dataset", + "AindAcquisitionDataMapper", + "AindInstrumentDataMapper", ] diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py index b64b5dd0..19750c56 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py @@ -2,6 +2,7 @@ import os import sys from datetime import datetime, timezone +from decimal import Decimal from pathlib import Path from typing import List, Optional @@ -14,10 +15,11 @@ from aind_behavior_services.rig import cameras as abs_camera from aind_behavior_services.rig import water_valve as abs_water_valve from aind_behavior_services.session import Session -from aind_behavior_services.utils import get_fields_of_type, utcnow +from aind_behavior_services.utils import get_fields_of_type, model_from_json_file, utcnow from aind_data_schema.components.configs import TriggerType from aind_data_schema.components.measurements import CalibrationFit, FitType, GenericModel, VolumeCalibration from aind_data_schema.core.acquisition import ( + CALIBRATIONS, Acquisition, AcquisitionSubjectDetails, Code, @@ -30,147 +32,179 @@ from aind_data_schema_models import units from aind_data_schema_models.modalities import Modality from clabe.data_mapper import helpers as data_mapper_helpers -from cyclopts import App +from clabe.data_mapper.aind_data_schema import AindDataSchemaSessionDataMapper +from pydantic import ValidationError logger = logging.getLogger(__name__) -app = App() - - -@app.default -def acqusition_from_dataset( - data_directory: Path, repo_path: os.PathLike, end_time: Optional[datetime] = None -) -> Acquisition: - """ - Create acquisition model for completed session. - - Args: - data_directory (os.PathLike): - Path to the directory containing the dataset to analyze. This - directory is expected to include all required behavioral data files. - - repo_path (os.PathLike): - Path to github repository. - - end_time: Optional[datetime]: - End time of acquisition. If None, current time will be used. - - Returns: - Acquisition: - Acquisition model for session - - Raises: - FileNotFoundError: - If the specified data directory or required files do not exist. - - ValueError: - If the dataset is malformed or missing required fields for - computing metrics. - """ - dataset = df_foraging_dataset(data_directory) - input_schemas = dataset["Behavior"]["InputSchemas"] - session_model = Session.model_validate(input_schemas["Session"].data) - rig_model = AindDynamicForagingRig.model_validate(input_schemas["Rig"].data) - task_logic_model = AindDynamicForagingTaskLogic.model_validate(input_schemas["TaskLogic"].data) - repository = git.Repo(repo_path) - - if end_time is None: - logger.warning("Session end time is not set. Using current time as end time.") - acquisition_end_time = datetime.now(tz=timezone.utc) - - bonsai_code = _get_bonsai_as_code(repository) - python_code = _get_python_as_code(repository) - - cameras = data_mapper_helpers.get_cameras(rig_model, exclude_without_video_writer=True) - camera_configs = [_get_cameras_config(k, v, repository) for k, v in cameras.items()] - - # construct data stream - modalities: list[Modality] = [getattr(Modality, "BEHAVIOR")] - if len(camera_configs) > 0: - modalities.append(getattr(Modality, "BEHAVIOR_VIDEOS")) - modalities = list(set(modalities)) - - active_devices = [ - _device[0] - for _device in get_fields_of_type(rig_model, AbsDevice, stop_recursion_on_type=False) - if _device[0] is not None and not isinstance(_device[1], abs_camera.CameraController) - ] - - data_streams = [ - DataStream( - stream_start_time=session_model.date, - stream_end_time=acquisition_end_time, - code=[bonsai_code, python_code], - active_devices=active_devices, - modalities=modalities, - configurations=camera_configs, - notes=session_model.notes, + +class AindAcquisitionDataMapper(AindDataSchemaSessionDataMapper): + def __init__( + self, data_path: os.PathLike, repository_path: os.PathLike, session_end_time: Optional[datetime] = None + ): + """ + Class to create acquisition model for completed session. + + Args: + data_path (os.PathLike): + Path to the directory containing the dataset to analyze. This + directory is expected to include all required behavioral data files. + + repository_path (os.PathLike): + Path to github repository. + + session_end_time: Optional[datetime]: + End time of acquisition. If None, current time will be used. + """ + + self.data_path = data_path + self.repository_path = repository_path + self.session_end_time = session_end_time + + self.session_model = model_from_json_file( + json_path=Path(self.data_path) / "behavior" / "Logs" / "session_output.json", model=Session + ) + self._mapped: Optional[Acquisition] = None + + def session_schema(self): + return self.mapped + + @property + def session_name(self) -> str: + if self.session_model.session_name is None: + raise ValueError("Session name is not set in the session model.") + return self.session_model.session_name + + def map(self) -> Acquisition: + logger.info("Mapping aind-data-schema Acquisition.") + try: + self._mapped = self._map() + return self._mapped + except (ValidationError, ValueError, IOError) as e: + logger.error("Failed to map to aind-data-schema Session. %s", e) + raise e + + def _map(self) -> Acquisition: + """ + Create acquisition model for completed session. + + Returns: + Acquisition: + Acquisition model for session + + Raises: + FileNotFoundError: + If the specified data directory or required files do not exist. + + ValueError: + If the dataset is malformed or missing required fields for + computing metrics. + """ + dataset = df_foraging_dataset(self.data_path) + input_schemas = dataset["Behavior"]["InputSchemas"] + session_model = Session.model_validate(input_schemas["Session"].data) + rig_model = AindDynamicForagingRig.model_validate(input_schemas["Rig"].data) + task_logic_model = AindDynamicForagingTaskLogic.model_validate(input_schemas["TaskLogic"].data) + repository = git.Repo(self.repository_path) + + if self.session_end_time is None: + logger.warning("Session end time is not set. Using current time as end time.") + acquisition_end_time = datetime.now(tz=timezone.utc) + + bonsai_code = _get_bonsai_as_code(repository) + python_code = _get_python_as_code(repository) + + cameras = data_mapper_helpers.get_cameras(rig_model, exclude_without_video_writer=True) + camera_configs = [_get_camera_config(k, v, repository) for k, v in cameras.items()] + + # construct data stream + modalities: list[Modality.ONE_OF] = [getattr(Modality, "BEHAVIOR")] + if len(camera_configs) > 0: + modalities.append(getattr(Modality, "BEHAVIOR_VIDEOS")) + modalities = list(set(modalities)) + + active_devices = [ + _device[0] + for _device in get_fields_of_type(rig_model, AbsDevice, stop_recursion_on_type=False) + if _device[0] is not None and not isinstance(_device[1], abs_camera.CameraController) + ] + + data_streams = [ + DataStream( + stream_start_time=session_model.date, + stream_end_time=acquisition_end_time, + code=[bonsai_code, python_code], + active_devices=active_devices, + modalities=modalities, + configurations=camera_configs, + notes=session_model.notes, + ) + ] + + # populate behavior epoch + metrics = dataset["Behavior"]["Metrics"].data + trainer_state = dataset["Behavior"]["TrainerState"].data + performance_metrics = PerformanceMetrics(output_parameters=metrics.model_dump()) + + stimulus_epoch = StimulusEpoch( + stimulus_start_time=session_model.date, + stimulus_end_time=acquisition_end_time, + stimulus_name="GoCue", + code=bonsai_code, + stimulus_modalities=[StimulusModality.AUDITORY], + performance_metrics=performance_metrics, + curriculum_status=trainer_state.stage.name, ) - ] - - # populate behavior epoch - metrics = dataset["Behavior"]["Metrics"].data - trainer_state = dataset["Behavior"]["TrainerState"].data - performance_metrics = PerformanceMetrics(output_parameters=metrics.model_dump()) - - stimulus_epoch = StimulusEpoch( - stimulus_start_time=session_model.date, - stimulus_end_time=acquisition_end_time, - stimulus_name="GoCue", - code=bonsai_code, - stimulus_modalities=[StimulusModality.AUDITORY], - performance_metrics=performance_metrics, - curriculum_status=trainer_state.stage.name, - ) - # Construct aind-data-schema session - return Acquisition( - subject_id=session_model.subject, - subject_details=_get_subject_details(data_directory), - instrument_id=rig_model.rig_name, - acquisition_end_time=acquisition_end_time, - acquisition_start_time=session_model.date, - experimenters=session_model.experimenter, - acquisition_type=session_model.experiment or task_logic_model.name, - coordinate_system=None, - data_streams=data_streams, - calibrations=_get_water_calibration(rig_model), - stimulus_epochs=[stimulus_epoch], - ) + # Construct aind-data-schema session + return Acquisition( + subject_id=session_model.subject, + subject_details=_get_subject_details(self.data_path), + instrument_id=rig_model.rig_name, + acquisition_end_time=acquisition_end_time, + acquisition_start_time=session_model.date, + experimenters=session_model.experimenter, + acquisition_type=session_model.experiment or task_logic_model.name, + coordinate_system=None, + data_streams=data_streams, + calibrations=_get_water_calibration(rig_model), + stimulus_epochs=[stimulus_epoch], + ) -def _get_subject_details(data_directory: os.PathLike) -> AcquisitionSubjectDetails: +def _get_subject_details(data_path: os.PathLike) -> AcquisitionSubjectDetails: + water = calculate_consumed_water(data_path) return AcquisitionSubjectDetails( mouse_platform_name="tube", - reward_consumed_total=calculate_consumed_water(data_directory), + reward_consumed_total=None if not water else Decimal(str(water)), reward_consumed_unit=units.VolumeUnit.ML, ) -def _get_water_calibration(rig_model: AindDynamicForagingRig) -> List[VolumeCalibration]: +def _get_water_calibration(rig_model: AindDynamicForagingRig) -> List[CALIBRATIONS]: water_calibrations = get_fields_of_type(rig_model, abs_water_valve.WaterValveCalibration) vol_cal = [] - for device_name, water_calibration in water_calibrations: - c = water_calibration - vol_cal.append( - VolumeCalibration( - device_name=device_name, - calibration_date=water_calibration.date if water_calibration.date else utcnow(), - input=list(c.interval_average.keys()), - output=list(c.interval_average.values()), - input_unit=units.TimeUnit.S, - output_unit=units.VolumeUnit.ML, - fit=CalibrationFit( - fit_type=FitType.LINEAR, - fit_parameters=GenericModel.model_validate(c.model_dump()), - ), + for device_name, wc in water_calibrations: + if device_name and wc.interval_average: + vol_cal.append( + VolumeCalibration( + device_name=device_name, + calibration_date=wc.date if wc.date else utcnow(), + input=list(wc.interval_average.keys()), + output=list(wc.interval_average.values()), + input_unit=units.TimeUnit.S, + output_unit=units.VolumeUnit.ML, + fit=CalibrationFit( + fit_type=FitType.LINEAR, + fit_parameters=GenericModel.model_validate(wc.model_dump()), + ), + ) ) - ) return vol_cal -def _get_cameras_config(name: str, camera: abs_camera.CameraTypes, repository: git.Repo) -> List[DetectorConfig]: +def _get_camera_config(name: str, camera: abs_camera.CameraTypes, repository: git.Repo) -> DetectorConfig: if isinstance(camera.video_writer, abs_camera.VideoWriterFfmpeg): compression = Code( @@ -185,18 +219,14 @@ def _get_cameras_config(name: str, camera: abs_camera.CameraTypes, repository: g else: raise ValueError("Camera does not have a valid video writer configured.") - camera = DetectorConfig( + return DetectorConfig( device_name=name, exposure_time=getattr(camera, "exposure", -1), exposure_time_unit=units.TimeUnit.US, trigger_type=TriggerType.EXTERNAL, - compression=compression(camera.video_writer), + compression=compression, ) - cameras = data_mapper_helpers.get_cameras(AindDynamicForagingTaskLogic, exclude_without_video_writer=True) - - return list(map(camera, cameras.keys(), cameras.values())) - def _get_bonsai_as_code(repository: git.Repo) -> Code: bonsai_folder = Path(Path(repository.working_tree_dir) / "bonsai" / "bonsai.exe").parent diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py index 42176523..d053432a 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py @@ -10,8 +10,8 @@ class DataMapperCli(BaseSettings, cli_kebab_case=True): - data_directory: os.PathLike = Field(description="Path to the session data directory.") - repo_path: os.PathLike = Field( + data_path: os.PathLike = Field(description="Path to the session data directory.") + repository_path: os.PathLike = Field( default=Path("."), description="Path to the repository. By default it will use the current directory." ) session_end_time: AwareDatetime | None = Field( @@ -22,22 +22,28 @@ class DataMapperCli(BaseSettings, cli_kebab_case=True): def cli_cmd(self): """Generate aind-data-schema metadata for the Dynamic Foraging dataset located at the specified path.""" - from .acquisition import acqusition_from_dataset - from .instrument import instrument_from_dataset + from .acquisition import AindAcquisitionDataMapper + from .instrument import AindInstrumentDataMapper - acquisition = acqusition_from_dataset( - data_directory=Path(self.data_directory), - repo_path=Path(self.repo_path), - end_time=self.session_end_time, + session_mapper = AindAcquisitionDataMapper( + data_path=Path(self.data_path), + repository_path=Path(self.repository_path), + session_end_time=self.session_end_time, ) - instrument = instrument_from_dataset(data_directory=Path(self.data_directory)) + acquisition = session_mapper.map() - acquisition.write_standard_file(output_directory=Path(self.data_directory), filename_suffix=self.suffix) - instrument.write_standard_file(output_directory=Path(self.data_directory), filename_suffix=self.suffix) + rig_mapper = AindInstrumentDataMapper(data_path=Path(self.data_path)) + instrument = rig_mapper.map() + + assert session_mapper.mapped is not None + assert rig_mapper.mapped is not None + + acquisition.write_standard_file(output_directory=Path(self.repository_path), filename_suffix=self.suffix) + instrument.write_standard_file(output_directory=Path(self.repository_path), filename_suffix=self.suffix) logger.info( "Mapping completed! Saved acquisition.json, instrument.json to %s", - self.data_directory, + self.repository_path, ) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py index bb4d4f05..7a79d816 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py @@ -1,4 +1,7 @@ +import logging +import os from datetime import date +from decimal import Decimal from pathlib import Path from aind_behavior_dynamic_foraging.data_contract import dataset as df_foraging_dataset @@ -20,156 +23,183 @@ from aind_data_schema.core.instrument import Instrument from aind_data_schema_models.modalities import Modality from aind_data_schema_models.organizations import Organization -from cyclopts import App - -app = App() - - -@app.default -def instrument_from_dataset( - data_directory: Path, -) -> Instrument: - """ - Create Instrument model for completed session. - - Args: - data_directory (os.PathLike): - Path to the directory containing the dataset to analyze. This - directory is expected to include all required behavioral data files. - - Returns: - Instrument: - Instrument model for session - - Raises: - FileNotFoundError: - If the specified data directory or required files do not exist. - - ValueError: - If the dataset is malformed or missing required fields for - computing metrics. - """ - - dataset = df_foraging_dataset(data_directory) - input_schemas = dataset["Behavior"]["InputSchemas"] - rig = AindDynamicForagingRig.model_validate(input_schemas["Rig"].data) - - components = [] - connections = [] - - # cameras - for name, cam in rig.triggered_camera_controller.cameras.items(): - camera = Camera( - name=name, - serial_number=cam.serial_number, - manufacturer=Organization.SPINNAKER, - data_interface=DataInterface.COAX, - ) - assembly = CameraAssembly( - name=f"{name}Assembly", - camera=camera, - target=CameraTarget.BODY if "Body" in name else CameraTarget.FACE, - lens=Lens(name="Lens A", manufacturer=Organization.FUJINON), - relative_position=[AnatomicalRelative.RIGHT if "Body" in name else AnatomicalRelative.SUPERIOR], - ) - components.append(assembly) - - # behavior board - components.append( - HarpDevice( - name="BehaviorBoard", - harp_device_type=HarpDeviceType.BEHAVIOR, - serial_number=rig.harp_behavior.serial_number, - manufacturer=Organization.CHAMPALIMAUD, - is_clock_generator=False, - ) - ) - - # clock generator - components.append( - HarpDevice( - name="ClockGenerator", - harp_device_type=HarpDeviceType.WHITERABBIT, - serial_number=rig.harp_clock_generator.serial_number, - is_clock_generator=True, - ) - ) - - # sound card - components.append( - HarpDevice( - name="SoundCard", - harp_device_type=HarpDeviceType.SOUNDCARD, - serial_number=rig.harp_sound_card.serial_number, - manufacturer=Organization.CHAMPALIMAUD, - is_clock_generator=False, - ) - ) +from clabe.data_mapper.aind_data_schema import AindDataSchemaRigDataMapper + +logger = logging.getLogger(__name__) + + +class AindInstrumentDataMapper(AindDataSchemaRigDataMapper): + def __init__( + self, + data_path: os.PathLike, + ): + """ + Create Instrument model for completed session. + + Args: + data_directory (os.PathLike): + Path to the directory containing the dataset to analyze. This + directory is expected to include all required behavioral data files. + """ + + super().__init__() + self._data_path = Path(data_path) + + def rig_schema(self): + return self.mapped + + @property + def session_name(self): + raise NotImplementedError("Method not implemented.") + + def map(self) -> Instrument: + logger.info("Mapping aind-data-schema Instrument.") + self._mapped = self._map() + return self.mapped + + def _map(self) -> Instrument: + """ + Create Instrument model for completed session. + + Returns: + Instrument: + Instrument model for session + + Raises: + FileNotFoundError: + If the specified data directory or required files do not exist. + + ValueError: + If the dataset is malformed or missing required fields for + computing metrics. + """ + + dataset = df_foraging_dataset(self._data_path) + input_schemas = dataset["Behavior"]["InputSchemas"] + rig = AindDynamicForagingRig.model_validate(input_schemas["Rig"].data) + + components = [] + connections = [] + + # cameras + for name, cam in rig.triggered_camera_controller.cameras.items(): + camera = Camera( + name=name, + serial_number=cam.serial_number, + manufacturer=Organization.SPINNAKER, + data_interface=DataInterface.COAX, + ) + assembly = CameraAssembly( + name=f"{name}Assembly", + camera=camera, + target=CameraTarget.BODY if "Body" in name else CameraTarget.FACE, + lens=Lens(name="Lens A", manufacturer=Organization.FUJINON), + relative_position=[AnatomicalRelative.RIGHT if "Body" in name else AnatomicalRelative.SUPERIOR], + ) + components.append(assembly) - # optional harp devices - if rig.harp_lickometer_left: + # behavior board components.append( HarpDevice( - name="LickometerLeft", - harp_device_type=HarpDeviceType.LICKETYSPLIT, - serial_number=rig.harp_lickometer_left.serial_number, + name="BehaviorBoard", + harp_device_type=HarpDeviceType.BEHAVIOR, + serial_number=rig.harp_behavior.serial_number, + manufacturer=Organization.CHAMPALIMAUD, is_clock_generator=False, ) ) - if rig.harp_lickometer_right: + + # clock generator components.append( HarpDevice( - name="LickometerRight", - serial_number=rig.harp_lickometer_right.serial_number, - harp_device_type=HarpDeviceType.LICKETYSPLIT, - is_clock_generator=False, + name="ClockGenerator", + harp_device_type=HarpDeviceType.WHITERABBIT, + serial_number=rig.harp_clock_generator.serial_number, + is_clock_generator=True, ) ) - if rig.harp_sniff_detector: + + # sound card components.append( HarpDevice( - name="SniffDetector", - harp_device_type=HarpDeviceType.SNIFFDETECTOR, - serial_number=rig.harp_sniff_detector.serial_number, + name="SoundCard", + harp_device_type=HarpDeviceType.SOUNDCARD, + serial_number=rig.harp_sound_card.serial_number, + manufacturer=Organization.CHAMPALIMAUD, is_clock_generator=False, ) ) - if rig.harp_environment_sensor: + + # optional harp devices + if rig.harp_lickometer_left: + components.append( + HarpDevice( + name="LickometerLeft", + harp_device_type=HarpDeviceType.LICKETYSPLIT, + serial_number=rig.harp_lickometer_left.serial_number, + is_clock_generator=False, + ) + ) + if rig.harp_lickometer_right: + components.append( + HarpDevice( + name="LickometerRight", + serial_number=rig.harp_lickometer_right.serial_number, + harp_device_type=HarpDeviceType.LICKETYSPLIT, + is_clock_generator=False, + ) + ) + if rig.harp_sniff_detector: + components.append( + HarpDevice( + name="SniffDetector", + harp_device_type=HarpDeviceType.SNIFFDETECTOR, + serial_number=rig.harp_sniff_detector.serial_number, + is_clock_generator=False, + ) + ) + if rig.harp_environment_sensor: + components.append( + HarpDevice( + name="EnvironmentSensor", + harp_device_type=HarpDeviceType.ENVIRONMENTSENSOR, + serial_number=rig.harp_environment_sensor.serial_number, + is_clock_generator=False, + ) + ) + + # manipulator components.append( - HarpDevice( - name="EnvironmentSensor", - harp_device_type=HarpDeviceType.ENVIRONMENTSENSOR, - serial_number=rig.harp_environment_sensor.serial_number, - is_clock_generator=False, + MotorizedStage( + name="Manipulator", + serial_number=rig.manipulator.serial_number, + travel=Decimal("30"), ) ) - # manipulator - components.append(MotorizedStage(name="Manipulator", serial_number=rig.manipulator.serial_number, travel=0.0)) - - # connections - for name in rig.triggered_camera_controller.cameras: - connections.append( - Connection( - source_device="BehaviorBoard", - target_device=name, + # connections + for name in rig.triggered_camera_controller.cameras: + connections.append( + Connection( + source_device="BehaviorBoard", + target_device=name, + ) ) - ) - return Instrument( - instrument_id=rig.rig_name, - modification_date=date.today(), - modalities=[Modality.BEHAVIOR, Modality.BEHAVIOR_VIDEOS], - coordinate_system=CoordinateSystem( - name="RigCoordinateSystem", - origin=Origin.ORIGIN, - axes=[ - Axis(name=AxisName.X, direction=Direction.LR), - Axis(name=AxisName.Y, direction=Direction.FB), - Axis(name=AxisName.Z, direction=Direction.DU), - ], - axis_unit=SizeUnit.MM, - ), - components=components, - connections=connections, - ) + return Instrument( + instrument_id=rig.rig_name, + modification_date=date.today(), + modalities=[Modality.BEHAVIOR, Modality.BEHAVIOR_VIDEOS], + coordinate_system=CoordinateSystem( + name="RigCoordinateSystem", + origin=Origin.ORIGIN, + axes=[ + Axis(name=AxisName.X, direction=Direction.LR), + Axis(name=AxisName.Y, direction=Direction.FB), + Axis(name=AxisName.Z, direction=Direction.DU), + ], + axis_unit=SizeUnit.MM, + ), + components=components, + connections=connections, + ) From 3cbc9be070d4bd7dc24be06a378b2ea2d7dc14fb Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Tue, 16 Jun 2026 10:24:40 -0700 Subject: [PATCH 33/45] unpins aind-behavior-dynamic-foraging --- acquisition.json | 268 +++++++++++++++ instrument.json | 306 ++++++++++++++++++ .../pyproject.toml | 2 +- .../pyproject.toml | 2 +- 4 files changed, 576 insertions(+), 2 deletions(-) create mode 100644 acquisition.json create mode 100644 instrument.json diff --git a/acquisition.json b/acquisition.json new file mode 100644 index 00000000..24ce008d --- /dev/null +++ b/acquisition.json @@ -0,0 +1,268 @@ +{ + "object_type": "Acquisition", + "describedBy": "https://raw.githubusercontent.com/AllenNeuralDynamics/aind-data-schema/main/src/aind_data_schema/core/acquisition.py", + "schema_version": "2.4.0", + "subject_id": "821586", + "specimen_id": null, + "acquisition_start_time": "2026-04-28T20:03:47.289698Z", + "acquisition_start_tz": 0, + "acquisition_end_time": "2026-06-16T17:24:03.642253Z", + "experimenters": [ + "micah.woodard" + ], + "protocol_id": null, + "ethics_review_id": null, + "instrument_id": "test_rig", + "acquisition_type": "AindDynamicForaging", + "notes": null, + "coordinate_system": null, + "calibrations": [ + { + "object_type": "Volume calibration", + "device_name": "water_valve_left", + "calibration_date": "2026-06-16T17:24:03.890836Z", + "description": "Volume measured for various solenoid opening times", + "protocol_id": null, + "measured_at": null, + "input": [ + 0.02, + 0.03, + 0.04 + ], + "input_unit": "second", + "repeats": null, + "output": [ + 0.00193, + 0.002846, + 0.003568 + ], + "output_unit": "milliliter", + "fit": { + "object_type": "Calibration fit", + "fit_type": "linear", + "fit_parameters": { + "date": null, + "measurements": [ + { + "valve_open_interval": 0.1, + "valve_open_time": 0.02, + "water_weight": [ + 0.386 + ], + "repeat_count": 200 + }, + { + "valve_open_interval": 0.1, + "valve_open_time": 0.03, + "water_weight": [ + 2.846 + ], + "repeat_count": 1000 + }, + { + "valve_open_interval": 0.1, + "valve_open_time": 0.04, + "water_weight": [ + 3.568 + ], + "repeat_count": 1000 + } + ], + "interval_average": { + "0.02": 0.00193, + "0.03": 0.002846, + "0.04": 0.003568 + }, + "slope": 0.0819, + "offset": 0.000294, + "r2": 0.999546, + "valid_domain": [ + 0.02, + 0.04 + ] + } + }, + "notes": null + }, + { + "object_type": "Volume calibration", + "device_name": "water_valve_right", + "calibration_date": "2026-06-16T17:24:03.913085Z", + "description": "Volume measured for various solenoid opening times", + "protocol_id": null, + "measured_at": null, + "input": [ + 0.02, + 0.03, + 0.04 + ], + "input_unit": "second", + "repeats": null, + "output": [ + 0.001894, + 0.002667, + 0.003356 + ], + "output_unit": "milliliter", + "fit": { + "object_type": "Calibration fit", + "fit_type": "linear", + "fit_parameters": { + "date": null, + "measurements": [ + { + "valve_open_interval": 0.1, + "valve_open_time": 0.02, + "water_weight": [ + 1.894 + ], + "repeat_count": 1000 + }, + { + "valve_open_interval": 0.1, + "valve_open_time": 0.03, + "water_weight": [ + 2.667 + ], + "repeat_count": 1000 + }, + { + "valve_open_interval": 0.1, + "valve_open_time": 0.04, + "water_weight": [ + 3.364 + ], + "repeat_count": 1000 + } + ], + "interval_average": { + "0.02": 0.001894, + "0.03": 0.002667, + "0.04": 0.003356 + }, + "slope": 0.0731, + "offset": 0.000432, + "r2": 0.999974, + "valid_domain": [ + 0.02, + 0.04 + ] + } + }, + "notes": null + } + ], + "maintenance": [], + "data_streams": [ + { + "object_type": "Data stream", + "stream_start_time": "2026-04-28T20:03:47.289698Z", + "stream_end_time": "2026-06-16T17:24:03.642253Z", + "modalities": [ + { + "name": "Behavior", + "abbreviation": "behavior" + } + ], + "code": [ + { + "object_type": "Code", + "url": "git@github.com:AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git", + "name": "Aind.Behavior.DynamicForaging", + "version": "e8681118c00201d92d70c139878eb8c9541e1bda", + "container": null, + "run_script": null, + "language": "Bonsai", + "language_version": "2.9.0", + "input_data": null, + "parameters": null, + "core_dependency": null + }, + { + "object_type": "Code", + "url": "git@github.com:AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git", + "name": "aind-behavior-dynamic-foraging", + "version": "e8681118c00201d92d70c139878eb8c9541e1bda", + "container": null, + "run_script": null, + "language": "Python", + "language_version": "3.13.11", + "input_data": null, + "parameters": null, + "core_dependency": null + } + ], + "notes": null, + "active_devices": [ + "BodyCamera", + "SideCamera", + "harp_behavior", + "harp_clock_generator", + "harp_sound_card", + "harp_environment_sensor", + "manipulator" + ], + "configurations": [], + "connections": [] + } + ], + "stimulus_epochs": [ + { + "object_type": "Stimulus epoch", + "stimulus_start_time": "2026-04-28T20:03:47.289698Z", + "stimulus_end_time": "2026-06-16T17:24:03.642253Z", + "stimulus_name": "GoCue", + "code": { + "object_type": "Code", + "url": "git@github.com:AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git", + "name": "Aind.Behavior.DynamicForaging", + "version": "e8681118c00201d92d70c139878eb8c9541e1bda", + "container": null, + "run_script": null, + "language": "Bonsai", + "language_version": "2.9.0", + "input_data": null, + "parameters": null, + "core_dependency": null + }, + "stimulus_modalities": [ + "Auditory" + ], + "performance_metrics": { + "object_type": "Performance metrics", + "output_parameters": { + "foraging_efficiency_per_session": [ + 1.4866204162537167 + ], + "unignored_trials_per_session": [ + 527 + ], + "total_sessions": 1, + "consecutive_sessions_at_current_stage": 1, + "stage_name": "stage_2" + }, + "reward_consumed_during_epoch": null, + "reward_consumed_unit": null, + "trials_total": null, + "trials_finished": null, + "trials_rewarded": null + }, + "notes": null, + "active_devices": [], + "configurations": [], + "training_protocol_name": null, + "curriculum_status": "stage_2" + } + ], + "manipulations": [], + "subject_details": { + "object_type": "Acquisition subject details", + "animal_weight_prior": null, + "animal_weight_post": null, + "weight_unit": "gram", + "anaesthesia": null, + "mouse_platform_name": "tube", + "reward_consumed_total": "0.43600000000000033", + "reward_consumed_unit": "milliliter" + } +} \ No newline at end of file diff --git a/instrument.json b/instrument.json new file mode 100644 index 00000000..f70bac07 --- /dev/null +++ b/instrument.json @@ -0,0 +1,306 @@ +{ + "object_type": "Instrument", + "describedBy": "https://raw.githubusercontent.com/AllenNeuralDynamics/aind-data-schema/main/src/aind_data_schema/core/instrument.py", + "schema_version": "2.2.3", + "location": null, + "instrument_id": "test_rig", + "modification_date": "2026-06-16", + "modalities": [ + { + "name": "Behavior", + "abbreviation": "behavior" + }, + { + "name": "Behavior videos", + "abbreviation": "behavior-videos" + } + ], + "calibrations": null, + "coordinate_system": { + "object_type": "Coordinate system", + "name": "RigCoordinateSystem", + "origin": "Origin", + "axes": [ + { + "object_type": "Axis", + "name": "X", + "direction": "Left_to_right" + }, + { + "object_type": "Axis", + "name": "Y", + "direction": "Front_to_back" + }, + { + "object_type": "Axis", + "name": "Z", + "direction": "Down_to_up" + } + ], + "axis_unit": "millimeter" + }, + "temperature_control": null, + "notes": null, + "connections": [ + { + "object_type": "Connection", + "source_device": "BehaviorBoard", + "source_port": null, + "target_device": "BodyCamera", + "target_port": null, + "send_and_receive": false + }, + { + "object_type": "Connection", + "source_device": "BehaviorBoard", + "source_port": null, + "target_device": "SideCamera", + "target_port": null, + "send_and_receive": false + } + ], + "components": [ + { + "object_type": "Camera assembly", + "relative_position": [ + "Right" + ], + "coordinate_system": null, + "transform": null, + "name": "BodyCameraAssembly", + "target": "Body", + "camera": { + "object_type": "Camera", + "name": "BodyCamera", + "serial_number": "23349426", + "manufacturer": { + "name": "Spinnaker", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "additional_settings": null, + "notes": null, + "detector_type": "Camera", + "data_interface": "Coax", + "cooling": "No cooling", + "frame_rate": null, + "frame_rate_unit": null, + "immersion": null, + "chroma": null, + "sensor_width": null, + "sensor_height": null, + "size_unit": "pixel", + "sensor_format": null, + "sensor_format_unit": null, + "bit_depth": null, + "bin_mode": "No binning", + "bin_width": null, + "bin_height": null, + "bin_unit": "pixel", + "gain": null, + "crop_offset_x": null, + "crop_offset_y": null, + "crop_width": null, + "crop_height": null, + "crop_unit": "pixel", + "recording_software": null, + "driver": null, + "driver_version": null + }, + "lens": { + "object_type": "Lens", + "name": "Lens A", + "serial_number": null, + "manufacturer": { + "name": "Fujinon", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "additional_settings": null, + "notes": null + }, + "filter": null + }, + { + "object_type": "Camera assembly", + "relative_position": [ + "Superior" + ], + "coordinate_system": null, + "transform": null, + "name": "SideCameraAssembly", + "target": "Face", + "camera": { + "object_type": "Camera", + "name": "SideCamera", + "serial_number": "23349424", + "manufacturer": { + "name": "Spinnaker", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "additional_settings": null, + "notes": null, + "detector_type": "Camera", + "data_interface": "Coax", + "cooling": "No cooling", + "frame_rate": null, + "frame_rate_unit": null, + "immersion": null, + "chroma": null, + "sensor_width": null, + "sensor_height": null, + "size_unit": "pixel", + "sensor_format": null, + "sensor_format_unit": null, + "bit_depth": null, + "bin_mode": "No binning", + "bin_width": null, + "bin_height": null, + "bin_unit": "pixel", + "gain": null, + "crop_offset_x": null, + "crop_offset_y": null, + "crop_width": null, + "crop_height": null, + "crop_unit": "pixel", + "recording_software": null, + "driver": null, + "driver_version": null + }, + "lens": { + "object_type": "Lens", + "name": "Lens A", + "serial_number": null, + "manufacturer": { + "name": "Fujinon", + "abbreviation": null, + "registry": null, + "registry_identifier": null + }, + "model": null, + "additional_settings": null, + "notes": null + }, + "filter": null + }, + { + "object_type": "Harp device", + "name": "BehaviorBoard", + "serial_number": null, + "manufacturer": { + "name": "Champalimaud Foundation", + "abbreviation": "Champalimaud", + "registry": "Research Organization Registry (ROR)", + "registry_identifier": "03g001n57" + }, + "model": null, + "additional_settings": null, + "notes": null, + "data_interface": "USB", + "channels": [], + "firmware_version": null, + "hardware_version": null, + "harp_device_type": { + "whoami": 1216, + "name": "Behavior" + }, + "core_version": null, + "tag_version": null, + "is_clock_generator": false + }, + { + "object_type": "Harp device", + "name": "ClockGenerator", + "serial_number": null, + "manufacturer": { + "name": "Open Ephys Production Site", + "abbreviation": "OEPS", + "registry": "Research Organization Registry (ROR)", + "registry_identifier": "007rkz355" + }, + "model": null, + "additional_settings": null, + "notes": null, + "data_interface": "USB", + "channels": [], + "firmware_version": null, + "hardware_version": null, + "harp_device_type": { + "whoami": 1404, + "name": "WhiteRabbit" + }, + "core_version": null, + "tag_version": null, + "is_clock_generator": true + }, + { + "object_type": "Harp device", + "name": "SoundCard", + "serial_number": null, + "manufacturer": { + "name": "Champalimaud Foundation", + "abbreviation": "Champalimaud", + "registry": "Research Organization Registry (ROR)", + "registry_identifier": "03g001n57" + }, + "model": null, + "additional_settings": null, + "notes": null, + "data_interface": "USB", + "channels": [], + "firmware_version": null, + "hardware_version": null, + "harp_device_type": { + "whoami": 1280, + "name": "SoundCard" + }, + "core_version": null, + "tag_version": null, + "is_clock_generator": false + }, + { + "object_type": "Harp device", + "name": "EnvironmentSensor", + "serial_number": null, + "manufacturer": { + "name": "Open Ephys Production Site", + "abbreviation": "OEPS", + "registry": "Research Organization Registry (ROR)", + "registry_identifier": "007rkz355" + }, + "model": null, + "additional_settings": null, + "notes": null, + "data_interface": "USB", + "channels": [], + "firmware_version": null, + "hardware_version": null, + "harp_device_type": { + "whoami": 1405, + "name": "EnvironmentSensor" + }, + "core_version": null, + "tag_version": null, + "is_clock_generator": false + }, + { + "object_type": "Motorized stage", + "name": "Manipulator", + "serial_number": null, + "manufacturer": null, + "model": null, + "additional_settings": null, + "notes": null, + "travel": "30", + "travel_unit": "millimeter", + "firmware": null + } + ] +} \ No newline at end of file diff --git a/workspace/aind_behavior_dynamic_foraging_curricula/pyproject.toml b/workspace/aind_behavior_dynamic_foraging_curricula/pyproject.toml index acb6a8f9..77febb94 100644 --- a/workspace/aind_behavior_dynamic_foraging_curricula/pyproject.toml +++ b/workspace/aind_behavior_dynamic_foraging_curricula/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "aind-behavior-curriculum >= 0.0.38", "numpy>=2.4.2", "pydantic-settings", - "aind-behavior-dynamic-foraging==0.0.2rc30" + "aind-behavior-dynamic-foraging" ] [tool.uv.sources] diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml index d0ba0510..d3a8000b 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml @@ -23,7 +23,7 @@ readme = {file = "README.md", content-type = "text/markdown"} dependencies = [ "numpy>=2.4.2", "pydantic-settings", - "aind-behavior-dynamic-foraging==0.0.2rc30", + "aind-behavior-dynamic-foraging", "aind-data-schema>=2.6.0", "cyclopts>=4.10.0" ] From 148a9a894c778a789e45075659219a88164c5365 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 17 Jun 2026 08:57:30 -0700 Subject: [PATCH 34/45] removes acquisition and instrument file --- acquisition.json | 268 ----------------------------------------- instrument.json | 306 ----------------------------------------------- 2 files changed, 574 deletions(-) delete mode 100644 acquisition.json delete mode 100644 instrument.json diff --git a/acquisition.json b/acquisition.json deleted file mode 100644 index 24ce008d..00000000 --- a/acquisition.json +++ /dev/null @@ -1,268 +0,0 @@ -{ - "object_type": "Acquisition", - "describedBy": "https://raw.githubusercontent.com/AllenNeuralDynamics/aind-data-schema/main/src/aind_data_schema/core/acquisition.py", - "schema_version": "2.4.0", - "subject_id": "821586", - "specimen_id": null, - "acquisition_start_time": "2026-04-28T20:03:47.289698Z", - "acquisition_start_tz": 0, - "acquisition_end_time": "2026-06-16T17:24:03.642253Z", - "experimenters": [ - "micah.woodard" - ], - "protocol_id": null, - "ethics_review_id": null, - "instrument_id": "test_rig", - "acquisition_type": "AindDynamicForaging", - "notes": null, - "coordinate_system": null, - "calibrations": [ - { - "object_type": "Volume calibration", - "device_name": "water_valve_left", - "calibration_date": "2026-06-16T17:24:03.890836Z", - "description": "Volume measured for various solenoid opening times", - "protocol_id": null, - "measured_at": null, - "input": [ - 0.02, - 0.03, - 0.04 - ], - "input_unit": "second", - "repeats": null, - "output": [ - 0.00193, - 0.002846, - 0.003568 - ], - "output_unit": "milliliter", - "fit": { - "object_type": "Calibration fit", - "fit_type": "linear", - "fit_parameters": { - "date": null, - "measurements": [ - { - "valve_open_interval": 0.1, - "valve_open_time": 0.02, - "water_weight": [ - 0.386 - ], - "repeat_count": 200 - }, - { - "valve_open_interval": 0.1, - "valve_open_time": 0.03, - "water_weight": [ - 2.846 - ], - "repeat_count": 1000 - }, - { - "valve_open_interval": 0.1, - "valve_open_time": 0.04, - "water_weight": [ - 3.568 - ], - "repeat_count": 1000 - } - ], - "interval_average": { - "0.02": 0.00193, - "0.03": 0.002846, - "0.04": 0.003568 - }, - "slope": 0.0819, - "offset": 0.000294, - "r2": 0.999546, - "valid_domain": [ - 0.02, - 0.04 - ] - } - }, - "notes": null - }, - { - "object_type": "Volume calibration", - "device_name": "water_valve_right", - "calibration_date": "2026-06-16T17:24:03.913085Z", - "description": "Volume measured for various solenoid opening times", - "protocol_id": null, - "measured_at": null, - "input": [ - 0.02, - 0.03, - 0.04 - ], - "input_unit": "second", - "repeats": null, - "output": [ - 0.001894, - 0.002667, - 0.003356 - ], - "output_unit": "milliliter", - "fit": { - "object_type": "Calibration fit", - "fit_type": "linear", - "fit_parameters": { - "date": null, - "measurements": [ - { - "valve_open_interval": 0.1, - "valve_open_time": 0.02, - "water_weight": [ - 1.894 - ], - "repeat_count": 1000 - }, - { - "valve_open_interval": 0.1, - "valve_open_time": 0.03, - "water_weight": [ - 2.667 - ], - "repeat_count": 1000 - }, - { - "valve_open_interval": 0.1, - "valve_open_time": 0.04, - "water_weight": [ - 3.364 - ], - "repeat_count": 1000 - } - ], - "interval_average": { - "0.02": 0.001894, - "0.03": 0.002667, - "0.04": 0.003356 - }, - "slope": 0.0731, - "offset": 0.000432, - "r2": 0.999974, - "valid_domain": [ - 0.02, - 0.04 - ] - } - }, - "notes": null - } - ], - "maintenance": [], - "data_streams": [ - { - "object_type": "Data stream", - "stream_start_time": "2026-04-28T20:03:47.289698Z", - "stream_end_time": "2026-06-16T17:24:03.642253Z", - "modalities": [ - { - "name": "Behavior", - "abbreviation": "behavior" - } - ], - "code": [ - { - "object_type": "Code", - "url": "git@github.com:AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git", - "name": "Aind.Behavior.DynamicForaging", - "version": "e8681118c00201d92d70c139878eb8c9541e1bda", - "container": null, - "run_script": null, - "language": "Bonsai", - "language_version": "2.9.0", - "input_data": null, - "parameters": null, - "core_dependency": null - }, - { - "object_type": "Code", - "url": "git@github.com:AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git", - "name": "aind-behavior-dynamic-foraging", - "version": "e8681118c00201d92d70c139878eb8c9541e1bda", - "container": null, - "run_script": null, - "language": "Python", - "language_version": "3.13.11", - "input_data": null, - "parameters": null, - "core_dependency": null - } - ], - "notes": null, - "active_devices": [ - "BodyCamera", - "SideCamera", - "harp_behavior", - "harp_clock_generator", - "harp_sound_card", - "harp_environment_sensor", - "manipulator" - ], - "configurations": [], - "connections": [] - } - ], - "stimulus_epochs": [ - { - "object_type": "Stimulus epoch", - "stimulus_start_time": "2026-04-28T20:03:47.289698Z", - "stimulus_end_time": "2026-06-16T17:24:03.642253Z", - "stimulus_name": "GoCue", - "code": { - "object_type": "Code", - "url": "git@github.com:AllenNeuralDynamics/Aind.Behavior.DynamicForaging.git", - "name": "Aind.Behavior.DynamicForaging", - "version": "e8681118c00201d92d70c139878eb8c9541e1bda", - "container": null, - "run_script": null, - "language": "Bonsai", - "language_version": "2.9.0", - "input_data": null, - "parameters": null, - "core_dependency": null - }, - "stimulus_modalities": [ - "Auditory" - ], - "performance_metrics": { - "object_type": "Performance metrics", - "output_parameters": { - "foraging_efficiency_per_session": [ - 1.4866204162537167 - ], - "unignored_trials_per_session": [ - 527 - ], - "total_sessions": 1, - "consecutive_sessions_at_current_stage": 1, - "stage_name": "stage_2" - }, - "reward_consumed_during_epoch": null, - "reward_consumed_unit": null, - "trials_total": null, - "trials_finished": null, - "trials_rewarded": null - }, - "notes": null, - "active_devices": [], - "configurations": [], - "training_protocol_name": null, - "curriculum_status": "stage_2" - } - ], - "manipulations": [], - "subject_details": { - "object_type": "Acquisition subject details", - "animal_weight_prior": null, - "animal_weight_post": null, - "weight_unit": "gram", - "anaesthesia": null, - "mouse_platform_name": "tube", - "reward_consumed_total": "0.43600000000000033", - "reward_consumed_unit": "milliliter" - } -} \ No newline at end of file diff --git a/instrument.json b/instrument.json deleted file mode 100644 index f70bac07..00000000 --- a/instrument.json +++ /dev/null @@ -1,306 +0,0 @@ -{ - "object_type": "Instrument", - "describedBy": "https://raw.githubusercontent.com/AllenNeuralDynamics/aind-data-schema/main/src/aind_data_schema/core/instrument.py", - "schema_version": "2.2.3", - "location": null, - "instrument_id": "test_rig", - "modification_date": "2026-06-16", - "modalities": [ - { - "name": "Behavior", - "abbreviation": "behavior" - }, - { - "name": "Behavior videos", - "abbreviation": "behavior-videos" - } - ], - "calibrations": null, - "coordinate_system": { - "object_type": "Coordinate system", - "name": "RigCoordinateSystem", - "origin": "Origin", - "axes": [ - { - "object_type": "Axis", - "name": "X", - "direction": "Left_to_right" - }, - { - "object_type": "Axis", - "name": "Y", - "direction": "Front_to_back" - }, - { - "object_type": "Axis", - "name": "Z", - "direction": "Down_to_up" - } - ], - "axis_unit": "millimeter" - }, - "temperature_control": null, - "notes": null, - "connections": [ - { - "object_type": "Connection", - "source_device": "BehaviorBoard", - "source_port": null, - "target_device": "BodyCamera", - "target_port": null, - "send_and_receive": false - }, - { - "object_type": "Connection", - "source_device": "BehaviorBoard", - "source_port": null, - "target_device": "SideCamera", - "target_port": null, - "send_and_receive": false - } - ], - "components": [ - { - "object_type": "Camera assembly", - "relative_position": [ - "Right" - ], - "coordinate_system": null, - "transform": null, - "name": "BodyCameraAssembly", - "target": "Body", - "camera": { - "object_type": "Camera", - "name": "BodyCamera", - "serial_number": "23349426", - "manufacturer": { - "name": "Spinnaker", - "abbreviation": null, - "registry": null, - "registry_identifier": null - }, - "model": null, - "additional_settings": null, - "notes": null, - "detector_type": "Camera", - "data_interface": "Coax", - "cooling": "No cooling", - "frame_rate": null, - "frame_rate_unit": null, - "immersion": null, - "chroma": null, - "sensor_width": null, - "sensor_height": null, - "size_unit": "pixel", - "sensor_format": null, - "sensor_format_unit": null, - "bit_depth": null, - "bin_mode": "No binning", - "bin_width": null, - "bin_height": null, - "bin_unit": "pixel", - "gain": null, - "crop_offset_x": null, - "crop_offset_y": null, - "crop_width": null, - "crop_height": null, - "crop_unit": "pixel", - "recording_software": null, - "driver": null, - "driver_version": null - }, - "lens": { - "object_type": "Lens", - "name": "Lens A", - "serial_number": null, - "manufacturer": { - "name": "Fujinon", - "abbreviation": null, - "registry": null, - "registry_identifier": null - }, - "model": null, - "additional_settings": null, - "notes": null - }, - "filter": null - }, - { - "object_type": "Camera assembly", - "relative_position": [ - "Superior" - ], - "coordinate_system": null, - "transform": null, - "name": "SideCameraAssembly", - "target": "Face", - "camera": { - "object_type": "Camera", - "name": "SideCamera", - "serial_number": "23349424", - "manufacturer": { - "name": "Spinnaker", - "abbreviation": null, - "registry": null, - "registry_identifier": null - }, - "model": null, - "additional_settings": null, - "notes": null, - "detector_type": "Camera", - "data_interface": "Coax", - "cooling": "No cooling", - "frame_rate": null, - "frame_rate_unit": null, - "immersion": null, - "chroma": null, - "sensor_width": null, - "sensor_height": null, - "size_unit": "pixel", - "sensor_format": null, - "sensor_format_unit": null, - "bit_depth": null, - "bin_mode": "No binning", - "bin_width": null, - "bin_height": null, - "bin_unit": "pixel", - "gain": null, - "crop_offset_x": null, - "crop_offset_y": null, - "crop_width": null, - "crop_height": null, - "crop_unit": "pixel", - "recording_software": null, - "driver": null, - "driver_version": null - }, - "lens": { - "object_type": "Lens", - "name": "Lens A", - "serial_number": null, - "manufacturer": { - "name": "Fujinon", - "abbreviation": null, - "registry": null, - "registry_identifier": null - }, - "model": null, - "additional_settings": null, - "notes": null - }, - "filter": null - }, - { - "object_type": "Harp device", - "name": "BehaviorBoard", - "serial_number": null, - "manufacturer": { - "name": "Champalimaud Foundation", - "abbreviation": "Champalimaud", - "registry": "Research Organization Registry (ROR)", - "registry_identifier": "03g001n57" - }, - "model": null, - "additional_settings": null, - "notes": null, - "data_interface": "USB", - "channels": [], - "firmware_version": null, - "hardware_version": null, - "harp_device_type": { - "whoami": 1216, - "name": "Behavior" - }, - "core_version": null, - "tag_version": null, - "is_clock_generator": false - }, - { - "object_type": "Harp device", - "name": "ClockGenerator", - "serial_number": null, - "manufacturer": { - "name": "Open Ephys Production Site", - "abbreviation": "OEPS", - "registry": "Research Organization Registry (ROR)", - "registry_identifier": "007rkz355" - }, - "model": null, - "additional_settings": null, - "notes": null, - "data_interface": "USB", - "channels": [], - "firmware_version": null, - "hardware_version": null, - "harp_device_type": { - "whoami": 1404, - "name": "WhiteRabbit" - }, - "core_version": null, - "tag_version": null, - "is_clock_generator": true - }, - { - "object_type": "Harp device", - "name": "SoundCard", - "serial_number": null, - "manufacturer": { - "name": "Champalimaud Foundation", - "abbreviation": "Champalimaud", - "registry": "Research Organization Registry (ROR)", - "registry_identifier": "03g001n57" - }, - "model": null, - "additional_settings": null, - "notes": null, - "data_interface": "USB", - "channels": [], - "firmware_version": null, - "hardware_version": null, - "harp_device_type": { - "whoami": 1280, - "name": "SoundCard" - }, - "core_version": null, - "tag_version": null, - "is_clock_generator": false - }, - { - "object_type": "Harp device", - "name": "EnvironmentSensor", - "serial_number": null, - "manufacturer": { - "name": "Open Ephys Production Site", - "abbreviation": "OEPS", - "registry": "Research Organization Registry (ROR)", - "registry_identifier": "007rkz355" - }, - "model": null, - "additional_settings": null, - "notes": null, - "data_interface": "USB", - "channels": [], - "firmware_version": null, - "hardware_version": null, - "harp_device_type": { - "whoami": 1405, - "name": "EnvironmentSensor" - }, - "core_version": null, - "tag_version": null, - "is_clock_generator": false - }, - { - "object_type": "Motorized stage", - "name": "Manipulator", - "serial_number": null, - "manufacturer": null, - "model": null, - "additional_settings": null, - "notes": null, - "travel": "30", - "travel_unit": "millimeter", - "firmware": null - } - ] -} \ No newline at end of file From 9a0351446a6df6463aab6667adc89eae77efed93 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Tue, 23 Jun 2026 11:47:31 -0700 Subject: [PATCH 35/45] fixes based on review --- .../acquisition.py | 12 ++- .../instrument.py | 86 +++++++++++++++++-- 2 files changed, 91 insertions(+), 7 deletions(-) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py index 19750c56..5b81f6e6 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py @@ -144,7 +144,17 @@ def _map(self) -> Acquisition: # populate behavior epoch metrics = dataset["Behavior"]["Metrics"].data trainer_state = dataset["Behavior"]["TrainerState"].data - performance_metrics = PerformanceMetrics(output_parameters=metrics.model_dump()) + trial_outcomes = dataset["Behavior"]["SoftwareEvents"]["TrialOutcome"].data["data"].iloc + rewarded = sum(to["is_rewarded"] for to in trial_outcomes if to["trial"]["is_auto_response_right"] is None) + water = calculate_consumed_water(self.data_path) + performance_metrics = PerformanceMetrics( + reward_consumed_during_epoch=None if not water else Decimal(str(water)), + reward_consumed_unit=units.VolumeUnit.ML, + trials_total=trial_outcomes[:].shape[0], + trials_finished=metrics.unignored_trials_per_session[-1], + trials_rewarded=rewarded, + output_parameters=metrics.model_dump(), + ) stimulus_epoch = StimulusEpoch( stimulus_start_time=session_model.date, diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py index 7a79d816..ffffe3f9 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py @@ -3,7 +3,10 @@ from datetime import date from decimal import Decimal from pathlib import Path +import yaml +from aind_behavior_services.utils import get_fields_of_type, utcnow +from aind_behavior_services.rig import water_valve as abs_water_valve from aind_behavior_dynamic_foraging.data_contract import dataset as df_foraging_dataset from aind_behavior_dynamic_foraging.rig import AindDynamicForagingRig from aind_data_schema.components.connections import Connection @@ -19,7 +22,13 @@ Lens, MotorizedStage, SizeUnit, + CameraChroma, + Cooling, ) +from aind_data_schema.core.acquisition import CALIBRATIONS +from aind_data_schema.components.measurements import CalibrationFit, FitType, GenericModel, VolumeCalibration +from aind_data_schema.base import GenericModel +from aind_data_schema_models.units import FrequencyUnit, TimeUnit, VolumeUnit from aind_data_schema.core.instrument import Instrument from aind_data_schema_models.modalities import Modality from aind_data_schema_models.organizations import Organization @@ -45,6 +54,29 @@ def __init__( super().__init__() self._data_path = Path(data_path) + @staticmethod + def _get_water_calibration(rig_model: AindDynamicForagingRig) -> list[CALIBRATIONS]: + + water_calibrations = get_fields_of_type(rig_model, abs_water_valve.WaterValveCalibration) + vol_cal = [] + for device_name, wc in water_calibrations: + if device_name and wc.interval_average: + vol_cal.append( + VolumeCalibration( + device_name=device_name, + calibration_date=wc.date if wc.date else utcnow(), + input=list(wc.interval_average.keys()), + output=list(wc.interval_average.values()), + input_unit=TimeUnit.S, + output_unit=VolumeUnit.ML, + fit=CalibrationFit( + fit_type=FitType.LINEAR, + fit_parameters=GenericModel.model_validate(wc.model_dump()), + ), + ) + ) + return vol_cal + def rig_schema(self): return self.mapped @@ -82,7 +114,31 @@ def _map(self) -> Instrument: connections = [] # cameras + controller = rig.triggered_camera_controller + fps = float(controller.frame_rate) if controller.frame_rate else float("nan") for name, cam in rig.triggered_camera_controller.cameras.items(): + Camera( + name=name, + manufacturer=Organization.FLIR, + chroma=CameraChroma.BW, + cooling=Cooling.NO_COOLING, + data_interface=DataInterface.USB, + sensor_format="1/2.9", + sensor_format_unit=SizeUnit.IN, + sensor_width=720, + sensor_height=540, + model="Blackfly S BFS-U3-04S2M", + frame_rate=Decimal(str(fps)), + frame_rate_unit=FrequencyUnit.HZ, + gain=Decimal(str(cam.gain) if cam.gain is not None else "0"), + serial_number=cam.serial_number, + crop_offset_x=cam.region_of_interest.x if cam.region_of_interest.x > 0 else None, + crop_offset_y=cam.region_of_interest.y if cam.region_of_interest.y > 0 else None, + crop_width=cam.region_of_interest.width if cam.region_of_interest.width > 0 else None, + crop_height=cam.region_of_interest.height if cam.region_of_interest.height > 0 else None, + crop_unit=SizeUnit.PX, + additional_settings=GenericModel.model_validate(cam.model_dump()), + ) camera = Camera( name=name, serial_number=cam.serial_number, @@ -99,6 +155,7 @@ def _map(self) -> Instrument: components.append(assembly) # behavior board + behavior_board = dataset["Behavior"]["HarpBehavior"].load() components.append( HarpDevice( name="BehaviorBoard", @@ -106,20 +163,26 @@ def _map(self) -> Instrument: serial_number=rig.harp_behavior.serial_number, manufacturer=Organization.CHAMPALIMAUD, is_clock_generator=False, + firmware_version=behavior_board.device_reader.device.firmwareVersion, + hardware_version=behavior_board.device_reader.device.hardwareTargets, ) ) # clock generator + clock_generator = dataset["Behavior"]["HarpClockGenerator"].load() components.append( HarpDevice( name="ClockGenerator", harp_device_type=HarpDeviceType.WHITERABBIT, serial_number=rig.harp_clock_generator.serial_number, is_clock_generator=True, + firmware_version=clock_generator.device_reader.device.firmwareVersion, + hardware_version=clock_generator.device_reader.device.hardwareTargets, ) ) # sound card + sound_card = dataset["Behavior"]["HarpSoundCard"].load() components.append( HarpDevice( name="SoundCard", @@ -127,54 +190,64 @@ def _map(self) -> Instrument: serial_number=rig.harp_sound_card.serial_number, manufacturer=Organization.CHAMPALIMAUD, is_clock_generator=False, + firmware_version=sound_card.device_reader.device.firmwareVersion, + hardware_version=sound_card.device_reader.device.hardwareTargets, ) ) # optional harp devices if rig.harp_lickometer_left: + left = dataset["Behavior"]["HarpLickometerLeft"].load() components.append( HarpDevice( name="LickometerLeft", harp_device_type=HarpDeviceType.LICKETYSPLIT, serial_number=rig.harp_lickometer_left.serial_number, is_clock_generator=False, + firmware_version=left.device_reader.device.firmwareVersion, + hardware_version=left.device_reader.device.hardwareTargets, ) ) if rig.harp_lickometer_right: + right = dataset["Behavior"]["HarpLickometerRight"].load() components.append( HarpDevice( name="LickometerRight", serial_number=rig.harp_lickometer_right.serial_number, harp_device_type=HarpDeviceType.LICKETYSPLIT, is_clock_generator=False, + firmware_version=right.device_reader.device.firmwareVersion, + hardware_version=right.device_reader.device.hardwareTargets, ) ) if rig.harp_sniff_detector: + sniff = dataset["Behavior"]["HarpSniffDetector"].load() components.append( HarpDevice( name="SniffDetector", harp_device_type=HarpDeviceType.SNIFFDETECTOR, serial_number=rig.harp_sniff_detector.serial_number, is_clock_generator=False, + firmware_version=sniff.device_reader.device.firmwareVersion, + hardware_version=sniff.device_reader.device.hardwareTargets, ) ) if rig.harp_environment_sensor: + env_sen = dataset["Behavior"]["HarpEnvironmentSensor"].load() components.append( HarpDevice( name="EnvironmentSensor", harp_device_type=HarpDeviceType.ENVIRONMENTSENSOR, serial_number=rig.harp_environment_sensor.serial_number, is_clock_generator=False, + firmware_version=env_sen.device_reader.device.firmwareVersion, + hardware_version=env_sen.device_reader.device.hardwareTargets, ) ) - # manipulator + # manipulator\ components.append( - MotorizedStage( - name="Manipulator", - serial_number=rig.manipulator.serial_number, - travel=Decimal("30"), - ) + MotorizedStage(name="Manipulator", serial_number=rig.manipulator.serial_number, travel=Decimal("30")) ) # connections @@ -202,4 +275,5 @@ def _map(self) -> Instrument: ), components=components, connections=connections, + calibrations=self._get_water_calibration(rig), ) From 66bdd633f2e1040d9d31e02b8ec371efe451a66b Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Tue, 23 Jun 2026 11:48:45 -0700 Subject: [PATCH 36/45] lints --- .../instrument.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py index ffffe3f9..c2a1ca9a 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py @@ -3,35 +3,33 @@ from datetime import date from decimal import Decimal from pathlib import Path -import yaml -from aind_behavior_services.utils import get_fields_of_type, utcnow -from aind_behavior_services.rig import water_valve as abs_water_valve from aind_behavior_dynamic_foraging.data_contract import dataset as df_foraging_dataset from aind_behavior_dynamic_foraging.rig import AindDynamicForagingRig +from aind_behavior_services.rig import water_valve as abs_water_valve +from aind_behavior_services.utils import get_fields_of_type, utcnow from aind_data_schema.components.connections import Connection from aind_data_schema.components.coordinates import Axis, AxisName, CoordinateSystem, Direction, Origin from aind_data_schema.components.devices import ( AnatomicalRelative, Camera, CameraAssembly, + CameraChroma, CameraTarget, + Cooling, DataInterface, HarpDevice, HarpDeviceType, Lens, MotorizedStage, SizeUnit, - CameraChroma, - Cooling, ) -from aind_data_schema.core.acquisition import CALIBRATIONS from aind_data_schema.components.measurements import CalibrationFit, FitType, GenericModel, VolumeCalibration -from aind_data_schema.base import GenericModel -from aind_data_schema_models.units import FrequencyUnit, TimeUnit, VolumeUnit +from aind_data_schema.core.acquisition import CALIBRATIONS from aind_data_schema.core.instrument import Instrument from aind_data_schema_models.modalities import Modality from aind_data_schema_models.organizations import Organization +from aind_data_schema_models.units import FrequencyUnit, TimeUnit, VolumeUnit from clabe.data_mapper.aind_data_schema import AindDataSchemaRigDataMapper logger = logging.getLogger(__name__) From 873d4ba836575cca86dec8fe1931e058d32964f9 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Thu, 25 Jun 2026 10:08:27 -0700 Subject: [PATCH 37/45] saves metadata to data directory --- .../src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py index d053432a..51b2dfbc 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py @@ -38,8 +38,8 @@ def cli_cmd(self): assert session_mapper.mapped is not None assert rig_mapper.mapped is not None - acquisition.write_standard_file(output_directory=Path(self.repository_path), filename_suffix=self.suffix) - instrument.write_standard_file(output_directory=Path(self.repository_path), filename_suffix=self.suffix) + acquisition.write_standard_file(output_directory=Path(self.data_path), filename_suffix=self.suffix) + instrument.write_standard_file(output_directory=Path(self.data_path), filename_suffix=self.suffix) logger.info( "Mapping completed! Saved acquisition.json, instrument.json to %s", From b50ba62b5f90ce6e7e3512026acef6f79d19ba51 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Thu, 25 Jun 2026 10:14:47 -0700 Subject: [PATCH 38/45] fixes log --- .../src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py index 51b2dfbc..4cbb8acb 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/cli.py @@ -43,7 +43,7 @@ def cli_cmd(self): logger.info( "Mapping completed! Saved acquisition.json, instrument.json to %s", - self.repository_path, + self.data_path, ) From 39e38ca4953a1bab8bd5e61c94b67fac0485fa9d Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Fri, 26 Jun 2026 10:39:19 -0700 Subject: [PATCH 39/45] Revert "Merge branch 'fix-pulse-time' into feat-metadata-mapping" This reverts commit 7bcc0abec793b824805b1f1c8a911d3abf4adb80, reversing changes made to 89c51f6f85bb6facab3b2ff2d4486a6104256599. --- src/Extensions/OperationControl.bonsai | 86 +++++--------------------- src/Extensions/ValveUi.bonsai | 39 +----------- src/main.bonsai | 56 ++++------------- 3 files changed, 28 insertions(+), 153 deletions(-) diff --git a/src/Extensions/OperationControl.bonsai b/src/Extensions/OperationControl.bonsai index cf3db7bf..7931b7d4 100644 --- a/src/Extensions/OperationControl.bonsai +++ b/src/Extensions/OperationControl.bonsai @@ -347,35 +347,12 @@ SetRewardAmount - - SetRightRewardAmount - - - - - - - Source1 - - - - - - - - TaskLogicParameters RewardSize.LeftValueVolume - - - - - Item2 - RigSchema @@ -409,34 +386,12 @@ SetIsRightValveMs - - SetRightRewardAmount - - - - - - Source1 - - - - - - - - TaskLogicParameters RewardSize.RightValueVolume - - - - - Item2 - RigSchema @@ -473,36 +428,27 @@ - - - - - - + + + + + + - + + - - - - + + - - - + + + + - + + - - - - - - - - - - diff --git a/src/Extensions/ValveUi.bonsai b/src/Extensions/ValveUi.bonsai index 818f430c..e4cc8a0a 100644 --- a/src/Extensions/ValveUi.bonsai +++ b/src/Extensions/ValveUi.bonsai @@ -344,19 +344,6 @@ - - - - - - - - true - - - - SetRightRewardAmount - @@ -394,16 +381,12 @@ - - - - @@ -831,14 +814,6 @@ - - - false - - - - SetRightRewardAmount - IsFlushingValveRight @@ -1000,14 +975,6 @@ - - - true - - - - SetRightRewardAmount - @@ -1061,12 +1028,8 @@ - - - - - + diff --git a/src/main.bonsai b/src/main.bonsai index b3d0aefd..a5e1dbe9 100644 --- a/src/main.bonsai +++ b/src/main.bonsai @@ -7,7 +7,6 @@ xmlns:p2="clr-namespace:;assembly=Extensions" xmlns:p3="clr-namespace:System.Reactive;assembly=System.Reactive.Core" xmlns:p4="clr-namespace:AllenNeuralDynamics.AindBehaviorServices.DataTypes;assembly=AllenNeuralDynamics.AindBehaviorServices" - xmlns:sys="clr-namespace:System;assembly=mscorlib" xmlns:scr="clr-namespace:Bonsai.Scripting.Expressions;assembly=Bonsai.Scripting.Expressions" xmlns="https://bonsai-rx.org/2018/workflow"> @@ -151,9 +150,6 @@ GlobalTrial - - SetRightRewardAmount - GlobalTrialMetrics @@ -208,32 +204,6 @@ ExperimentState - - SetRightRewardAmount - - - - 1 - - - - - true - - - - SetRightRewardAmount - - - - 1 - - - - - false - - -5 @@ -263,25 +233,21 @@ + - - - + + + - - + + - - - - + + + + - - - - - - + From 74376dc25bbb3bab8b4cc5f285dfe96fcf59e932 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Tue, 30 Jun 2026 10:02:02 -0700 Subject: [PATCH 40/45] fixes bonsai directory --- .../acquisition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py index 5b81f6e6..cbb6f33e 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/acquisition.py @@ -239,7 +239,7 @@ def _get_camera_config(name: str, camera: abs_camera.CameraTypes, repository: gi def _get_bonsai_as_code(repository: git.Repo) -> Code: - bonsai_folder = Path(Path(repository.working_tree_dir) / "bonsai" / "bonsai.exe").parent + bonsai_folder = Path(Path(repository.working_tree_dir) / ".bonsai" / "bonsai.exe").parent bonsai_env = data_mapper_helpers.snapshot_bonsai_environment(bonsai_folder / "bonsai.config") bonsai_version = bonsai_env.get("Bonsai", "unknown") assert isinstance(repository, git.Repo) From 3d0c5831eb71e00d4d67b91390d4fa14af6c5761 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Tue, 30 Jun 2026 10:05:41 -0700 Subject: [PATCH 41/45] regenerates --- schema/aind_behavior_dynamic_foraging.json | 10 +++++----- .../AindBehaviorDynamicForaging.Generated.cs | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/schema/aind_behavior_dynamic_foraging.json b/schema/aind_behavior_dynamic_foraging.json index b887ffa7..521d0d4e 100644 --- a/schema/aind_behavior_dynamic_foraging.json +++ b/schema/aind_behavior_dynamic_foraging.json @@ -3,7 +3,7 @@ "AindDynamicForagingRig": { "properties": { "aind_behavior_services_pkg_version": { - "default": "0.13.5", + "default": "0.13.7", "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", "title": "aind_behavior_services package version", "type": "string" @@ -194,7 +194,7 @@ "title": "Rng Seed" }, "aind_behavior_services_pkg_version": { - "default": "0.13.5", + "default": "0.13.7", "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", "title": "aind_behavior_services package version", "type": "string" @@ -2801,14 +2801,14 @@ "Session": { "properties": { "aind_behavior_services_pkg_version": { - "default": "0.13.5", + "default": "0.13.7", "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", "title": "aind_behavior_services package version", "type": "string" }, "version": { - "const": "0.13.5", - "default": "0.13.5", + "const": "0.13.7", + "default": "0.13.7", "title": "Version", "type": "string" }, diff --git a/src/Extensions/AindBehaviorDynamicForaging.Generated.cs b/src/Extensions/AindBehaviorDynamicForaging.Generated.cs index 52a7c6c6..3a227bd4 100644 --- a/src/Extensions/AindBehaviorDynamicForaging.Generated.cs +++ b/src/Extensions/AindBehaviorDynamicForaging.Generated.cs @@ -49,7 +49,7 @@ public partial class AindDynamicForagingRig public AindDynamicForagingRig() { - _aindBehaviorServicesPkgVersion = "0.13.5"; + _aindBehaviorServicesPkgVersion = "0.13.7"; _version = "0.0.2"; _triggeredCameraController = new CameraControllerSpinnakerCamera(); _harpBehavior = new HarpBehavior(); @@ -590,7 +590,7 @@ public partial class AindDynamicForagingTaskParameters public AindDynamicForagingTaskParameters() { - _aindBehaviorServicesPkgVersion = "0.13.5"; + _aindBehaviorServicesPkgVersion = "0.13.7"; _rewardSize = new RewardSize(); _trialGenerator = new TrialGeneratorSpec(); } @@ -4918,8 +4918,8 @@ public partial class Session public Session() { - _aindBehaviorServicesPkgVersion = "0.13.5"; - _version = "0.13.5"; + _aindBehaviorServicesPkgVersion = "0.13.7"; + _version = "0.13.7"; _experimenter = new System.Collections.Generic.List(); _allowDirtyRepo = false; _skipHardwareValidation = false; From d5ab37bf70cf9ed715b8377f2b2171eeb6321813 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Tue, 30 Jun 2026 10:09:04 -0700 Subject: [PATCH 42/45] regenerates curriculum --- schema/coupled_baiting.json | 12 ++++++------ schema/uncoupled.json | 12 ++++++------ schema/uncoupled_baiting.json | 12 ++++++------ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/schema/coupled_baiting.json b/schema/coupled_baiting.json index 80179c46..5dd004c1 100644 --- a/schema/coupled_baiting.json +++ b/schema/coupled_baiting.json @@ -11,7 +11,7 @@ "description": "", "task_parameters": { "rng_seed": null, - "aind_behavior_services_pkg_version": "0.13.5", + "aind_behavior_services_pkg_version": "0.13.7", "reward_size": { "right_value_volume": 4.0, "left_value_volume": 4.0 @@ -186,7 +186,7 @@ "description": "", "task_parameters": { "rng_seed": null, - "aind_behavior_services_pkg_version": "0.13.5", + "aind_behavior_services_pkg_version": "0.13.7", "reward_size": { "right_value_volume": 2.0, "left_value_volume": 2.0 @@ -289,7 +289,7 @@ "description": "", "task_parameters": { "rng_seed": null, - "aind_behavior_services_pkg_version": "0.13.5", + "aind_behavior_services_pkg_version": "0.13.7", "reward_size": { "right_value_volume": 2.0, "left_value_volume": 2.0 @@ -392,7 +392,7 @@ "description": "", "task_parameters": { "rng_seed": null, - "aind_behavior_services_pkg_version": "0.13.5", + "aind_behavior_services_pkg_version": "0.13.7", "reward_size": { "right_value_volume": 2.0, "left_value_volume": 2.0 @@ -495,7 +495,7 @@ "description": "", "task_parameters": { "rng_seed": null, - "aind_behavior_services_pkg_version": "0.13.5", + "aind_behavior_services_pkg_version": "0.13.7", "reward_size": { "right_value_volume": 2.0, "left_value_volume": 2.0 @@ -606,7 +606,7 @@ "description": "", "task_parameters": { "rng_seed": null, - "aind_behavior_services_pkg_version": "0.13.5", + "aind_behavior_services_pkg_version": "0.13.7", "reward_size": { "right_value_volume": 2.0, "left_value_volume": 2.0 diff --git a/schema/uncoupled.json b/schema/uncoupled.json index 9b5caf9b..7a9b0193 100644 --- a/schema/uncoupled.json +++ b/schema/uncoupled.json @@ -11,7 +11,7 @@ "description": "", "task_parameters": { "rng_seed": null, - "aind_behavior_services_pkg_version": "0.13.5", + "aind_behavior_services_pkg_version": "0.13.7", "reward_size": { "right_value_volume": 4.0, "left_value_volume": 4.0 @@ -186,7 +186,7 @@ "description": "", "task_parameters": { "rng_seed": null, - "aind_behavior_services_pkg_version": "0.13.5", + "aind_behavior_services_pkg_version": "0.13.7", "reward_size": { "right_value_volume": 2.0, "left_value_volume": 2.0 @@ -289,7 +289,7 @@ "description": "", "task_parameters": { "rng_seed": null, - "aind_behavior_services_pkg_version": "0.13.5", + "aind_behavior_services_pkg_version": "0.13.7", "reward_size": { "right_value_volume": 2.0, "left_value_volume": 2.0 @@ -392,7 +392,7 @@ "description": "", "task_parameters": { "rng_seed": null, - "aind_behavior_services_pkg_version": "0.13.5", + "aind_behavior_services_pkg_version": "0.13.7", "reward_size": { "right_value_volume": 2.0, "left_value_volume": 2.0 @@ -481,7 +481,7 @@ "description": "", "task_parameters": { "rng_seed": null, - "aind_behavior_services_pkg_version": "0.13.5", + "aind_behavior_services_pkg_version": "0.13.7", "reward_size": { "right_value_volume": 2.0, "left_value_volume": 2.0 @@ -566,7 +566,7 @@ "description": "", "task_parameters": { "rng_seed": null, - "aind_behavior_services_pkg_version": "0.13.5", + "aind_behavior_services_pkg_version": "0.13.7", "reward_size": { "right_value_volume": 2.0, "left_value_volume": 2.0 diff --git a/schema/uncoupled_baiting.json b/schema/uncoupled_baiting.json index 1ceddb14..ba001566 100644 --- a/schema/uncoupled_baiting.json +++ b/schema/uncoupled_baiting.json @@ -11,7 +11,7 @@ "description": "", "task_parameters": { "rng_seed": null, - "aind_behavior_services_pkg_version": "0.13.5", + "aind_behavior_services_pkg_version": "0.13.7", "reward_size": { "right_value_volume": 4.0, "left_value_volume": 4.0 @@ -186,7 +186,7 @@ "description": "", "task_parameters": { "rng_seed": null, - "aind_behavior_services_pkg_version": "0.13.5", + "aind_behavior_services_pkg_version": "0.13.7", "reward_size": { "right_value_volume": 2.0, "left_value_volume": 2.0 @@ -289,7 +289,7 @@ "description": "", "task_parameters": { "rng_seed": null, - "aind_behavior_services_pkg_version": "0.13.5", + "aind_behavior_services_pkg_version": "0.13.7", "reward_size": { "right_value_volume": 2.0, "left_value_volume": 2.0 @@ -392,7 +392,7 @@ "description": "", "task_parameters": { "rng_seed": null, - "aind_behavior_services_pkg_version": "0.13.5", + "aind_behavior_services_pkg_version": "0.13.7", "reward_size": { "right_value_volume": 2.0, "left_value_volume": 2.0 @@ -481,7 +481,7 @@ "description": "", "task_parameters": { "rng_seed": null, - "aind_behavior_services_pkg_version": "0.13.5", + "aind_behavior_services_pkg_version": "0.13.7", "reward_size": { "right_value_volume": 2.0, "left_value_volume": 2.0 @@ -570,7 +570,7 @@ "description": "", "task_parameters": { "rng_seed": null, - "aind_behavior_services_pkg_version": "0.13.5", + "aind_behavior_services_pkg_version": "0.13.7", "reward_size": { "right_value_volume": 2.0, "left_value_volume": 2.0 From 11d24ccd8fda121e61b090fedfc7ed631c8b23da Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 1 Jul 2026 09:31:27 -0700 Subject: [PATCH 43/45] fixes camera issues --- pyproject.toml | 1 + uv.lock | 66 +++++++++++++++++++ .../pyproject.toml | 3 +- .../instrument.py | 27 ++++---- 4 files changed, 84 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4a717646..62d69e4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ readme = {file = "README.md", content-type = "text/markdown"} dependencies = [ "aind_behavior_services>=0.13.5", + "contraqctor>=0.5.8", "pydantic-settings", "scikit-learn>=1.8.0", ] diff --git a/uv.lock b/uv.lock index 01b900af..a0dfa692 100644 --- a/uv.lock +++ b/uv.lock @@ -52,6 +52,7 @@ version = "0.0.2" source = { editable = "." } dependencies = [ { name = "aind-behavior-services" }, + { name = "contraqctor" }, { name = "pydantic-settings" }, { name = "scikit-learn" }, ] @@ -80,6 +81,7 @@ docs = [ [package.metadata] requires-dist = [ { name = "aind-behavior-services", specifier = ">=0.13.5" }, + { name = "contraqctor", specifier = ">=0.5.8" }, { name = "contraqctor", marker = "extra == 'data'", specifier = ">=0.5.8" }, { name = "pydantic-settings" }, { name = "scikit-learn", specifier = ">=1.8.0" }, @@ -157,6 +159,7 @@ version = "0.0.1" source = { editable = "workspace/aind_behavior_dynamic_foraging_metadata_mapper" } dependencies = [ { name = "aind-behavior-dynamic-foraging" }, + { name = "aind-clabe" }, { name = "aind-data-schema" }, { name = "cyclopts" }, { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, @@ -182,6 +185,7 @@ docs = [ [package.metadata] requires-dist = [ { name = "aind-behavior-dynamic-foraging", editable = "." }, + { name = "aind-clabe", specifier = ">=0.11.2" }, { name = "aind-data-schema", specifier = ">=2.6.0" }, { name = "cyclopts", specifier = ">=4.10.0" }, { name = "numpy", specifier = ">=2.4.2" }, @@ -219,6 +223,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/bf/423d28364d564008a33d40d7aad1fbbed5e028b5bda39ed5cafc41651d7e/aind_behavior_services-0.13.7-py3-none-any.whl", hash = "sha256:ab57927ce283cb07a47c4b02eecee8038421f8292c5efbeeaaf907fd6a69eb08", size = 39614, upload-time = "2026-04-18T22:20:02.941Z" }, ] +[[package]] +name = "aind-clabe" +version = "0.11.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aind-behavior-services" }, + { name = "gitpython" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "requests" }, + { name = "rich" }, + { name = "semver" }, + { name = "textual" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/83/6150ad46286259a2296f6281eaa439aec60cfa9730cf884f27ec9600c080/aind_clabe-0.11.2.tar.gz", hash = "sha256:dce48bef3796377a6ab8773497cba1f44a77e3d08eb6cb8490414c283b68ac50", size = 101974, upload-time = "2026-06-30T17:03:46.045Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/eb/5c23a6b19448e9392a49e9426e3e9115872661d65fac310313043aeb91bd/aind_clabe-0.11.2-py3-none-any.whl", hash = "sha256:1b538dd087003c4a4676168e74643e7d428bcd8dcb56b948dc27205263ede5ce", size = 131595, upload-time = "2026-06-30T17:03:44.886Z" }, +] + [[package]] name = "aind-data-schema" version = "2.8.1" @@ -1020,6 +1043,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/dd/8050c947d435c8d4bc94e3252f4d8bb8a76cfb424f043a8680be637a57f1/kiwisolver-1.5.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:59cd8683f575d96df5bb48f6add94afc055012c29e28124fcae2b63661b9efb1", size = 73558, upload-time = "2026-03-09T13:15:52.112Z" }, ] +[[package]] +name = "linkify-it-py" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "uc-micro-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/c9/06ea13676ef354f0af6169587ae292d3e2406e212876a413bf9eece4eb23/linkify_it_py-2.1.0.tar.gz", hash = "sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b", size = 29158, upload-time = "2026-03-01T07:48:47.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, +] + [[package]] name = "markdown" version = "3.10.2" @@ -1041,6 +1076,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] +[package.optional-dependencies] +linkify = [ + { name = "linkify-it-py" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -2515,6 +2555,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, ] +[[package]] +name = "textual" +version = "8.2.8" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py", extra = ["linkify"] }, + { name = "mdit-py-plugins" }, + { name = "platformdirs" }, + { name = "pygments" }, + { name = "rich" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/21/39a76b01bd5eea82a04baaca7580e105d8c59450df03998345bb2cfb307b/textual-8.2.8.tar.gz", hash = "sha256:3f106a9fbc73e39dd266c9712432087de78a6d644084c7c241d6a25c3169115b", size = 1860502, upload-time = "2026-06-30T06:51:24.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/be/35261223d9416a0751cdff1c7b4a6f881387218a12d439fe22fefebc8c04/textual-8.2.8-py3-none-any.whl", hash = "sha256:267375fd402dc8d981457212efa71f0e3365fd17bba144ba9bb3ed7563cb374a", size = 731418, upload-time = "2026-06-30T06:51:26.364Z" }, +] + [[package]] name = "threadpoolctl" version = "3.6.0" @@ -2632,6 +2689,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, ] +[[package]] +name = "uc-micro-py" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/78/67/9a363818028526e2d4579334460df777115bdec1bb77c08f9db88f6389f2/uc_micro_py-2.0.0.tar.gz", hash = "sha256:c53691e495c8db60e16ffc4861a35469b0ba0821fe409a8a7a0a71864d33a811", size = 6611, upload-time = "2026-03-01T06:31:27.526Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, +] + [[package]] name = "urllib3" version = "2.7.0" diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml index d3a8000b..436b2a13 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/pyproject.toml @@ -25,7 +25,8 @@ dependencies = [ "pydantic-settings", "aind-behavior-dynamic-foraging", "aind-data-schema>=2.6.0", - "cyclopts>=4.10.0" + "cyclopts>=4.10.0", + "aind-clabe>=0.11.2", ] [tool.uv.sources] diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py index c2a1ca9a..7f115dd6 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py @@ -115,7 +115,7 @@ def _map(self) -> Instrument: controller = rig.triggered_camera_controller fps = float(controller.frame_rate) if controller.frame_rate else float("nan") for name, cam in rig.triggered_camera_controller.cameras.items(): - Camera( + camera = Camera( name=name, manufacturer=Organization.FLIR, chroma=CameraChroma.BW, @@ -137,12 +137,6 @@ def _map(self) -> Instrument: crop_unit=SizeUnit.PX, additional_settings=GenericModel.model_validate(cam.model_dump()), ) - camera = Camera( - name=name, - serial_number=cam.serial_number, - manufacturer=Organization.SPINNAKER, - data_interface=DataInterface.COAX, - ) assembly = CameraAssembly( name=f"{name}Assembly", camera=camera, @@ -161,8 +155,9 @@ def _map(self) -> Instrument: serial_number=rig.harp_behavior.serial_number, manufacturer=Organization.CHAMPALIMAUD, is_clock_generator=False, - firmware_version=behavior_board.device_reader.device.firmwareVersion, - hardware_version=behavior_board.device_reader.device.hardwareTargets, + firmware_version=behavior_board["FirmwareVersionHigh"], + hardware_version=behavior_board["HardwareVersionHigh"], + core_version=behavior_board["CoreVersionHigh"], ) ) @@ -174,8 +169,9 @@ def _map(self) -> Instrument: harp_device_type=HarpDeviceType.WHITERABBIT, serial_number=rig.harp_clock_generator.serial_number, is_clock_generator=True, - firmware_version=clock_generator.device_reader.device.firmwareVersion, - hardware_version=clock_generator.device_reader.device.hardwareTargets, + firmware_version=clock_generator["FirmwareVersionHigh"], + hardware_version=clock_generator["HardwareVersionHigh"], + core_version=clock_generator["CoreVersionHigh"], ) ) @@ -245,7 +241,14 @@ def _map(self) -> Instrument: # manipulator\ components.append( - MotorizedStage(name="Manipulator", serial_number=rig.manipulator.serial_number, travel=Decimal("30")) + MotorizedStage( + name="motorized_stage", + manufacturer=Organization.AIND, + model="328-300-00", + travel=Decimal("30"), + travel_unit=SizeUnit.CM, + notes="This stage is driven by the manipulator device.", + ) ) # connections From e55c0ea6c7883a2cd71faaf4e150c6fdeb0f8806 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 1 Jul 2026 09:41:31 -0700 Subject: [PATCH 44/45] undoes version --- .../instrument.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py index 7f115dd6..423e6545 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py @@ -155,9 +155,8 @@ def _map(self) -> Instrument: serial_number=rig.harp_behavior.serial_number, manufacturer=Organization.CHAMPALIMAUD, is_clock_generator=False, - firmware_version=behavior_board["FirmwareVersionHigh"], - hardware_version=behavior_board["HardwareVersionHigh"], - core_version=behavior_board["CoreVersionHigh"], + firmware_version=behavior_board.device.firmwareVersion, + hardware_version=behavior_board.device.hardwareTargets, ) ) @@ -169,9 +168,8 @@ def _map(self) -> Instrument: harp_device_type=HarpDeviceType.WHITERABBIT, serial_number=rig.harp_clock_generator.serial_number, is_clock_generator=True, - firmware_version=clock_generator["FirmwareVersionHigh"], - hardware_version=clock_generator["HardwareVersionHigh"], - core_version=clock_generator["CoreVersionHigh"], + firmware_version=clock_generator.device.firmwareVersion, + hardware_version=clock_generator.device.hardwareTargets, ) ) From fe710ab801a6623b72d29625d03c40db311c63c5 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 1 Jul 2026 09:42:44 -0700 Subject: [PATCH 45/45] fixes device_reader --- .../instrument.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py index 423e6545..c5b6036d 100644 --- a/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py +++ b/workspace/aind_behavior_dynamic_foraging_metadata_mapper/src/aind_behavior_dynamic_foraging_metadata_mapper/instrument.py @@ -155,8 +155,8 @@ def _map(self) -> Instrument: serial_number=rig.harp_behavior.serial_number, manufacturer=Organization.CHAMPALIMAUD, is_clock_generator=False, - firmware_version=behavior_board.device.firmwareVersion, - hardware_version=behavior_board.device.hardwareTargets, + firmware_version=behavior_board.device_reader.device.firmwareVersion, + hardware_version=behavior_board.device_reader.device.hardwareTargets, ) ) @@ -168,8 +168,8 @@ def _map(self) -> Instrument: harp_device_type=HarpDeviceType.WHITERABBIT, serial_number=rig.harp_clock_generator.serial_number, is_clock_generator=True, - firmware_version=clock_generator.device.firmwareVersion, - hardware_version=clock_generator.device.hardwareTargets, + firmware_version=clock_generator.device_reader.device.firmwareVersion, + hardware_version=clock_generator.device_reader.device.hardwareTargets, ) )