From eba6406bedf037dc1e43733d52164ea76866ffdc Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Mon, 30 Mar 2026 09:20:31 -0700 Subject: [PATCH 01/28] 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 20e4ca1..9e725b2 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 3f2e238..4bba0ad 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 b8b3b56..d8832a0 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 7f0a7ae..908e586 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 0000000..e69de29 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 0000000..132e3b6 --- /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 0000000..e69de29 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 0000000..2fc22fb --- /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 0000000..132aa8f --- /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/28] 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 2fc22fb..3a2d78f 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 132aa8f..8e8887c 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/28] 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 0000000..db8952e --- /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 8e8887c..0d226bb 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/28] 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 e69de29..6fbccdb 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/28] 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 6fbccdb..0425c72 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/28] 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 d8832a0..c776f3d 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 132e3b6..3932060 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 0425c72..2a12ebe 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 3a2d78f..f68a017 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 db8952e..c0d521c 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 0d226bb..c91ecff 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/28] 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 f68a017..b902699 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/28] 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 b902699..cec218d 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/28] 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 cec218d..02c448a 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 c0d521c..adb903d 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 c91ecff..3ab6c0a 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/28] 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 02c448a..16db7d5 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 adb903d..f4d5b76 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 3ab6c0a..03453fb 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/28] 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 4bba0ad..b88f99b 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 2a12ebe..6cc6967 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 16db7d5..fa66318 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 f4d5b76..13fd7ed 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 03453fb..8f25c05 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/28] 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 f6288ac..d460a6d 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 5fab0e0..e3d8061 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 93c37db..b098669 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/28] 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 66ee583..6050287 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/28] 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 e3d8061..7d5cae5 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/28] 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 d460a6d..fae67ee 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 7d5cae5..2009618 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/28] 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 0112c95..809bb83 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/28] 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 809bb83..3d37de9 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 995d81a..942926f 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/28] 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 0000000..ab7819d --- /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 fa66318..25ab936 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 0000000..0cfa5ba --- /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 13fd7ed..fe5a4ce 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 8f25c05..bb4d4f0 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/28] 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 3932060..88bf85e 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/28] 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 0cfa5ba..f8b64d5 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/28] 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 f8b64d5..fd2ffae 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/28] 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 25ab936..64eb3dd 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 fd2ffae..04f3bf2 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 fe5a4ce..64ffd27 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/28] 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 88bf85e..7571e5d 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 6cc6967..938adb6 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 64eb3dd..b64b5dd 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 04f3bf2..4217652 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 64ffd27..0000000 --- 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/28] 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 2009618..1780e75 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 b098669..34ba90f 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/28] 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 1780e75..ceddd4d 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/28] 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 ceddd4d..6c0c281 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/28] 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 3d37de9..2e093b7 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/28] 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 0b5daae..0943e38 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):