From eba6406bedf037dc1e43733d52164ea76866ffdc Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Mon, 30 Mar 2026 09:20:31 -0700 Subject: [PATCH 01/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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/46] 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 a92e79ea4655a0828f5e44debe01f148e371dfb3 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Tue, 9 Jun 2026 18:57:17 -0700 Subject: [PATCH 32/46] adds SetRewardAmount subject" --- src/Extensions/OperationControl.bonsai | 71 +- src/Extensions/ValveUi.bonsai | 12 + src/main.bonsai | 1069 +++++++++++++++++++++++- 3 files changed, 1109 insertions(+), 43 deletions(-) diff --git a/src/Extensions/OperationControl.bonsai b/src/Extensions/OperationControl.bonsai index 7931b7d4..1e050ef0 100644 --- a/src/Extensions/OperationControl.bonsai +++ b/src/Extensions/OperationControl.bonsai @@ -343,10 +343,16 @@ GiveRewardRight + + SetRewardAmount + SetRewardAmount + + Source1 + TaskLogicParameters @@ -427,28 +433,28 @@ - - - - - - - - - + + + + + + + + - - - - - - - - - + + + + + + + + + + @@ -607,27 +613,28 @@ - - - - + + + - - + + + - - - - + + + + - - + + - - + + + diff --git a/src/Extensions/ValveUi.bonsai b/src/Extensions/ValveUi.bonsai index e4cc8a0a..e55a3e5b 100644 --- a/src/Extensions/ValveUi.bonsai +++ b/src/Extensions/ValveUi.bonsai @@ -344,6 +344,9 @@ + + SetRewardAmount + @@ -387,6 +390,7 @@ + @@ -777,6 +781,9 @@ 1 + + SetRewardAmount + @@ -808,6 +815,7 @@ + @@ -938,6 +946,9 @@ 1 + + SetRewardAmount + @@ -969,6 +980,7 @@ + diff --git a/src/main.bonsai b/src/main.bonsai index a5e1dbe9..82ef917f 100644 --- a/src/main.bonsai +++ b/src/main.bonsai @@ -8,6 +8,11 @@ xmlns:p3="clr-namespace:System.Reactive;assembly=System.Reactive.Core" xmlns:p4="clr-namespace:AllenNeuralDynamics.AindBehaviorServices.DataTypes;assembly=AllenNeuralDynamics.AindBehaviorServices" xmlns:scr="clr-namespace:Bonsai.Scripting.Expressions;assembly=Bonsai.Scripting.Expressions" + xmlns:p5="clr-namespace:AllenNeuralDynamics.Core;assembly=AllenNeuralDynamics.Core" + xmlns:gui="clr-namespace:Bonsai.Gui;assembly=Bonsai.Gui" + xmlns:sys="clr-namespace:System;assembly=mscorlib" + xmlns:p6="clr-namespace:AllenNeuralDynamics.Core.Design;assembly=AllenNeuralDynamics.Core.Design" + xmlns:ui="clr-namespace:Bonsai.Design;assembly=Bonsai.Design" xmlns="https://bonsai-rx.org/2018/workflow"> @@ -112,6 +117,19 @@ IsWaveformUploadDone + + + false + + + + + 1 + + + + SetRewardAmount + @@ -233,21 +251,23 @@ - - + + - - + - + - + - - - - + + + + + + + @@ -392,7 +412,1034 @@ - + + Visualizers + + + + + 0 + + + + + + + TrialPlots + + + + GlobalTrialOutcome + + + 16 + + + + SoftwareEvent + + + IsRightLickEvent + + + + + + Source1 + + + Value + + + + + + + + + + + + LickRight + + + + IsRightLickEvent + + + + + + Source1 + + + Value + + + + + + + + + + + + + + LickLeft + + + + GiveRewardRight + + + + + + Source1 + + + + + + + + + + + WaterRight + + + + GiveRewardRight + + + + + + + Source1 + + + + + + + + + + + WaterLeft + + + + + + + 16 + 20 + + + QuiscentPeriod + Gold + 0.3 + + + ResponsePeriod + SandyBrown + 0.3 + + + RewardConsumptionPeriod + RosyBrown + 0.3 + + + ItiPeriod + Green + 0.3 + + + + + LickLeft + Red + 0.9 + 6 + Down + + + LickRight + Blue + 0.1 + 6 + Up + + + IsRightTriggerQuickRetract + DarkCyan + 0.5 + 6 + Plus + + + DeliverSecondaryReinforcer + Sienna + 0.5 + 6 + Plus + + + WaterRight + Blue + 0.2 + 6 + Circle + + + WaterLeft + Red + 0.8 + 6 + Circle + + + + 5 + + + + true + true + 1 + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ManualOverrides + + + + OffsetControl + + + + + 0.05 + + + + + 1 + + + + BumpSize + + + ManipulatorBiasTracker + + + {0:F2} + + + + + + + + true + true + Microsoft Sans Serif, 20.25pt + 0.00 + + + + Left + true + true + Microsoft Sans Serif, 15.75pt + ◀️ + + + BumpLeft + + + + Source1 + + + BumpSize + + + + + + Item2 + + + + -1 + + + + ManualSpoutDelta + + + + + + + + + + + + + + + + Right + true + true + Microsoft Sans Serif, 15.75pt + + + + BumpRight + + + + Source1 + + + BumpSize + + + + + + Item2 + + + ManualSpoutDelta + + + + + + + + + + + + + + + true + true + 2 + 1 + + + + + + + ExperimentState + + + + + + + + false + true + Microsoft Sans Serif, 22.125pt + 1 + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Spout offset + true + true + Microsoft Sans Serif, 15.75pt + Spout offset + + + + GiveWaterUI + + + + Left + true + true + 💧◀ + + + TriggerLeft + + + + Source1 + + + + false + + + + GiveManualWaterRight + + + + + + + + + + + + + Right + true + true + 💧▶ + + + TriggerRight + + + + Source1 + + + + true + + + + GiveManualWaterRight + + + + + + + + + + + + + ManualWater + true + true + 2 + 1 + + + Percent + 0.5 + + + Percent + 0.5 + + + + + + + + GiveManualWaterRight + + + GiveManualWaterRight + + + GiveRewardRight + + + + + + + + + + + + + + + + + + 💧 + true + true + Microsoft Sans Serif, 8.25pt + + + + + ForceAutoWater + + + + GlobalAutoWaterState + + + !it.IsLeft + + + + + + + + SetLeft + true + true + 🎣◀ + + + UpdateGlobalState + + + + Source1 + + + GlobalAutoWaterState + + + + + + Item2 + + + + + + it.SetLeft() + + + GlobalAutoWaterState + + + + + + + + + + + + + + + + + GlobalAutoWaterState + + + !it.IsRight + + + + + + + + SetRight + true + true + 🎣▶ + + + UpdateGlobalState + + + + Source1 + + + GlobalAutoWaterState + + + + + + Item2 + + + + + + it.SetRight() + + + GlobalAutoWaterState + + + + + + + + + + + + + + + + + GlobalAutoWaterState + + + it.HasValue + + + + + + + + Reset + false + true + + + + UpdateGlobalState + + + + Source1 + + + GlobalAutoWaterState + + + + + + Item2 + + + + + + it.Reset() + + + GlobalAutoWaterState + + + + + + + + + + + + + + + + + ForceAutoWater + true + true + 3 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 🎣 + true + true + Microsoft Sans Serif, 8.25pt + + + + + true + true + Microsoft Sans Serif, 15.75pt + + + + Manual Control + true + true + Microsoft Sans Serif, 36pt + 1 + 2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + LauncherControl + + + + + + + + ExperimentState + + + + + + + + + Start + true + true + Start Experiment + + + StartExperimentToggleButton + + + + End + true + true + End Experiment + + + EndExperimentButton + + + + LauncherControl + true + true + Microsoft Sans Serif, 26pt + 2 + 4 + + + + + + + + + + + StartExperimentToggleButton + + + + 1 + + + + + StartExperimentShortcut + + + EndExperimentButton + + + + 1 + + + + + EndExperiment + + + StartExperiment + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + true + + + + + true + true + 1 + 2 + + + + + + + + true + true + 3 + 1 + + + + + + + + + TriggeredCamerasStream + + + + + + + + + + + true + true + 2 + 1 + + + + + + + true + true + 1 + 2 + + + Percent + 0.75 + + + Percent + 0.25 + + + + + + + + GlobalTrial + + + 2 + 16 + + + + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + DynamicForaging + true + + + Maximized + + + + + + + + From 6747b8567ac36c2bac02d56746b1314165ef06eb Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Tue, 9 Jun 2026 18:58:45 -0700 Subject: [PATCH 33/46] regroups visualizers --- src/main.bonsai | 1034 +---------------------------------------------- 1 file changed, 1 insertion(+), 1033 deletions(-) diff --git a/src/main.bonsai b/src/main.bonsai index 82ef917f..866dd917 100644 --- a/src/main.bonsai +++ b/src/main.bonsai @@ -8,11 +8,6 @@ xmlns:p3="clr-namespace:System.Reactive;assembly=System.Reactive.Core" xmlns:p4="clr-namespace:AllenNeuralDynamics.AindBehaviorServices.DataTypes;assembly=AllenNeuralDynamics.AindBehaviorServices" xmlns:scr="clr-namespace:Bonsai.Scripting.Expressions;assembly=Bonsai.Scripting.Expressions" - xmlns:p5="clr-namespace:AllenNeuralDynamics.Core;assembly=AllenNeuralDynamics.Core" - xmlns:gui="clr-namespace:Bonsai.Gui;assembly=Bonsai.Gui" - xmlns:sys="clr-namespace:System;assembly=mscorlib" - xmlns:p6="clr-namespace:AllenNeuralDynamics.Core.Design;assembly=AllenNeuralDynamics.Core.Design" - xmlns:ui="clr-namespace:Bonsai.Design;assembly=Bonsai.Design" xmlns="https://bonsai-rx.org/2018/workflow"> @@ -412,1034 +407,7 @@ - - Visualizers - - - - - 0 - - - - - - - TrialPlots - - - - GlobalTrialOutcome - - - 16 - - - - SoftwareEvent - - - IsRightLickEvent - - - - - - Source1 - - - Value - - - - - - - - - - - - LickRight - - - - IsRightLickEvent - - - - - - Source1 - - - Value - - - - - - - - - - - - - - LickLeft - - - - GiveRewardRight - - - - - - Source1 - - - - - - - - - - - WaterRight - - - - GiveRewardRight - - - - - - - Source1 - - - - - - - - - - - WaterLeft - - - - - - - 16 - 20 - - - QuiscentPeriod - Gold - 0.3 - - - ResponsePeriod - SandyBrown - 0.3 - - - RewardConsumptionPeriod - RosyBrown - 0.3 - - - ItiPeriod - Green - 0.3 - - - - - LickLeft - Red - 0.9 - 6 - Down - - - LickRight - Blue - 0.1 - 6 - Up - - - IsRightTriggerQuickRetract - DarkCyan - 0.5 - 6 - Plus - - - DeliverSecondaryReinforcer - Sienna - 0.5 - 6 - Plus - - - WaterRight - Blue - 0.2 - 6 - Circle - - - WaterLeft - Red - 0.8 - 6 - Circle - - - - 5 - - - - true - true - 1 - 2 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ManualOverrides - - - - OffsetControl - - - - - 0.05 - - - - - 1 - - - - BumpSize - - - ManipulatorBiasTracker - - - {0:F2} - - - - - - - - true - true - Microsoft Sans Serif, 20.25pt - 0.00 - - - - Left - true - true - Microsoft Sans Serif, 15.75pt - ◀️ - - - BumpLeft - - - - Source1 - - - BumpSize - - - - - - Item2 - - - - -1 - - - - ManualSpoutDelta - - - - - - - - - - - - - - - - Right - true - true - Microsoft Sans Serif, 15.75pt - - - - BumpRight - - - - Source1 - - - BumpSize - - - - - - Item2 - - - ManualSpoutDelta - - - - - - - - - - - - - - - true - true - 2 - 1 - - - - - - - ExperimentState - - - - - - - - false - true - Microsoft Sans Serif, 22.125pt - 1 - 2 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Spout offset - true - true - Microsoft Sans Serif, 15.75pt - Spout offset - - - - GiveWaterUI - - - - Left - true - true - 💧◀ - - - TriggerLeft - - - - Source1 - - - - false - - - - GiveManualWaterRight - - - - - - - - - - - - - Right - true - true - 💧▶ - - - TriggerRight - - - - Source1 - - - - true - - - - GiveManualWaterRight - - - - - - - - - - - - - ManualWater - true - true - 2 - 1 - - - Percent - 0.5 - - - Percent - 0.5 - - - - - - - - GiveManualWaterRight - - - GiveManualWaterRight - - - GiveRewardRight - - - - - - - - - - - - - - - - - - 💧 - true - true - Microsoft Sans Serif, 8.25pt - - - - - ForceAutoWater - - - - GlobalAutoWaterState - - - !it.IsLeft - - - - - - - - SetLeft - true - true - 🎣◀ - - - UpdateGlobalState - - - - Source1 - - - GlobalAutoWaterState - - - - - - Item2 - - - - - - it.SetLeft() - - - GlobalAutoWaterState - - - - - - - - - - - - - - - - - GlobalAutoWaterState - - - !it.IsRight - - - - - - - - SetRight - true - true - 🎣▶ - - - UpdateGlobalState - - - - Source1 - - - GlobalAutoWaterState - - - - - - Item2 - - - - - - it.SetRight() - - - GlobalAutoWaterState - - - - - - - - - - - - - - - - - GlobalAutoWaterState - - - it.HasValue - - - - - - - - Reset - false - true - - - - UpdateGlobalState - - - - Source1 - - - GlobalAutoWaterState - - - - - - Item2 - - - - - - it.Reset() - - - GlobalAutoWaterState - - - - - - - - - - - - - - - - - ForceAutoWater - true - true - 3 - 1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 🎣 - true - true - Microsoft Sans Serif, 8.25pt - - - - - true - true - Microsoft Sans Serif, 15.75pt - - - - Manual Control - true - true - Microsoft Sans Serif, 36pt - 1 - 2 - - - - - - - - - - - - - - - - - - - - - - - - - - - - LauncherControl - - - - - - - - ExperimentState - - - - - - - - - Start - true - true - Start Experiment - - - StartExperimentToggleButton - - - - End - true - true - End Experiment - - - EndExperimentButton - - - - LauncherControl - true - true - Microsoft Sans Serif, 26pt - 2 - 4 - - - - - - - - - - - StartExperimentToggleButton - - - - 1 - - - - - StartExperimentShortcut - - - EndExperimentButton - - - - 1 - - - - - EndExperiment - - - StartExperiment - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - true - true - - - - - true - true - 1 - 2 - - - - - - - - true - true - 3 - 1 - - - - - - - - - TriggeredCamerasStream - - - - - - - - - - - true - true - 2 - 1 - - - - - - - true - true - 1 - 2 - - - Percent - 0.75 - - - Percent - 0.25 - - - - - - - - GlobalTrial - - - 2 - 16 - - - - true - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - DynamicForaging - true - - - Maximized - - - - - - - - + From 07306f3acb02762775154b058034fbaae699484d Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 10 Jun 2026 03:45:00 -0700 Subject: [PATCH 34/46] sets set reward time to unit --- src/Extensions/OperationControl.bonsai | 93 +++++++++++++++----------- src/Extensions/ValveUi.bonsai | 6 ++ src/main.bonsai | 6 +- 3 files changed, 62 insertions(+), 43 deletions(-) diff --git a/src/Extensions/OperationControl.bonsai b/src/Extensions/OperationControl.bonsai index 1e050ef0..462f8523 100644 --- a/src/Extensions/OperationControl.bonsai +++ b/src/Extensions/OperationControl.bonsai @@ -343,15 +343,12 @@ GiveRewardRight - - SetRewardAmount - SetRewardAmount - - Source1 + + SetRewardAmount TaskLogicParameters @@ -359,6 +356,12 @@ RewardSize.LeftValueVolume + + + + + Item2 + RigSchema @@ -366,7 +369,7 @@ Calibration.WaterValveLeft - + RewardToTime @@ -392,12 +395,21 @@ SetIsRightValveMs + + SetRewardAmount + TaskLogicParameters RewardSize.RightValueVolume + + + + + Item2 + RigSchema @@ -405,7 +417,7 @@ Calibration.WaterValveRight - + RewardToTime @@ -422,7 +434,7 @@ - true + false @@ -433,28 +445,34 @@ + - + - + - - - - - + + + + + + - - - - - - + + + + + - - + + + + + + + @@ -613,28 +631,27 @@ - - - + + + + - - - + + - - - + + + + - - - + + - - - + + diff --git a/src/Extensions/ValveUi.bonsai b/src/Extensions/ValveUi.bonsai index e55a3e5b..a09a3fcd 100644 --- a/src/Extensions/ValveUi.bonsai +++ b/src/Extensions/ValveUi.bonsai @@ -344,6 +344,7 @@ + SetRewardAmount @@ -391,6 +392,7 @@ + @@ -781,6 +783,7 @@ 1 + SetRewardAmount @@ -816,6 +819,7 @@ + @@ -946,6 +950,7 @@ 1 + SetRewardAmount @@ -981,6 +986,7 @@ + diff --git a/src/main.bonsai b/src/main.bonsai index 866dd917..4b751d09 100644 --- a/src/main.bonsai +++ b/src/main.bonsai @@ -112,11 +112,7 @@ IsWaveformUploadDone - - - false - - + 1 From e1f8538693c3a5978528e3e89eeda3917bb4b004 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 10 Jun 2026 04:10:36 -0700 Subject: [PATCH 35/46] replaces zip with WithLatestFrom --- src/Extensions/OperationControl.bonsai | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Extensions/OperationControl.bonsai b/src/Extensions/OperationControl.bonsai index 462f8523..e359f8dd 100644 --- a/src/Extensions/OperationControl.bonsai +++ b/src/Extensions/OperationControl.bonsai @@ -357,7 +357,7 @@ RewardSize.LeftValueVolume - + Item2 @@ -369,7 +369,7 @@ Calibration.WaterValveLeft - + RewardToTime @@ -405,7 +405,7 @@ RewardSize.RightValueVolume - + Item2 @@ -417,7 +417,7 @@ Calibration.WaterValveRight - + RewardToTime From dcf60cdd86ac92f6f2264e39b355191882be91e2 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 10 Jun 2026 04:14:45 -0700 Subject: [PATCH 36/46] fixes copy paste error --- src/Extensions/OperationControl.bonsai | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Extensions/OperationControl.bonsai b/src/Extensions/OperationControl.bonsai index e359f8dd..2017f64a 100644 --- a/src/Extensions/OperationControl.bonsai +++ b/src/Extensions/OperationControl.bonsai @@ -434,7 +434,7 @@ - false + true From f16300c9c8578723042f3c3a47f7e40a9f5bfcb2 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 10 Jun 2026 04:47:00 -0700 Subject: [PATCH 37/46] sets right and left reward amount independently --- src/Extensions/OperationControl.bonsai | 76 ++++++++++++++++++-------- src/Extensions/ValveUi.bonsai | 48 ++++++++++------ src/main.bonsai | 63 ++++++++++++++------- 3 files changed, 128 insertions(+), 59 deletions(-) diff --git a/src/Extensions/OperationControl.bonsai b/src/Extensions/OperationControl.bonsai index 2017f64a..cf3db7bf 100644 --- a/src/Extensions/OperationControl.bonsai +++ b/src/Extensions/OperationControl.bonsai @@ -348,7 +348,21 @@ - SetRewardAmount + SetRightRewardAmount + + + + + + + Source1 + + + + + + + TaskLogicParameters @@ -396,7 +410,20 @@ SetIsRightValveMs - SetRewardAmount + SetRightRewardAmount + + + + + + Source1 + + + + + + + TaskLogicParameters @@ -445,34 +472,37 @@ - + - + - + - + - - - - - + + + + - - - - - - - - - + + + + + + + + + + - - - + + + + + + diff --git a/src/Extensions/ValveUi.bonsai b/src/Extensions/ValveUi.bonsai index a09a3fcd..5ab01bc6 100644 --- a/src/Extensions/ValveUi.bonsai +++ b/src/Extensions/ValveUi.bonsai @@ -344,9 +344,16 @@ - - SetRewardAmount + SetRightRewardAmount + + + + true + + + + SetRightRewardAmount @@ -392,7 +399,8 @@ - + + @@ -783,10 +791,6 @@ 1 - - - SetRewardAmount - @@ -818,14 +822,20 @@ - - + + + false + + + + SetRightRewardAmount + IsFlushingValveRight @@ -950,10 +960,6 @@ 1 - - - SetRewardAmount - @@ -985,14 +991,20 @@ - - + + + true + + + + SetRightRewardAmount + @@ -1046,8 +1058,12 @@ + - + + + + diff --git a/src/main.bonsai b/src/main.bonsai index 4b751d09..b3d0aefd 100644 --- a/src/main.bonsai +++ b/src/main.bonsai @@ -7,6 +7,7 @@ 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"> @@ -112,15 +113,6 @@ IsWaveformUploadDone - - - - 1 - - - - SetRewardAmount - @@ -159,6 +151,9 @@ GlobalTrial + + SetRightRewardAmount + GlobalTrialMetrics @@ -213,6 +208,32 @@ ExperimentState + + SetRightRewardAmount + + + + 1 + + + + + true + + + + SetRightRewardAmount + + + + 1 + + + + + false + + -5 @@ -242,23 +263,25 @@ - - - + + + - - + + - - + + - - - + + - + + + + From 842b5283dfd60aec6116941155c4dd5e812a102d Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Wed, 10 Jun 2026 05:15:13 -0700 Subject: [PATCH 38/46] resets calibration independently --- src/Extensions/ValveUi.bonsai | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Extensions/ValveUi.bonsai b/src/Extensions/ValveUi.bonsai index 5ab01bc6..818f430c 100644 --- a/src/Extensions/ValveUi.bonsai +++ b/src/Extensions/ValveUi.bonsai @@ -344,8 +344,10 @@ - - SetRightRewardAmount + + + + @@ -392,14 +394,15 @@ + - + From e8681118c00201d92d70c139878eb8c9541e1bda Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Tue, 16 Jun 2026 10:20:43 -0700 Subject: [PATCH 39/46] 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 40/46] 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 41/46] 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 42/46] 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 43/46] 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 44/46] 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 45/46] 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 46/46] 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 @@ + - - - + + + - - + + - - - - + + + + - - - - - - +