Source code for nwb2bids.bids_models._general_metadata

import json
import pathlib
import typing

import pydantic
import pynwb
import typing_extensions


[docs] class GeneralMetadata(pydantic.BaseModel): """ General device and session metadata extracted from NWB files. While NWB treats this information as high-level session-specific metadata, BIDS treats these fields as modality specific and as pertaining to 'parameters' that can vary. This should typically be written to two files, depending on the modality: - `_ecephys.json` - `_icephys.json` """ InstitutionName: str | None = pydantic.Field( description="The name of the institution in charge of the equipment that produced the measurements.", default=None, ) InstitutionAddress: str | None = pydantic.Field( description="The address of the institution in charge of the equipment that produced the measurements.", default=None, ) InstitutionalDepartmentName: str | None = pydantic.Field( description="The department in the institution in charge of the equipment that produced the measurements.", default=None, ) PowerLineFrequency: float | typing.Literal["n/a"] = pydantic.Field( description=( "Frequency (in Hz) of the power grid at the geographical location of the " "instrument (for example, 50 or 60)." ), default="n/a", ) Manufacturer: str | None = pydantic.Field( description='Manufacturer of the equipment that produced the measurements. For example, "TDT", "Blackrock".', default=None, ) ManufacturersModelName: str | None = pydantic.Field( description="Manufacturer's model name of the equipment that produced the measurements.", default=None, ) ManufacturersModelVersion: str | None = pydantic.Field( description="Manufacturer's model version of the equipment that produced the measurements.", default=None, ) RecordingSetupName: str | None = pydantic.Field( description="Custom name of the recording setup.", default=None, ) SamplingFrequency: float = pydantic.Field( description=( "Sampling frequency (in Hz) of all the data in the recording, regardless of their type " "(for example, 2400). Internal (maximum) sampling frequency (in Hz) of the recording " '(for example, "24000").' ), default=-1.0, ) DeviceSerialNumber: str | None = pydantic.Field( description=( "The serial number of the equipment that produced the measurements. A pseudonym can also be used to " "prevent the equipment from being identifiable, so long as each pseudonym is unique within the dataset. " "The serial number of the components of the setup, RECOMMENDED to add serial numbers and versions of ALL " "components constituting the setup." ), default=None, ) SoftwareName: str | None = pydantic.Field( description=( "Name of the software that was used to present the stimuli. " "The name of the software suite used to record the data." ), default=None, ) SoftwareVersions: str | None = pydantic.Field( description="Manufacturer's designation of software version of the equipment that produced the measurements.", default=None, ) RecordingDuration: float | None = pydantic.Field( description="Length of the recording in seconds (for example, 3600).", default=None, ) RecordingType: str | None = pydantic.Field( description=( 'Defines whether the recording is "continuous", "discontinuous", or "epoched", where "epoched" is limited ' "to time windows about events of interest (for example, stimulus presentations or subject responses). " 'Must be one of: "continuous", "epoched", "discontinuous".' ), default=None, ) EpochLength: float | None = pydantic.Field( description=( "Duration of individual epochs in seconds (for example, 1) in case of epoched data. If recording was " "continuous or discontinuous, leave out the field. Must be a number greater than or equal to 0." ), default=None, ) SoftwareFilters: dict[str, dict[str, typing.Any]] | typing.Literal["n/a"] = pydantic.Field( description=( 'Object of temporal software filters applied, or "n/a" if the data is not available. Each key-value pair ' "in the JSON object is a name of the filter and an object in which its parameters are defined as " "key-value pairs ( for example, " '{"Anti-aliasing filter": {"half-amplitude cutoff (Hz)": 500, "Roll-off": "6dB/Octave"}}).' ), default="n/a", ) HardwareFilters: dict[str, dict[str, typing.Any]] | typing.Literal["n/a"] = pydantic.Field( description=( 'Object of temporal hardware filters applied, or "n/a" if the data is not available. Each key-value pair ' "in the JSON object is a name of the filter and an object in which its parameters are defined as " "key-value pairs. For example, " '{"Highpass RC filter": {"Half amplitude cutoff (Hz)": 0.0159, "Roll-off": "6dB/Octave"}}.' ), default="n/a", ) PharmaceuticalName: str | None = pydantic.Field( description="Name of pharmaceutical.", default=None, ) PharmaceuticalDoseAmount: float | list[float] | None = pydantic.Field( description="Dose amount of administered pharmaceutical.", default=None, ) PharmaceuticalDoseUnits: str | None = pydantic.Field( description='Unit format relating to pharmaceutical dose (for example, "mg" or "mg/kg").', default=None, ) PharmaceuticalDoseRegimen: str | None = pydantic.Field( description=( "Details of the pharmaceutical dose regimen. Either adequate description or short-code relating to " 'regimen documented elsewhere (for example, "single oral bolus").' ), default=None, ) PharmaceuticalDoseTime: float | list[float] | None = pydantic.Field( description=( "Time of administration of pharmaceutical dose, relative to time zero. For an infusion, this should be a " "vector with two elements specifying the start and end of the infusion period. For more complex dose " "regimens, the regimen description should be complete enough to enable unambiguous interpretation of " '"PharmaceuticalDoseTime". Unit format of the specified pharmaceutical dose time MUST be seconds.' ), default=None, ) BodyPart: str | None = pydantic.Field( description="Body part of the organ / body region scanned.", default=None, ) BodyPartDetails: str | None = pydantic.Field( description='Additional details about body part or location (for example: "corpus callosum").', default=None, ) BodyPartDetailsOntology: str | None = pydantic.Field( description=( 'URI of ontology used for BodyPartDetails (for example: "https://www.ebi.ac.uk/ols/ontologies/uberon").' ), default=None, ) SampleEnvironment: str | None = pydantic.Field( description=( 'Environment in which the sample was imaged. MUST be one of: "in vivo", "ex vivo" or "in vitro". ' 'Must be one of: "in vivo", "ex vivo", "in vitro".' ), default=None, ) SampleEmbedding: str | None = pydantic.Field( description='Description of the tissue sample embedding (for example: "Epoxy resin").', default=None, ) SliceThickness: int | float | None = pydantic.Field( description=( 'Slice thickness of the tissue sample in the unit micrometers ("um") (for example: 5). ' "Must be a number greater than 0." ), default=None, ) SampleExtractionProtocol: str | None = pydantic.Field( description="Description of the sample extraction protocol or URI (for example from protocols.io).", default=None, ) SupplementarySignals: str | None = pydantic.Field( description=( "Description of the supplementary signal (additional modalities) recorded in parallel and are " "also stored in the data file." ), default=None, ) TaskName: str | None = pydantic.Field( description=( "Name of the task. No two tasks should have the same name. The task label included in the filename " 'MAY be derived from this "TaskName" field by removing all non-alphanumeric or + characters (that is, ' "all except those matching [0-9a-zA-Z+]), and potentially replacing spaces with + to ease readability. " 'For example "TaskName" "faces n-back" or "head nodding" could correspond to task labels faces+n+back ' "or facesnback and head+nodding or headnodding, respectively. A RECOMMENDED convention is to name " "resting state task using labels beginning with rest." ), default=None, ) TaskDescription: str | None = pydantic.Field( description="Longer description of the task.", default=None, ) Instructions: str | None = pydantic.Field( description=( "Text of the instructions given to participants before the recording. This is especially important in " "context of resting state recordings and distinguishing between eyes open and eyes closed paradigms." ), default=None, ) CogAtlasID: str | None = pydantic.Field( description="URI of the corresponding Cognitive Atlas Task term.", default=None, ) CogPOID: str | None = pydantic.Field( description="URI of the corresponding CogPO term.", default=None, ) model_config = pydantic.ConfigDict( validate_assignment=True, # Re-validate model on mutation extra="allow", # Allow additional custom fields )
[docs] @classmethod @pydantic.validate_call def from_nwbfiles(cls, nwbfiles: list[pydantic.InstanceOf[pynwb.NWBFile]]) -> typing_extensions.Self: """ Extracts all unique general metadata from the in-memory NWBFile objects. """ if len(nwbfiles) > 1: message = "Conversion of multiple NWB files per session is not yet supported." raise NotImplementedError(message) nwbfile = nwbfiles[0] dictionary: dict[str, str | int | float | None] = { "PowerLineFrequency": "n/a", "SamplingFrequency": -1.0, "SoftwareFilters": "n/a", } if nwbfile.institution is not None: dictionary["InstitutionName"] = nwbfile.institution all_acquisition_electrical_series = [ series for series in nwbfile.acquisition.values() if isinstance(series, pynwb.ecephys.ElectricalSeries) ] if len(all_acquisition_electrical_series) == 1: electrical_series = all_acquisition_electrical_series[0] if electrical_series.rate is not None: dictionary["SamplingFrequency"] = electrical_series.rate if electrical_series.data is not None and electrical_series.rate is not None: dictionary["RecordingDuration"] = electrical_series.data.shape[0] / electrical_series.rate electrode_group = electrical_series.electrodes[0].group[0] if (manufacturer := electrode_group.device.manufacturer) is not None and manufacturer != "": dictionary["Manufacturer"] = manufacturer if (model_number := electrode_group.device.model_number) is not None and model_number != "": dictionary["ManufacturersModelVersion"] = model_number if (brain_region := electrode_group.location) is not None and brain_region not in ["", "unknown", "n/a"]: dictionary["BodyPart"] = "BRAIN" dictionary["BodyPartDetails"] = brain_region general_metadata = cls(**dictionary) return general_metadata
[docs] @pydantic.validate_call def to_json(self, file_path: str | pathlib.Path) -> None: """ Save the general metadata to a JSON file. Parameters ---------- file_path : str or pathlib.Path The path to the file where the metadata will be saved. """ with pathlib.Path(file_path).open(mode="w") as file_stream: json.dump(obj=self.model_dump(exclude_none=True), fp=file_stream, indent=4)