|
9 | 9 | import pynwb |
10 | 10 | import typing_extensions |
11 | 11 |
|
| 12 | +from ._model_utils import _build_json_sidecar |
12 | 13 | from ..bids_models._base_metadata_model import BaseMetadataContainerModel, BaseMetadataModel |
13 | 14 | from ..notifications import Notification |
14 | 15 |
|
@@ -40,26 +41,149 @@ def _infer_scalar_field( |
40 | 41 |
|
41 | 42 |
|
42 | 43 | class Channel(BaseMetadataModel): |
43 | | - name: str |
44 | | - electrode_name: str |
45 | | - type: str = "n/a" |
46 | | - units: str = "V" |
47 | | - sampling_frequency: float | None = None |
48 | | - low_cutoff: float | None = None |
49 | | - high_cutoff: float | None = None |
50 | | - reference: str | None = None |
51 | | - notch: str | None = None |
52 | | - channel_label: str | None = None |
53 | | - stream_id: str | None = None |
54 | | - description: str | None = None |
55 | | - software_filter_types: str | None = None |
56 | | - status: typing.Literal["good", "bad"] | None = None |
57 | | - status_description: str | None = None |
58 | | - gain: float | None = None |
59 | | - time_offset: float | None = None |
60 | | - time_reference_channel: str | None = None |
61 | | - ground: str | None = None |
62 | | - recording_mode: str | None = None |
| 44 | + name: str = pydantic.Field( |
| 45 | + description="Label of the channel.", |
| 46 | + title="Channel name", |
| 47 | + ) |
| 48 | + electrode_name: str = pydantic.Field( |
| 49 | + description=( |
| 50 | + "Name of the electrode contact point. The value MUST match a name entry in the corresponding " |
| 51 | + "*_electrodes.tsv file, linking this channel to its associated electrode contact point. " |
| 52 | + "For channels not associated with an electrode, use n/a." |
| 53 | + ), |
| 54 | + title="Electrode name", |
| 55 | + ) |
| 56 | + type: str = pydantic.Field( |
| 57 | + description=( |
| 58 | + "Type of channel; MUST use the channel types listed below. Note that the type MUST be in upper-case." |
| 59 | + ), |
| 60 | + title="Type", |
| 61 | + default="n/a", |
| 62 | + ) |
| 63 | + units: str = pydantic.Field( |
| 64 | + description=( |
| 65 | + "Physical unit of the value represented in this channel, for example, V for Volt, or fT/cm for femto Tesla " |
| 66 | + "per centimeter (see Units)." |
| 67 | + ), |
| 68 | + title="Units", |
| 69 | + default="V", |
| 70 | + ) |
| 71 | + sampling_frequency: float | None = pydantic.Field( |
| 72 | + description="Sampling rate of the channel in Hz.", |
| 73 | + title="Sampling frequency", |
| 74 | + default=None, |
| 75 | + ) |
| 76 | + low_cutoff: float | None = pydantic.Field( |
| 77 | + description=( |
| 78 | + "Frequencies used for the high-pass filter applied to the channel in Hz. If no high-pass " |
| 79 | + "filter applied, use n/a." |
| 80 | + ), |
| 81 | + title="Low cutoff", |
| 82 | + default=None, |
| 83 | + ) |
| 84 | + high_cutoff: float | None = pydantic.Field( |
| 85 | + description=( |
| 86 | + "Frequencies used for the low-pass filter applied to the channel in Hz. " |
| 87 | + "If no low-pass filter applied, use n/a. Note that hardware anti-aliasing in A/D conversion of " |
| 88 | + "all MEG/EEG/EMG electronics applies a low-pass filter; specify its frequency here if applicable." |
| 89 | + ), |
| 90 | + title="High cutoff", |
| 91 | + default=None, |
| 92 | + ) |
| 93 | + reference: str | None = pydantic.Field( |
| 94 | + description=( |
| 95 | + "The reference for the given channel. When the reference is an electrode in *_electrodes.tsv, " |
| 96 | + 'use the name of that electrode. If a corresponding electrode is not applicable, use "n/a".' |
| 97 | + ), |
| 98 | + title="Reference", |
| 99 | + default=None, |
| 100 | + ) |
| 101 | + notch: str | None = pydantic.Field( |
| 102 | + description=( |
| 103 | + "Frequencies used for the notch filter applied to the channel, in Hz. " |
| 104 | + "If notch filters are applied at multiple frequencies, these frequencies MAY be specified as a list, " |
| 105 | + "for example, [60, 120, 180]. If no notch filter was applied, use n/a." |
| 106 | + ), |
| 107 | + title="Notch", |
| 108 | + default=None, |
| 109 | + ) |
| 110 | + channel_label: str | None = pydantic.Field( |
| 111 | + description=( |
| 112 | + "Human readable identifier. Use this name to specify the content of signals not generated by electrodes. " |
| 113 | + "For example, 'DAQ internal synchronization signals', 'behavioral signals', 'behavioral cues'." |
| 114 | + ), |
| 115 | + title="Channel label", |
| 116 | + default=None, |
| 117 | + ) |
| 118 | + stream_id: str | None = pydantic.Field( |
| 119 | + description="Data stream of the recording the signal.", |
| 120 | + title="Stream ID", |
| 121 | + default=None, |
| 122 | + ) |
| 123 | + description: str | None = pydantic.Field( |
| 124 | + description="Brief free-text description of the channel, or other information of interest.", |
| 125 | + title="Description", |
| 126 | + default=None, |
| 127 | + ) |
| 128 | + software_filter_types: str | None = pydantic.Field( |
| 129 | + description=( |
| 130 | + "The types of software filters applied to this channel. " |
| 131 | + "The Levels for this column SHOULD be defined in the accompanying *_channels.json file, " |
| 132 | + "mapping each filter type key to its description. Use n/a if no software filters were " |
| 133 | + "applied to this channel." |
| 134 | + ), |
| 135 | + title="Software filter types", |
| 136 | + default=None, |
| 137 | + ) |
| 138 | + status: typing.Literal["good", "bad", "n/a"] | None = pydantic.Field( |
| 139 | + description=( |
| 140 | + "Data quality observed on the channel. " |
| 141 | + "A channel is considered bad if its data quality is compromised by excessive noise. " |
| 142 | + "If quality is unknown, then a value of n/a may be used. Description of noise type SHOULD " |
| 143 | + "be provided in [status_description]." |
| 144 | + ), |
| 145 | + title="Status", |
| 146 | + default=None, |
| 147 | + ) |
| 148 | + status_description: str | None = pydantic.Field( |
| 149 | + description=( |
| 150 | + "Freeform text description of noise or artifact affecting data quality on the channel. " |
| 151 | + "It is meant to explain why the channel was declared bad in the status column." |
| 152 | + ), |
| 153 | + title="Status description", |
| 154 | + default=None, |
| 155 | + ) |
| 156 | + gain: float | None = pydantic.Field( |
| 157 | + description=( |
| 158 | + "Amplification factor applied from signal detection at the electrode to the signal stored in the data file." |
| 159 | + " If no gain factor is provided it is assumed to be 1." |
| 160 | + ), |
| 161 | + title="Gain", |
| 162 | + default=None, |
| 163 | + ) |
| 164 | + time_offset: float | None = pydantic.Field( |
| 165 | + description="Time shift between signal of this channel to a reference channel in seconds.", |
| 166 | + title="Time offset", |
| 167 | + default=None, |
| 168 | + ) |
| 169 | + time_reference_channel: str | None = pydantic.Field( |
| 170 | + description="Name of the channel that is used for time alignment of signals.", |
| 171 | + title="Time reference channel", |
| 172 | + default=None, |
| 173 | + ) |
| 174 | + ground: str | None = pydantic.Field( |
| 175 | + description=( |
| 176 | + "Information on the ground. For example, 'chamber screw', 'head post', 'ear clip'. " |
| 177 | + "Only should be used to optionally override the global ground in the _ecephys.json or _icephys.json file." |
| 178 | + ), |
| 179 | + title="Ground", |
| 180 | + default=None, |
| 181 | + ) |
| 182 | + recording_mode: str | None = pydantic.Field( |
| 183 | + description="The mode of recording for patch clamp datasets (for example, voltage clamp, current clamp).", |
| 184 | + title="Recording mode", |
| 185 | + default=None, |
| 186 | + ) |
63 | 187 |
|
64 | 188 |
|
65 | 189 | class ChannelTable(BaseMetadataContainerModel): |
@@ -246,9 +370,9 @@ def to_json(self, file_path: str | pathlib.Path) -> None: |
246 | 370 | file_path : path |
247 | 371 | The path to the output JSON file. |
248 | 372 | """ |
| 373 | + file_path = pathlib.Path(file_path) |
| 374 | + |
| 375 | + json_content = _build_json_sidecar(models=self.channels) |
| 376 | + |
249 | 377 | with file_path.open(mode="w") as file_stream: |
250 | | - json.dump( |
251 | | - obj=dict(), # TODO |
252 | | - fp=file_stream, |
253 | | - indent=4, |
254 | | - ) |
| 378 | + json.dump(obj=json_content, fp=file_stream, indent=4) |
0 commit comments