Skip to content

Commit 291bd6b

Browse files
authored
Merge pull request #466 from AllenNeuralDynamics/feat-update-aind-data-schema
Update `aind-data-schema` package version to 2.2.0
2 parents ca02ddd + 4981fe8 commit 291bd6b

File tree

9 files changed

+182
-185
lines changed

9 files changed

+182
-185
lines changed

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ data = ["contraqctor>=0.5.3, <0.6.0"]
3232

3333
launcher = [
3434
"aind-clabe[aind-services]>=0.9",
35-
"aind-data-schema>=2",
35+
"aind-data-schema>=2.2",
3636
"aind_behavior_vr_foraging[data]",
3737
]
3838

@@ -42,7 +42,7 @@ dev = [
4242
"aind_behavior_vr_foraging[launcher]",
4343
"aind_behavior_vr_foraging[data]",
4444
"ruff",
45-
"codespell"
45+
"codespell",
4646
]
4747

4848
docs = [

src/aind_behavior_vr_foraging/cli.py

Lines changed: 4 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,14 @@
1-
import os
21
import typing as t
3-
from pathlib import Path
42

5-
from pydantic import AwareDatetime, Field, RootModel
6-
from pydantic_settings import BaseSettings, CliApp, CliPositionalArg, CliSubCommand
3+
from pydantic import Field, RootModel
4+
from pydantic_settings import BaseSettings, CliApp, CliSubCommand
75

86
from aind_behavior_vr_foraging import __semver__, regenerate
7+
from aind_behavior_vr_foraging.data_mappers import DataMapperCli
8+
from aind_behavior_vr_foraging.data_qc import DataQcCli
99
from aind_behavior_vr_foraging.launcher import ClabeCli
1010

1111

12-
class DataMapperCli(BaseSettings, cli_kebab_case=True):
13-
data_path: os.PathLike = Field(description="Path to the session data directory.")
14-
repo_path: os.PathLike = Field(
15-
default=Path("."), description="Path to the repository. By default it will use the current directory."
16-
)
17-
curriculum_suggestion: t.Optional[os.PathLike] = Field(
18-
default=None, description="Path to curriculum suggestion file."
19-
)
20-
session_end_time: AwareDatetime | None = Field(
21-
default=None,
22-
description="End time of the session in ISO format. If not provided, will use the time the data mapping is run.",
23-
)
24-
25-
def cli_cmd(self):
26-
from aind_behavior_vr_foraging.data_mappers import cli_cmd
27-
28-
return cli_cmd(self)
29-
30-
31-
class DataQcCli(BaseSettings, cli_kebab_case=True):
32-
data_path: CliPositionalArg[os.PathLike] = Field(description="Path to the session data directory.")
33-
version: str = Field(default=__semver__, description="Version of the dataset.")
34-
report_path: Path | None = Field(
35-
default=None, description="Path to save the Html QC report. If not provided, report is not saved."
36-
)
37-
38-
def cli_cmd(self):
39-
from aind_behavior_vr_foraging.data_qc import cli_cmd
40-
41-
return cli_cmd(self)
42-
43-
4412
class VersionCli(RootModel):
4513
root: t.Any
4614

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,50 @@
11
import logging
2+
import os
3+
import typing as t
24
from pathlib import Path
3-
from typing import TYPE_CHECKING
45

5-
from ._rig import AindRigDataMapper
6-
from ._session import AindSessionDataMapper
6+
from pydantic import AwareDatetime, Field
7+
from pydantic_settings import BaseSettings
78

89
logger = logging.getLogger(__name__)
910

10-
if TYPE_CHECKING:
11-
from aind_behavior_vr_foraging.cli import DataMapperCli
1211

13-
14-
def cli_cmd(cli_settings: "DataMapperCli"):
15-
"""Generate aind-data-schema metadata for the VR Foraging dataset located at the specified path."""
16-
session_mapped = AindSessionDataMapper(
17-
data_path=Path(cli_settings.data_path),
18-
repo_path=Path(cli_settings.repo_path),
19-
session_end_time=cli_settings.session_end_time,
20-
).map()
21-
22-
rig_mapped = AindRigDataMapper(data_path=Path(cli_settings.data_path)).map()
23-
24-
assert session_mapped is not None
25-
26-
session_mapped.instrument_id = rig_mapped.instrument_id
27-
logger.info("Writing session.json to %s", cli_settings.data_path)
28-
session_mapped.write_standard_file(Path(cli_settings.data_path))
29-
logger.info("Writing rig.json to %s", cli_settings.data_path)
30-
rig_mapped.write_standard_file(Path(cli_settings.data_path))
31-
logger.info("Mapping completed!")
12+
class DataMapperCli(BaseSettings, cli_kebab_case=True):
13+
data_path: os.PathLike = Field(description="Path to the session data directory.")
14+
repo_path: os.PathLike = Field(
15+
default=Path("."), description="Path to the repository. By default it will use the current directory."
16+
)
17+
curriculum_suggestion: t.Optional[os.PathLike] = Field(
18+
default=None, description="Path to curriculum suggestion file."
19+
)
20+
session_end_time: AwareDatetime | None = Field(
21+
default=None,
22+
description="End time of the session in ISO format. If not provided, will use the time the data mapping is run.",
23+
)
24+
suffix: t.Optional[str] = Field(default="vrforaging", description="Suffix to append to the output filenames.")
25+
26+
def cli_cmd(self):
27+
"""Generate aind-data-schema metadata for the VR Foraging dataset located at the specified path."""
28+
from ._rig import AindInstrumentDataMapper
29+
from ._session import AindAcquisitionDataMapper
30+
31+
session_mapper = AindAcquisitionDataMapper(
32+
data_path=Path(self.data_path),
33+
repo_path=Path(self.repo_path),
34+
session_end_time=self.session_end_time,
35+
)
36+
session_mapper.map()
37+
38+
rig_mapper = AindInstrumentDataMapper(data_path=Path(self.data_path))
39+
rig_mapper.map()
40+
41+
assert session_mapper.mapped is not None
42+
assert rig_mapper.mapped is not None
43+
44+
session_mapper.mapped.instrument_id = rig_mapper.mapped.instrument_id
45+
session_mapper.mapped.write_standard_file(output_directory=Path(self.data_path), filename_suffix=self.suffix)
46+
rig_mapper.mapped.write_standard_file(output_directory=Path(self.data_path), filename_suffix=self.suffix)
47+
logger.info(
48+
"Mapping completed! Saved acquisition.json and instrument.json to %s",
49+
self.data_path,
50+
)

src/aind_behavior_vr_foraging/data_mappers/_rig.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def get_spawned_device(self, name: str) -> devices.Device:
3939
raise ValueError(f"Device {name} not found in spawned devices of {self.device_name}")
4040

4141

42-
class AindRigDataMapper(ads.AindDataSchemaRigDataMapper):
42+
class AindInstrumentDataMapper(ads.AindDataSchemaRigDataMapper):
4343
def __init__(
4444
self,
4545
data_path: os.PathLike,
@@ -55,9 +55,6 @@ def rig_schema(self):
5555
def session_name(self):
5656
raise NotImplementedError("Method not implemented.")
5757

58-
def write_standard_file(self) -> None:
59-
self.mapped.write_standard_file(self._data_path)
60-
6158
def map(self) -> instrument.Instrument:
6259
logger.info("Mapping aind-data-schema Rig.")
6360
self._mapped = self._map(self._data_path)

src/aind_behavior_vr_foraging/data_mappers/_session.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
logger = logging.getLogger(__name__)
2929

3030

31-
class AindSessionDataMapper(ads.AindDataSchemaSessionDataMapper):
31+
class AindAcquisitionDataMapper(ads.AindDataSchemaSessionDataMapper):
3232
def __init__(
3333
self,
3434
data_path: os.PathLike,
@@ -124,9 +124,6 @@ def _map(self) -> acquisition.Acquisition:
124124
)
125125
return aind_data_schema_session
126126

127-
def write_standard_file(self) -> None:
128-
self.mapped.write_standard_file(Path(self._data_path))
129-
130127
def _get_subject_details(self) -> acquisition.AcquisitionSubjectDetails:
131128
return acquisition.AcquisitionSubjectDetails(
132129
mouse_platform_name=TrackedDevices.WHEEL,
Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,32 @@
1+
import logging
2+
import os
13
from pathlib import Path
2-
from typing import TYPE_CHECKING
34

4-
if TYPE_CHECKING:
5-
from ..cli import DataQcCli
5+
from pydantic import Field
6+
from pydantic_settings import BaseSettings, CliPositionalArg
67

8+
from aind_behavior_vr_foraging import __semver__
79

8-
def cli_cmd(cli_settings: "DataQcCli"):
9-
"""Run data quality checks on the VR Foraging dataset located at the specified path."""
10-
from ..data_contract import dataset
11-
from .data_qc import make_qc_runner
10+
logger = logging.getLogger(__name__)
1211

13-
vr_dataset = dataset(Path(cli_settings.data_path), cli_settings.version)
14-
runner = make_qc_runner(vr_dataset)
15-
results = runner.run_all_with_progress()
16-
if report_path := cli_settings.report_path:
17-
from contraqctor.qc.reporters import HtmlReporter
1812

19-
reporter = HtmlReporter(output_path=report_path)
20-
reporter.report_results(results, serialize_context_exportable_obj=True)
13+
class DataQcCli(BaseSettings, cli_kebab_case=True):
14+
data_path: CliPositionalArg[os.PathLike] = Field(description="Path to the session data directory.")
15+
version: str = Field(default=__semver__, description="Version of the dataset.")
16+
report_path: Path | None = Field(
17+
default=None, description="Path to save the Html QC report. If not provided, report is not saved."
18+
)
19+
20+
def cli_cmd(self):
21+
"""Run data quality checks on the VR Foraging dataset located at the specified path."""
22+
from ..data_contract import dataset
23+
from .data_qc import make_qc_runner
24+
25+
vr_dataset = dataset(Path(self.data_path), self.version)
26+
runner = make_qc_runner(vr_dataset)
27+
results = runner.run_all_with_progress()
28+
if report_path := self.report_path:
29+
from contraqctor.qc.reporters import HtmlReporter
30+
31+
reporter = HtmlReporter(output_path=report_path)
32+
reporter.report_results(results, serialize_context_exportable_obj=True)

src/aind_behavior_vr_foraging/launcher.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from aind_behavior_services.calibration.aind_manipulator import ManipulatorPosition
66
from aind_behavior_services.session import AindBehaviorSessionModel
7+
from aind_behavior_services.utils import utcnow
78
from clabe import resource_monitor
89
from clabe.apps import (
910
AindBehaviorServicesBonsaiApp,
@@ -19,7 +20,7 @@
1920
from pydantic_settings import CliApp
2021

2122
from aind_behavior_vr_foraging import data_contract
22-
from aind_behavior_vr_foraging.data_mappers import AindRigDataMapper, AindSessionDataMapper
23+
from aind_behavior_vr_foraging.data_mappers import DataMapperCli
2324
from aind_behavior_vr_foraging.rig import AindVrForagingRig
2425
from aind_behavior_vr_foraging.task_logic import AindVrForagingTaskLogic
2526

@@ -77,6 +78,7 @@ async def experiment(launcher: Launcher) -> None:
7778

7879
# Curriculum
7980
suggestion: CurriculumSuggestion | None = None
81+
suggestion_path: Path | None = None
8082
if not (
8183
(picker.trainer_state is None)
8284
or (picker.trainer_state.is_on_curriculum is False)
@@ -91,20 +93,19 @@ async def experiment(launcher: Launcher) -> None:
9193
await trainer.run_async()
9294
suggestion = trainer.process_suggestion()
9395
# Dump suggestion for debugging (for now, but we will prob remove this later)
94-
_dump_suggestion(suggestion, launcher.session_directory)
96+
suggestion_path = _dump_suggestion(suggestion, launcher.session_directory)
9597
# Push updated trainer state back to the database
9698
picker.push_new_suggestion(suggestion.trainer_state)
9799

98100
# Mappers
99101
assert launcher.repository.working_tree_dir is not None
100-
ads_session = AindSessionDataMapper(
102+
103+
DataMapperCli(
101104
data_path=launcher.session_directory,
102105
repo_path=launcher.repository.working_tree_dir, # type: ignore[arg-type]
103-
curriculum_suggestion=suggestion,
104-
).map()
105-
ads_session.write_standard_file(launcher.session_directory)
106-
ads_rig = AindRigDataMapper(data_path=launcher.session_directory).map()
107-
ads_rig.write_standard_file(launcher.session_directory)
106+
curriculum_suggestion=suggestion_path,
107+
session_end_time=utcnow(),
108+
).cli_cmd()
108109

109110
# Run data qc
110111
if picker.ui_helper.prompt_yes_no_question("Would you like to generate a qc report?"):
@@ -142,10 +143,12 @@ async def experiment(launcher: Launcher) -> None:
142143
return
143144

144145

145-
def _dump_suggestion(suggestion: CurriculumSuggestion, session_directory: Path) -> None:
146+
def _dump_suggestion(suggestion: CurriculumSuggestion, session_directory: Path) -> Path:
146147
logger.info(f"Dumping curriculum suggestion to: {session_directory / 'Behavior' / 'Logs' / 'suggestion.json'}")
147-
with open(session_directory / "Behavior" / "Logs" / "suggestion.json", "w", encoding="utf-8") as f:
148+
suggestion_path = session_directory / "Behavior" / "Logs" / "suggestion.json"
149+
with open(suggestion_path, "w", encoding="utf-8") as f:
148150
f.write(suggestion.model_dump_json(indent=2))
151+
return suggestion_path
149152

150153

151154
class ByAnimalManipulatorModifier(ByAnimalModifier[AindVrForagingRig]):

tests/test_aind_data_mapper.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from aind_data_schema.core import acquisition, instrument
1010
from aind_data_schema.utils import compatibility_check
1111

12-
from aind_behavior_vr_foraging.data_mappers import AindRigDataMapper, AindSessionDataMapper
12+
from aind_behavior_vr_foraging.data_mappers._rig import AindInstrumentDataMapper
13+
from aind_behavior_vr_foraging.data_mappers._session import AindAcquisitionDataMapper
1314

1415
sys.path.append(".")
1516
from aind_behavior_vr_foraging.cli import DataMapperCli
@@ -41,18 +42,18 @@ def setUp(self):
4142
self.repo_path = Path("./")
4243
self.session_end_time = datetime(2023, 1, 1, 12, 0, 0, tzinfo=timezone.utc)
4344

44-
self.session_mapper = AindSessionDataMapper(
45+
self.session_mapper = AindAcquisitionDataMapper(
4546
data_path=self.data_path,
4647
repo_path=self.repo_path,
4748
session_end_time=self.session_end_time,
4849
)
4950

50-
self.rig_mapper = AindRigDataMapper(data_path=self.data_path)
51+
self.rig_mapper = AindInstrumentDataMapper(data_path=self.data_path)
5152

5253
def tearDown(self):
5354
self.temp_dir.cleanup()
5455

55-
@patch("aind_behavior_vr_foraging.data_mappers._session.AindSessionDataMapper._map")
56+
@patch("aind_behavior_vr_foraging.data_mappers._session.AindAcquisitionDataMapper._map")
5657
def test_session_mock_map(self, mock_map):
5758
mock_map.return_value = MagicMock()
5859
result = self.session_mapper.map()
@@ -68,7 +69,7 @@ def test_session_round_trip(self):
6869
assert mapped is not None
6970
acquisition.Acquisition.model_validate_json(mapped.model_dump_json())
7071

71-
@patch("aind_behavior_vr_foraging.data_mappers._rig.AindRigDataMapper._map")
72+
@patch("aind_behavior_vr_foraging.data_mappers._rig.AindInstrumentDataMapper._map")
7273
def test_rig_mock_map(self, mock_map):
7374
mock_map.return_value = MagicMock()
7475
result = self.rig_mapper.map()
@@ -91,16 +92,16 @@ def test_instrument_acquisition_compatibility(self):
9192
assert rig_mapped is not None
9293
compatibility_check.InstrumentAcquisitionCompatibility(
9394
instrument=rig_mapped, acquisition=session_mapped
94-
).run_compatibility_check()
95+
).run_compatibility_check(raise_for_missing_devices=True)
9596

9697
def test_mapper_cli(self):
9798
DataMapperCli(
9899
data_path=self.data_path,
99100
repo_path=self.repo_path,
100101
session_end_time=self.session_end_time,
101102
).cli_cmd()
102-
instrument_path = self.data_path / "instrument.json"
103-
acquisition_path = self.data_path / "acquisition.json"
103+
instrument_path = self.data_path / "instrument_vrforaging.json"
104+
acquisition_path = self.data_path / "acquisition_vrforaging.json"
104105

105106
self.assertTrue(instrument_path.exists())
106107
self.assertTrue(acquisition_path.exists())

0 commit comments

Comments
 (0)