diff --git a/examples/aibs_smartspim_instrument.json b/examples/aibs_smartspim_instrument.json deleted file mode 100644 index a799ec7f6..000000000 --- a/examples/aibs_smartspim_instrument.json +++ /dev/null @@ -1,510 +0,0 @@ -{ - "describedBy": "https://raw.githubusercontent.com/AllenNeuralDynamics/aind-data-schema/main/src/aind_data_schema/core/instrument.py", - "schema_version": "1.0.5", - "instrument_id": "440_SmartSPIM2_20231004", - "modification_date": "2023-10-04", - "instrument_type": "SmartSPIM", - "manufacturer": { - "name": "LifeCanvas", - "abbreviation": null, - "registry": null, - "registry_identifier": null - }, - "temperature_control": false, - "optical_tables": [ - { - "device_type": "Optical table", - "name": "Table", - "serial_number": null, - "manufacturer": { - "name": "Technical Manufacturing Corporation", - "abbreviation": "TMC", - "registry": null, - "registry_identifier": null - }, - "model": "CleanTop", - "path_to_cad": null, - "port_index": null, - "additional_settings": {}, - "notes": null, - "length": "35", - "width": "29", - "table_size_unit": "inch", - "vibration_control": true - } - ], - "enclosure": null, - "objectives": [ - { - "device_type": "Objective", - "name": "TLX Objective", - "serial_number": null, - "manufacturer": { - "name": "Thorlabs", - "abbreviation": null, - "registry": { - "name": "Research Organization Registry", - "abbreviation": "ROR" - }, - "registry_identifier": "04gsnvb07" - }, - "model": "TL4X-SAP", - "path_to_cad": null, - "port_index": null, - "additional_settings": {}, - "notes": "Thorlabs TL4X-SAP with LifeCanvas dipping cap and correction optics.", - "numerical_aperture": "0.2", - "magnification": "3.6", - "immersion": "multi", - "objective_type": null - } - ], - "detectors": [ - { - "device_type": "Detector", - "name": "Camera 1", - "serial_number": "001107", - "manufacturer": { - "name": "Hamamatsu", - "abbreviation": null, - "registry": { - "name": "Research Organization Registry", - "abbreviation": "ROR" - }, - "registry_identifier": "03natb733" - }, - "model": "C14440-20UP", - "path_to_cad": null, - "port_index": null, - "additional_settings": {}, - "notes": null, - "detector_type": "Camera", - "data_interface": "USB", - "cooling": "Air", - "computer_name": null, - "frame_rate": null, - "frame_rate_unit": null, - "immersion": null, - "chroma": null, - "sensor_width": null, - "sensor_height": null, - "size_unit": "pixel", - "sensor_format": null, - "sensor_format_unit": null, - "bit_depth": null, - "bin_mode": "None", - "bin_width": null, - "bin_height": null, - "bin_unit": "pixel", - "gain": null, - "crop_offset_x": null, - "crop_offset_y": null, - "crop_width": null, - "crop_height": null, - "crop_unit": "pixel", - "recording_software": null, - "driver": null, - "driver_version": null - } - ], - "light_sources": [ - { - "device_type": "Laser", - "name": "Ex_488", - "serial_number": "VL01222A11", - "manufacturer": { - "name": "Vortran", - "abbreviation": null, - "registry": null, - "registry_identifier": null - }, - "model": "Stradus", - "path_to_cad": null, - "port_index": null, - "additional_settings": {}, - "notes": "All lasers controlled via Vortran VersaLase System", - "wavelength": 488, - "wavelength_unit": "nanometer", - "maximum_power": "150", - "power_unit": "milliwatt", - "coupling": "Single-mode fiber", - "coupling_efficiency": null, - "coupling_efficiency_unit": "percent", - "item_number": null - }, - { - "device_type": "Laser", - "name": "Ex_561", - "serial_number": "417927", - "manufacturer": { - "name": "Coherent Scientific", - "abbreviation": null, - "registry": { - "name": "Research Organization Registry", - "abbreviation": "ROR" - }, - "registry_identifier": "031tysd23" - }, - "model": "Obis", - "path_to_cad": null, - "port_index": null, - "additional_settings": {}, - "notes": "All lasers controlled via Vortran VersaLase System", - "wavelength": 561, - "wavelength_unit": "nanometer", - "maximum_power": "150", - "power_unit": "milliwatt", - "coupling": "Single-mode fiber", - "coupling_efficiency": null, - "coupling_efficiency_unit": "percent", - "item_number": null - }, - { - "device_type": "Laser", - "name": "Ex_647", - "serial_number": "VL01222A10", - "manufacturer": { - "name": "Vortran", - "abbreviation": null, - "registry": null, - "registry_identifier": null - }, - "model": "Stradus", - "path_to_cad": null, - "port_index": null, - "additional_settings": {}, - "notes": "All lasers controlled via Vortran VersaLase System", - "wavelength": 647, - "wavelength_unit": "nanometer", - "maximum_power": "160", - "power_unit": "milliwatt", - "coupling": "Single-mode fiber", - "coupling_efficiency": null, - "coupling_efficiency_unit": "percent", - "item_number": null - } - ], - "lenses": [], - "fluorescence_filters": [ - { - "device_type": "Filter", - "name": "Em_525", - "serial_number": null, - "manufacturer": { - "name": "Semrock", - "abbreviation": null, - "registry": null, - "registry_identifier": null - }, - "model": "FF03-525/50-25", - "path_to_cad": null, - "port_index": null, - "additional_settings": {}, - "notes": null, - "filter_type": "Band pass", - "diameter": "25", - "width": null, - "height": null, - "size_unit": "millimeter", - "thickness": "2.0", - "thickness_unit": "millimeter", - "filter_wheel_index": 0, - "cut_off_wavelength": null, - "cut_on_wavelength": null, - "center_wavelength": null, - "wavelength_unit": "nanometer", - "description": null - }, - { - "device_type": "Filter", - "name": "Em_600", - "serial_number": null, - "manufacturer": { - "name": "Semrock", - "abbreviation": null, - "registry": null, - "registry_identifier": null - }, - "model": "FF01-600/52-25", - "path_to_cad": null, - "port_index": null, - "additional_settings": {}, - "notes": null, - "filter_type": "Band pass", - "diameter": "25", - "width": null, - "height": null, - "size_unit": "millimeter", - "thickness": "2.0", - "thickness_unit": "millimeter", - "filter_wheel_index": 1, - "cut_off_wavelength": null, - "cut_on_wavelength": null, - "center_wavelength": null, - "wavelength_unit": "nanometer", - "description": null - }, - { - "device_type": "Filter", - "name": "Em_690", - "serial_number": null, - "manufacturer": { - "name": "Chroma", - "abbreviation": null, - "registry": null, - "registry_identifier": null - }, - "model": "ET690/50m", - "path_to_cad": null, - "port_index": null, - "additional_settings": {}, - "notes": null, - "filter_type": "Band pass", - "diameter": "25", - "width": null, - "height": null, - "size_unit": "millimeter", - "thickness": "2.0", - "thickness_unit": "millimeter", - "filter_wheel_index": 2, - "cut_off_wavelength": null, - "cut_on_wavelength": null, - "center_wavelength": null, - "wavelength_unit": "nanometer", - "description": null - } - ], - "motorized_stages": [ - { - "device_type": "Motorized stage", - "name": "Focus stage", - "serial_number": null, - "manufacturer": { - "name": "Applied Scientific Instrumentation", - "abbreviation": "ASI", - "registry": null, - "registry_identifier": null - }, - "model": "LS-100", - "path_to_cad": null, - "port_index": null, - "additional_settings": {}, - "notes": null, - "travel": "100", - "travel_unit": "millimeter", - "firmware": null - }, - { - "device_type": "Motorized stage", - "name": "Cylindrical lens #1", - "serial_number": null, - "manufacturer": { - "name": "IR Robot Co", - "abbreviation": null, - "registry": null, - "registry_identifier": null - }, - "model": "L12-20F-4", - "path_to_cad": null, - "port_index": null, - "additional_settings": {}, - "notes": null, - "travel": "41", - "travel_unit": "millimeter", - "firmware": null - }, - { - "device_type": "Motorized stage", - "name": "Cylindrical lens #2", - "serial_number": null, - "manufacturer": { - "name": "IR Robot Co", - "abbreviation": null, - "registry": null, - "registry_identifier": null - }, - "model": "L12-20F-4", - "path_to_cad": null, - "port_index": null, - "additional_settings": {}, - "notes": null, - "travel": "41", - "travel_unit": "millimeter", - "firmware": null - }, - { - "device_type": "Motorized stage", - "name": "Cylindrical lens #3", - "serial_number": null, - "manufacturer": { - "name": "IR Robot Co", - "abbreviation": null, - "registry": null, - "registry_identifier": null - }, - "model": "L12-20F-4", - "path_to_cad": null, - "port_index": null, - "additional_settings": {}, - "notes": null, - "travel": "41", - "travel_unit": "millimeter", - "firmware": null - }, - { - "device_type": "Motorized stage", - "name": "Cylindrical lens #4", - "serial_number": null, - "manufacturer": { - "name": "IR Robot Co", - "abbreviation": null, - "registry": null, - "registry_identifier": null - }, - "model": "L12-20F-4", - "path_to_cad": null, - "port_index": null, - "additional_settings": {}, - "notes": null, - "travel": "41", - "travel_unit": "millimeter", - "firmware": null - } - ], - "scanning_stages": [ - { - "device_type": "Motorized stage", - "name": "Sample stage Z", - "serial_number": null, - "manufacturer": { - "name": "Applied Scientific Instrumentation", - "abbreviation": "ASI", - "registry": null, - "registry_identifier": null - }, - "model": "LS-50", - "path_to_cad": null, - "port_index": null, - "additional_settings": {}, - "notes": null, - "travel": "50", - "travel_unit": "millimeter", - "firmware": null, - "stage_axis_direction": "Detection axis", - "stage_axis_name": "Z" - }, - { - "device_type": "Motorized stage", - "name": "Sample stage X", - "serial_number": null, - "manufacturer": { - "name": "Applied Scientific Instrumentation", - "abbreviation": "ASI", - "registry": null, - "registry_identifier": null - }, - "model": "LS-50", - "path_to_cad": null, - "port_index": null, - "additional_settings": {}, - "notes": null, - "travel": "50", - "travel_unit": "millimeter", - "firmware": null, - "stage_axis_direction": "Illumination axis", - "stage_axis_name": "X" - }, - { - "device_type": "Motorized stage", - "name": "Sample stage Y", - "serial_number": null, - "manufacturer": { - "name": "Applied Scientific Instrumentation", - "abbreviation": "ASI", - "registry": null, - "registry_identifier": null - }, - "model": "LS-50", - "path_to_cad": null, - "port_index": null, - "additional_settings": {}, - "notes": null, - "travel": "50", - "travel_unit": "millimeter", - "firmware": null, - "stage_axis_direction": "Perpendicular axis", - "stage_axis_name": "Y" - } - ], - "additional_devices": [ - { - "device_type": "Additional imaging device", - "name": "Lens 1", - "serial_number": null, - "manufacturer": { - "name": "Optotune", - "abbreviation": null, - "registry": null, - "registry_identifier": null - }, - "model": "EL-16-40-TC", - "path_to_cad": null, - "port_index": null, - "additional_settings": {}, - "notes": null, - "imaging_device_type": "Tunable lens" - }, - { - "device_type": "Additional imaging device", - "name": "Lens 2", - "serial_number": null, - "manufacturer": { - "name": "Optotune", - "abbreviation": null, - "registry": null, - "registry_identifier": null - }, - "model": "EL-16-40-TC", - "path_to_cad": null, - "port_index": null, - "additional_settings": {}, - "notes": null, - "imaging_device_type": "Tunable lens" - }, - { - "device_type": "Additional imaging device", - "name": "Sample chamber", - "serial_number": null, - "manufacturer": { - "name": "LifeCanvas", - "abbreviation": null, - "registry": null, - "registry_identifier": null - }, - "model": "Large-uncoated-glass", - "path_to_cad": null, - "port_index": null, - "additional_settings": {}, - "notes": null, - "imaging_device_type": "Sample Chamber" - } - ], - "calibration_date": null, - "calibration_data": null, - "com_ports": [ - { - "hardware_name": "Laser Launch", - "com_port": "COM3" - }, - { - "hardware_name": "ASI Tiger", - "com_port": "COM5" - }, - { - "hardware_name": "MightyZap", - "com_port": "COM4" - } - ], - "daqs": [], - "notes": null -} \ No newline at end of file diff --git a/examples/aibs_smartspim_instrument.py b/examples/aibs_smartspim_rig.py similarity index 76% rename from examples/aibs_smartspim_instrument.py rename to examples/aibs_smartspim_rig.py index 8fb42aca9..3e745e59a 100644 --- a/examples/aibs_smartspim_instrument.py +++ b/examples/aibs_smartspim_rig.py @@ -9,32 +9,26 @@ AdditionalImagingDevice, Detector, Filter, + ImagingInstrumentType, Laser, MotorizedStage, Objective, OpticalTable, ScanningStage, ) -from aind_data_schema.core.instrument import Com, Instrument +from aind_data_schema_models.modalities import Modality +from aind_data_schema.core.rig import Com, Rig -inst = Instrument( - instrument_id="440_SmartSPIM2_20231004", - modification_date=datetime.date(2023, 10, 4), - instrument_type="SmartSPIM", - manufacturer=Organization.LIFECANVAS, - objectives=[ - Objective( - name="TLX Objective", - numerical_aperture=0.2, - magnification=3.6, - immersion="multi", - manufacturer=Organization.THORLABS, - model="TL4X-SAP", - notes="Thorlabs TL4X-SAP with LifeCanvas dipping cap and correction optics.", - ), - ], - detectors=[ - Detector( +objective = Objective( + name="TLX Objective", + numerical_aperture=0.2, + magnification=3.6, + immersion="multi", + manufacturer=Organization.THORLABS, + model="TL4X-SAP", + notes="Thorlabs TL4X-SAP with LifeCanvas dipping cap and correction optics.", +) +camera1 = Detector( name="Camera 1", detector_type="Camera", data_interface="USB", @@ -42,10 +36,8 @@ manufacturer=Organization.HAMAMATSU, model="C14440-20UP", serial_number="001107", - ), - ], - light_sources=[ - Laser( + ) +laser1 = Laser( name="Ex_488", device_type="Laser", coupling="Single-mode fiber", @@ -55,8 +47,8 @@ manufacturer=Organization.VORTRAN, model="Stradus", notes="All lasers controlled via Vortran VersaLase System", - ), - Laser( + ) +laser2 = Laser( name="Ex_561", device_type="Laser", coupling="Single-mode fiber", @@ -66,8 +58,8 @@ manufacturer=Organization.COHERENT_SCIENTIFIC, model="Obis", notes="All lasers controlled via Vortran VersaLase System", - ), - Laser( + ) +laser3 = Laser( name="Ex_647", device_type="Laser", coupling="Single-mode fiber", @@ -77,68 +69,62 @@ manufacturer=Organization.VORTRAN, model="Stradus", notes="All lasers controlled via Vortran VersaLase System", - ), - ], - motorized_stages=[ - MotorizedStage( + ) +stage0 = MotorizedStage( name="Focus stage", model="LS-100", manufacturer=Organization.ASI, travel=100, - ), - MotorizedStage( + ) +stage1 = MotorizedStage( name="Cylindrical lens #1", model="L12-20F-4", manufacturer=Organization.IR_ROBOT_CO, travel=41, - ), - MotorizedStage( + ) +stage2 = MotorizedStage( name="Cylindrical lens #2", model="L12-20F-4", manufacturer=Organization.IR_ROBOT_CO, travel=41, - ), - MotorizedStage( + ) +stage3 = MotorizedStage( name="Cylindrical lens #3", model="L12-20F-4", manufacturer=Organization.IR_ROBOT_CO, travel=41, - ), - MotorizedStage( + ) +stage4 = MotorizedStage( name="Cylindrical lens #4", model="L12-20F-4", manufacturer=Organization.IR_ROBOT_CO, travel=41, - ), - ], - scanning_stages=[ - ScanningStage( + ) +scan_stage1 = ScanningStage( name="Sample stage Z", model="LS-50", manufacturer=Organization.ASI, stage_axis_direction="Detection axis", stage_axis_name="Z", travel=50, - ), - ScanningStage( + ) +scan_stage2 = ScanningStage( name="Sample stage X", model="LS-50", manufacturer=Organization.ASI, stage_axis_direction="Illumination axis", stage_axis_name="X", travel=50, - ), - ScanningStage( + ) +scan_stage3 = ScanningStage( name="Sample stage Y", model="LS-50", manufacturer=Organization.ASI, stage_axis_direction="Perpendicular axis", stage_axis_name="Y", travel=50, - ), - ], - optical_tables=[ - OpticalTable( + ) +table = OpticalTable( name="Table", model="CleanTop", # model="VIS2424-IG2-125A", # ~3 months length=35, # length=24, @@ -146,21 +132,7 @@ vibration_control=True, manufacturer=Organization.TMC, ) - ], - temperature_control=False, - com_ports=[ - Com( - hardware_name="Laser Launch", - com_port="COM3", - ), - Com( - hardware_name="ASI Tiger", - com_port="COM5", - ), - Com(hardware_name="MightyZap", com_port="COM4"), - ], - fluorescence_filters=[ - Filter( +filter0 = Filter( name="Em_525", filter_type="Band pass", manufacturer=Organization.SEMROCK, @@ -169,8 +141,8 @@ thickness_unit=SizeUnit.MM, model="FF03-525/50-25", filter_wheel_index=0, - ), - Filter( + ) +filter1 = Filter( name="Em_600", filter_type="Band pass", manufacturer=Organization.SEMROCK, @@ -179,8 +151,8 @@ thickness_unit=SizeUnit.MM, model="FF01-600/52-25", filter_wheel_index=1, - ), - Filter( + ) +filter2 = Filter( name="Em_690", filter_type="Band pass", manufacturer=Organization.CHROMA, @@ -189,29 +161,67 @@ thickness_unit=SizeUnit.MM, model="ET690/50m", filter_wheel_index=2, - ), - ], - additional_devices=[ - AdditionalImagingDevice( + ) +lens1 = AdditionalImagingDevice( name="Lens 1", imaging_device_type="Tunable lens", manufacturer=Organization.OPTOTUNE, model="EL-16-40-TC", - ), - AdditionalImagingDevice( + ) +lens2 = AdditionalImagingDevice( name="Lens 2", imaging_device_type="Tunable lens", manufacturer=Organization.OPTOTUNE, model="EL-16-40-TC", - ), - AdditionalImagingDevice( + ) +lens3 = AdditionalImagingDevice( name="Sample chamber", imaging_device_type="Sample Chamber", manufacturer=Organization.LIFECANVAS, model="Large-uncoated-glass", + ) + +inst = Rig( + rig_id="440_SmartSPIM2_20231004", + modification_date=datetime.date(2023, 10, 4), + instrument_type=ImagingInstrumentType.SMARTSPIM, + modalities=[Modality.SPIM], + manufacturer=Organization.LIFECANVAS, + temperature_control=False, + components=[ + objective, + camera1, + laser1, + laser2, + laser3, + stage0, + stage1, + stage2, + stage3, + stage4, + scan_stage1, + scan_stage2, + scan_stage3, + table, + filter0, + filter1, + filter2, + lens1, + lens2, + lens3, + ], + com_ports=[ + Com( + hardware_name="Laser Launch", + com_port="COM3", ), + Com( + hardware_name="ASI Tiger", + com_port="COM5", + ), + Com(hardware_name="MightyZap", com_port="COM4"), ], ) serialized = inst.model_dump_json() -deserialized = Instrument.model_validate_json(serialized) +deserialized = Rig.model_validate_json(serialized) deserialized.write_standard_file(prefix="aibs_smartspim") diff --git a/examples/test.py b/examples/test.py new file mode 100644 index 000000000..ea2c88a9d --- /dev/null +++ b/examples/test.py @@ -0,0 +1,30 @@ +from typing import Annotated, List, Union +from pydantic import BaseModel, Field + + +class TypeA(BaseModel): + item_type: str = "TypeA" + value: int + + +class TypeB(BaseModel): + item_type: str = "TypeB" + value: str + + +class TypeC(BaseModel): + item_type: str = "TypeC" + value: float + + +class MyModel(BaseModel): + items: List[Annotated[Union[TypeA, TypeB, TypeC], Field(discriminator="item_type")]] + + +# Example usage: +data = MyModel(items=[TypeA(value=42), TypeB(value="string"), TypeC(value=3.14)]) +print(data) + +# Example: Providing only a subset +data_subset = MyModel(items=[TypeA(value=42), TypeC(value=3.14)]) +print(data_subset) diff --git a/src/aind_data_schema/base.py b/src/aind_data_schema/base.py index cffbfb7a2..fe669da91 100644 --- a/src/aind_data_schema/base.py +++ b/src/aind_data_schema/base.py @@ -154,9 +154,7 @@ def default_filename(cls): """ Returns standard filename in snakecase """ - parent_classes = [ - base_class for base_class in cls.__bases__ if base_class.__name__ != DataCoreModel.__name__ - ] + parent_classes = [base_class for base_class in cls.__bases__ if base_class.__name__ != DataCoreModel.__name__] name = cls.__name__ diff --git a/src/aind_data_schema/components/devices.py b/src/aind_data_schema/components/devices.py index af739013d..cf30b9202 100644 --- a/src/aind_data_schema/components/devices.py +++ b/src/aind_data_schema/components/devices.py @@ -273,7 +273,7 @@ class Device(DataModel): default=None, title="Path to CAD diagram", description="For CUSTOM manufactured devices" ) port_index: Optional[str] = Field(default=None, title="Port index") - additional_settings: GenericModelType = Field(GenericModel(), title="Additional parameters") + additional_settings: Optional[GenericModelType] = Field(default=None, title="Additional parameters") notes: Optional[str] = Field(default=None, title="Notes") @@ -933,6 +933,3 @@ class MyomatrixArray(Device): device_type: Literal["Myomatrix Array"] = "Myomatrix Array" array_type: MyomatrixArrayType = Field(..., title="Array type") - - -LIGHT_SOURCES = Annotated[Union[Laser, LightEmittingDiode, Lamp], Field(discriminator="device_type")] diff --git a/src/aind_data_schema/core/instrument.py b/src/aind_data_schema/core/instrument.py deleted file mode 100644 index c62667805..000000000 --- a/src/aind_data_schema/core/instrument.py +++ /dev/null @@ -1,110 +0,0 @@ -""" schema describing imaging instrument """ - -from datetime import date -from typing import List, Literal, Optional - -from aind_data_schema_models.organizations import Organization -from pydantic import Field, SkipValidation, ValidationInfo, field_validator - -from aind_data_schema.base import DataCoreModel, DataModel -from aind_data_schema.components.devices import ( - LIGHT_SOURCES, - AdditionalImagingDevice, - DAQDevice, - Detector, - Enclosure, - Filter, - ImagingInstrumentType, - Lens, - MotorizedStage, - Objective, - OpticalTable, - ScanningStage, -) - - -class Com(DataModel): - """Description of a communication system""" - - hardware_name: str = Field(..., title="Controlled hardware device") - com_port: str = Field(..., title="COM port") - - -class Instrument(DataCoreModel): - """Description of an instrument, which is a collection of devices""" - - _DESCRIBED_BY_URL = DataCoreModel._DESCRIBED_BY_BASE_URL.default + "aind_data_schema/core/instrument.py" - describedBy: str = Field(default=_DESCRIBED_BY_URL, json_schema_extra={"const": _DESCRIBED_BY_URL}) - schema_version: SkipValidation[Literal["1.0.5"]] = Field(default="1.0.5") - - instrument_id: Optional[str] = Field( - default=None, - description="Unique instrument identifier, name convention: --", - title="Instrument ID", - ) - modification_date: date = Field(..., title="Date of modification") - instrument_type: ImagingInstrumentType = Field(..., title="Instrument type") - manufacturer: Organization.ONE_OF = Field(..., title="Instrument manufacturer") - temperature_control: Optional[bool] = Field(default=None, title="Temperature control") - optical_tables: List[OpticalTable] = Field(default=[], title="Optical table") - enclosure: Optional[Enclosure] = Field(default=None, title="Enclosure") - objectives: List[Objective] = Field(..., title="Objectives") - detectors: List[Detector] = Field(default=[], title="Detectors") - light_sources: List[LIGHT_SOURCES] = Field(default=[], title="Light sources") - lenses: List[Lens] = Field(default=[], title="Lenses") - fluorescence_filters: List[Filter] = Field(default=[], title="Fluorescence filters") - motorized_stages: List[MotorizedStage] = Field(default=[], title="Motorized stages") - scanning_stages: List[ScanningStage] = Field(default=[], title="Scanning motorized stages") - additional_devices: List[AdditionalImagingDevice] = Field(default=[], title="Additional devices") - calibration_date: Optional[date] = Field( - default=None, - description="Date of most recent calibration", - title="Calibration date", - ) - calibration_data: Optional[str] = Field( - default=None, - description="Path to calibration data from most recent calibration", - title="Calibration data", - ) - com_ports: List[Com] = Field(default=[], title="COM ports") - daqs: List[DAQDevice] = Field(default=[], title="DAQ") - notes: Optional[str] = Field(default=None, validate_default=True) - - @field_validator("daqs", mode="after") - def validate_device_names(cls, value: List[DAQDevice], info: ValidationInfo) -> List[DAQDevice]: - """validate that all DAQ channels are connected to devices that - actually exist - """ - daqs = value - all_devices = ( - info.data["motorized_stages"] - + info.data["scanning_stages"] - + info.data["light_sources"] - + info.data["detectors"] - + info.data["additional_devices"] - + daqs - ) - all_device_names = [device.name for device in all_devices] - for daq in daqs: - for channel in daq.channels: - if channel.device_name not in all_device_names: - raise ValueError( - f"Device name validation error: '{channel.device_name}' " - + f"is connected to '{channel.channel_name}' on '{daq.name}', but " - + "this device is not part of the rig." - ) - return daqs - - @field_validator("notes", mode="after") - def validate_other(cls, value: Optional[str], info: ValidationInfo) -> Optional[str]: - """Validator for other/notes""" - - if info.data.get("instrument_type") == ImagingInstrumentType.OTHER and not value: - raise ValueError( - "Notes cannot be empty if instrument_type is Other. Describe the instrument_type in the notes field." - ) - if info.data.get("manufacturer") == Organization.OTHER and not value: - raise ValueError( - "Notes cannot be empty if manufacturer is Other. Describe the manufacturer in the notes field." - ) - return value diff --git a/src/aind_data_schema/core/rig.py b/src/aind_data_schema/core/rig.py index 1e594aa6a..58d1485b1 100644 --- a/src/aind_data_schema/core/rig.py +++ b/src/aind_data_schema/core/rig.py @@ -7,10 +7,11 @@ from pydantic import Field, SkipValidation, ValidationInfo, field_serializer, field_validator, model_validator from typing_extensions import Annotated -from aind_data_schema.base import DataCoreModel +from aind_data_schema_models.organizations import Organization +from aind_data_schema.base import DataCoreModel, DataModel from aind_data_schema.components.coordinates import Axis, Origin from aind_data_schema.components.devices import ( - LIGHT_SOURCES, + AdditionalImagingDevice, Calibration, CameraAssembly, CameraTarget, @@ -23,62 +24,66 @@ FiberAssembly, Filter, HarpDevice, + ImagingInstrumentType, + Lamp, + Laser, LaserAssembly, Lens, + LightEmittingDiode, Monitor, + MotorizedStage, MousePlatform, NeuropixelsBasestation, Objective, Olfactometer, OpenEphysAcquisitionBoard, + OpticalTable, Patch, PockelsCell, PolygonalScanner, RewardDelivery, + ScanningStage, Speaker, ) MOUSE_PLATFORMS = Annotated[Union[tuple(MousePlatform.__subclasses__())], Field(discriminator="device_type")] -STIMULUS_DEVICES = Annotated[Union[Monitor, Olfactometer, RewardDelivery, Speaker], Field(discriminator="device_type")] -RIG_DAQ_DEVICES = Annotated[ - Union[HarpDevice, NeuropixelsBasestation, OpenEphysAcquisitionBoard, DAQDevice], Field(discriminator="device_type") -] RIG_ID_PATTERN = r"^[a-zA-Z0-9]+_[a-zA-Z0-9-]+_\d{8}$" +class Com(DataModel): + """Description of a communication system""" + + hardware_name: str = Field(..., title="Controlled hardware device") + com_port: str = Field(..., title="COM port") + + +class Connection(DataModel): + """Connection between two devices""" + + device_names: List[str] = Field(..., title="Names of connected devices") + inputs: Optional[List[bool]] = Field(default=None, title="Input status") + outputs: Optional[List[bool]] = Field(default=None, title="Output status") + channels: Optional[List[int]] = Field(default=None, title="Connection channels") + + class Rig(DataCoreModel): """Description of a rig""" + # metametadata _DESCRIBED_BY_URL = DataCoreModel._DESCRIBED_BY_BASE_URL.default + "aind_data_schema/core/rig.py" describedBy: str = Field(default=_DESCRIBED_BY_URL, json_schema_extra={"const": _DESCRIBED_BY_URL}) schema_version: SkipValidation[Literal["1.0.5"]] = Field(default="1.0.5") + + # rig definition rig_id: str = Field( ..., description="Unique rig identifier, name convention: --", title="Rig ID", pattern=RIG_ID_PATTERN, ) + mouse_platform: Optional[MOUSE_PLATFORMS] = Field(default=None, title="Mouse platform") modification_date: date = Field(..., title="Date of modification") - mouse_platform: MOUSE_PLATFORMS - stimulus_devices: List[STIMULUS_DEVICES] = Field(default=[], title="Stimulus devices") - cameras: List[CameraAssembly] = Field(default=[], title="Camera assemblies") - enclosure: Optional[Enclosure] = Field(default=None, title="Enclosure") - ephys_assemblies: List[EphysAssembly] = Field(default=[], title="Ephys probes") - fiber_assemblies: List[FiberAssembly] = Field(default=[], title="Inserted fiber optics") - stick_microscopes: List[CameraAssembly] = Field(default=[], title="Stick microscopes") - laser_assemblies: List[LaserAssembly] = Field(default=[], title="Laser modules") - patch_cords: List[Patch] = Field(default=[], title="Patch cords") - light_sources: List[LIGHT_SOURCES] = Field(default=[], title="Light sources") - detectors: List[Detector] = Field(default=[], title="Detectors") - objectives: List[Objective] = Field(default=[], title="Objectives") - filters: List[Filter] = Field(default=[], title="Filters") - lenses: List[Lens] = Field(default=[], title="Lenses") - digital_micromirror_devices: List[DigitalMicromirrorDevice] = Field(default=[], title="DMDs") - polygonal_scanners: List[PolygonalScanner] = Field(default=[], title="Polygonal scanners") - pockels_cells: List[PockelsCell] = Field(default=[], title="Pockels cells") - additional_devices: List[Device] = Field(default=[], title="Additional devices") - daqs: List[RIG_DAQ_DEVICES] = Field(default=[], title="Data acquisition devices") - calibrations: List[Calibration] = Field(..., title="Full calibration of devices") + calibrations: Optional[List[Calibration]] = Field(default=None, title="Full calibration of devices") ccf_coordinate_transform: Optional[str] = Field( default=None, title="CCF coordinate transform", @@ -87,8 +92,59 @@ class Rig(DataCoreModel): origin: Optional[Origin] = Field(default=None, title="Origin point for rig position transforms") rig_axes: Optional[List[Axis]] = Field(default=None, title="Rig axes", min_length=3, max_length=3) modalities: Set[Modality.ONE_OF] = Field(..., title="Modalities") + com_ports: List[Com] = Field(default=[], title="COM ports") + instrument_type: Optional[ImagingInstrumentType] = Field(default=None, title="Instrument type") + manufacturer: Optional[Organization.ONE_OF] = Field(default=None, title="Instrument manufacturer") + temperature_control: Optional[bool] = Field(default=None, title="Temperature control") notes: Optional[str] = Field(default=None, title="Notes") + connections: List[Connection] = Field( + default=[], + title="Connections", + description="List of all connections between devices in the rig", + ) + + components: List[ + Annotated[ + Union[ + Monitor, + Olfactometer, + RewardDelivery, + Speaker, + CameraAssembly, + Enclosure, + EphysAssembly, + FiberAssembly, + LaserAssembly, + Patch, + Laser, + LightEmittingDiode, + Lamp, + Detector, + Objective, + Filter, + Lens, + DigitalMicromirrorDevice, + PolygonalScanner, + PockelsCell, + HarpDevice, + NeuropixelsBasestation, + OpenEphysAcquisitionBoard, + OpticalTable, + MotorizedStage, + ScanningStage, + AdditionalImagingDevice, + DAQDevice, + Device, # note that order matters in the Union, DAQDevice and Device should go last + ], + Field(discriminator="device_type"), + ] + ] = Field( + default=[], + title="Components", + description="List of all devices in the rig", + ) + @field_serializer("modalities", when_used="json") def serialize_modalities(self, modalities: Set[Modality.ONE_OF]): """Dynamically serialize modalities based on their type.""" @@ -99,154 +155,79 @@ def validate_cameras_other(self): """check if any cameras contain an 'other' field""" if self.notes is None: - for camera_assembly in self.cameras + self.stick_microscopes: - if camera_assembly.camera_target == CameraTarget.OTHER: + for component in self.components: + if isinstance(component, CameraAssembly) and component.camera_target == CameraTarget.OTHER: raise ValueError( f"Notes cannot be empty if a camera target contains an 'Other' field. " - f"Describe the camera target from ({camera_assembly.name}) in the notes field" + f"Describe the camera target from ({component.name}) in the notes field" ) return self - @field_validator("daqs", mode="after") - def validate_device_names(cls, value: List[DAQDevice], info: ValidationInfo) -> List[DAQDevice]: - """validate that all DAQ channels are connected to devices that - actually exist + @field_validator("connections", mode="after") + def validate_device_names(cls, value: List[Connection], info: ValidationInfo) -> List[Connection]: + """validate that all connections map between devices that actually exist """ - daqs = value - non_reward_delivery_stimulus_devices = [ - d for d in info.data.get("stimulus_devices", []) if not isinstance(d, RewardDelivery) - ] - standard_devices = ( - daqs - + info.data.get("light_sources", []) - + info.data.get("patch_cords", []) - + info.data.get("detectors", []) - + info.data.get("digital_micromirror_devices", []) - + info.data.get("polygonal_scanners", []) - + info.data.get("pockels_cells", []) - + info.data.get("additional_devices", []) - + non_reward_delivery_stimulus_devices - ) - camera_devices = info.data.get("cameras", []) + info.data.get("stick_microscopes", []) - standard_device_names = [device.name for device in standard_devices] - camera_names = [camera.camera.name for camera in camera_devices] - ephys_assembly_names = [ - probe.name for ephys_assembly in info.data.get("ephys_assemblies", []) for probe in ephys_assembly.probes - ] - laser_assembly_names = [ - laser.name for laser_assembly in info.data.get("laser_assemblies", []) for laser in laser_assembly.lasers - ] - mouse_platform_names = [] if info.data.get("mouse_platform") is None else [info.data["mouse_platform"].name] - reward_deliveries = [d for d in info.data.get("stimulus_devices", []) if isinstance(d, RewardDelivery)] - reward_delivery_device_names = [] - for rd in reward_deliveries: - for rs in rd.reward_spouts: - reward_delivery_device_names += [rs.name, rs.solenoid_valve.name, rs.lick_sensor.name] - - all_device_names = ( - standard_device_names - + camera_names - + ephys_assembly_names - + laser_assembly_names - + mouse_platform_names - + reward_delivery_device_names - ) - - for daq in daqs: - for channel in daq.channels: - if channel.device_name not in all_device_names: + device_names = [device.name for device in info.data.get("components", [])] + + for connection in value: + for device_name in connection.device_names: + if device_name not in device_names: raise ValueError( - f"Device name validation error: '{channel.device_name}' " - + f"is connected to '{channel.channel_name}' on '{daq.name}', but " - + "this device is not part of the rig." + f"Device name validation error: '{device_name}' is not part of the rig." ) - return daqs - @staticmethod - def _validate_ephys_modality(value: Set[Modality.ONE_OF], info: ValidationInfo) -> List[str]: - """Validate ecephys modality has ephys_assemblies and stick_microscopes""" - errors = [] - if Modality.ECEPHYS in value: - for k, v in { - "ephys_assemblies": len(info.data.get("ephys_assemblies", [])) > 0, - }.items(): - if v is False: - errors.append(f"{k} field must be utilized for Ecephys modality") - return errors - - @staticmethod - def _validate_fib_modality(value: Set[Modality.ONE_OF], info: ValidationInfo) -> List[str]: - """Validate FIB modality has light_sources, detectors, and patch_cords""" - errors = [] - if Modality.FIB in value: - for k, v in { - "light_sources": len(info.data.get("light_sources", [])) > 0, - "detectors": len(info.data.get("detectors", [])) > 0, - "patch_cords": len(info.data.get("patch_cords", [])) > 0, - }.items(): - if v is False: - errors.append(f"{k} field must be utilized for FIB modality") - return errors - - @staticmethod - def _validate_pophys_modality(value: Set[Modality.ONE_OF], info: ValidationInfo) -> List[str]: - """Validate POPHYS modality has light_sources, detectors, and objectives""" - errors = [] - if Modality.POPHYS in value: - for k, v in { - "light_sources": len(info.data.get("light_sources", [])) > 0, - "detectors": len(info.data.get("detectors", [])) > 0, - "objectives": len(info.data.get("objectives", [])) > 0, - }.items(): - if v is False: - errors.append(f"{k} field must be utilized for POPHYS modality") - return errors - - @staticmethod - def _validate_slap_modality(value: Set[Modality.ONE_OF], info: ValidationInfo) -> List[str]: - """Validate SLAP modality has light_sources, detectors, and objectives""" - errors = [] - if Modality.SLAP in value: - for k, v in { - "light_sources": len(info.data.get("light_sources", [])) > 0, - "detectors": len(info.data.get("detectors", [])) > 0, - "objectives": len(info.data.get("objectives", [])) > 0, - }.items(): - if v is False: - errors.append(f"{k} field must be utilized for SLAP modality") - return errors - - @staticmethod - def _validate_behavior_videos_modality(value: Set[Modality.ONE_OF], info: ValidationInfo) -> List[str]: - """Validate BEHAVIOR_VIDEOS modality has cameras""" - errors = [] - if Modality.BEHAVIOR_VIDEOS in value: - if len(info.data.get("cameras", [])) == 0: - errors.append("cameras field must be utilized for Behavior Videos modality") - return errors - - @staticmethod - def _validate_behavior_modality(value: Set[Modality.ONE_OF], info: ValidationInfo) -> List[str]: - """Validate that BEHAVIOR modality has stimulus_devices""" - errors = [] - if Modality.BEHAVIOR in value: - if len(info.data.get("stimulus_devices", [])) == 0: - errors.append("stimulus_devices field must be utilized for Behavior modality") + return value - return errors + @field_validator("notes", mode="after") + def validate_other(cls, value: Optional[str], info: ValidationInfo) -> Optional[str]: + """Validator for other/notes""" + + if info.data.get("instrument_type") == ImagingInstrumentType.OTHER and not value: + raise ValueError( + "Notes cannot be empty if instrument_type is Other. Describe the instrument_type in the notes field." + ) + if info.data.get("manufacturer") == Organization.OTHER and not value: + raise ValueError( + "Notes cannot be empty if manufacturer is Other. Describe the manufacturer in the notes field." + ) + return value @field_validator("modalities", mode="after") def validate_modalities(cls, value: Set[Modality.ONE_OF], info: ValidationInfo) -> Set[Modality.ONE_OF]: - """Validate each modality in modalities field has associated data""" - ephys_errors = cls._validate_ephys_modality(value, info) - fib_errors = cls._validate_fib_modality(value, info) - pophys_errors = cls._validate_pophys_modality(value, info) - slap_errors = cls._validate_slap_modality(value, info) - behavior_vids_errors = cls._validate_behavior_videos_modality(value, info) - behavior_errors = cls._validate_behavior_modality(value, info) - - errors = ephys_errors + fib_errors + pophys_errors + slap_errors + behavior_vids_errors + behavior_errors + """Validate that devices exist for the modalities specified""" + + type_mapping = { + Modality.ECEPHYS.abbreviation: [EphysAssembly], + Modality.FIB.abbreviation: [ + [Laser, LightEmittingDiode, Lamp], + [Detector], + [Patch] + ], + Modality.POPHYS.abbreviation: [ + [Laser, LightEmittingDiode, Lamp], + [Detector], + [Objective] + ], + Modality.SLAP.abbreviation: [ + [Laser, LightEmittingDiode, Lamp], + [Detector], + [Objective] + ], + Modality.BEHAVIOR_VIDEOS.abbreviation: [CameraAssembly], + Modality.BEHAVIOR.abbreviation: [Olfactometer, RewardDelivery, Speaker, Monitor], + } + + errors = [] + + for modality in value: + if modality.abbreviation in type_mapping: + for device_type in type_mapping[modality.abbreviation]: + if not any(isinstance(component, device) for device in device_type for component in info.data.get("components", [])): + errors.append( + f"Device type validation error: No device of type {device_type} is part of the rig." + ) + if len(errors) > 0: message = "\n ".join(errors) raise ValueError(message) diff --git a/tests/test_rig.py b/tests/test_rig.py index faa5cccb0..568b988ba 100644 --- a/tests/test_rig.py +++ b/tests/test_rig.py @@ -802,15 +802,7 @@ def test_rig_id_validator(self): rig_id="123", modification_date=date(2020, 10, 10), modalities=[Modality.ECEPHYS, Modality.FIB], - daqs=daqs, - cameras=[camera], - stick_microscopes=[stick_microscope], - light_sources=[light_source], - laser_assemblies=lms, - ephys_assemblies=ems, - detectors=[detector], - patch_cords=[patch_cord], - stimulus_devices=[stimulus_device], + components=[*daqs, camera, stick_microscope, light_source, *lms, *ems, detector, patch_cord, stimulus_device], mouse_platform=Disc(name="Disc A", radius=1), calibrations=[calibration], ) @@ -819,15 +811,7 @@ def test_rig_id_validator(self): rig_id="123_EPHYS-OPTO_2020-01-01", modification_date=date(2020, 10, 10), modalities=[Modality.ECEPHYS, Modality.FIB], - daqs=daqs, - cameras=[camera], - stick_microscopes=[stick_microscope], - light_sources=[light_source], - laser_assemblies=lms, - ephys_assemblies=ems, - detectors=[detector], - patch_cords=[patch_cord], - stimulus_devices=[stimulus_device], + components=[*daqs, camera, stick_microscope, light_source, *lms, *ems, detector, patch_cord, stimulus_device], mouse_platform=Disc(name="Disc A", radius=1), calibrations=[calibration], )