.. _annotate_fiber_photometry_metadata: How to Annotate Fiber Photometry Metadata ========================================= In general, neuroconv fills in as much metadata as we can extract from the source files. Most fiber photometry acquisition formats store little beyond the fluorescence traces themselves, so a conversion you run without adding any metadata writes just those traces, as a single ``FiberPhotometryResponseSeries``, and nothing that describes the nature of each trace: .. code-block:: python from neuroconv.tools.testing import MockFiberPhotometryInterface interface = MockFiberPhotometryInterface(num_fibers=2) nwbfile = interface.create_nwbfile() .. admonition:: Resulting structure :class: tip .. code-block:: text acquisition └── FiberPhotometryResponseSeries data (100, 2) (two traces, nothing describing them) The examples here use :py:class:`~neuroconv.tools.testing.mock_interfaces.MockFiberPhotometryInterface`, which synthesizes traces instead of reading a file, so every snippet runs as written with no data to download. Everything after the constructor is the same for any fiber photometry interface: swap in :py:class:`~neuroconv.datainterfaces.fiber_photometry.tdt.tdtfiberphotometrydatainterface.TDTFiberPhotometryInterface`, :py:class:`~neuroconv.datainterfaces.fiber_photometry.doric.doricfiberphotometrydatainterface.DoricFiberPhotometryInterface` or any other, with the arguments its format needs, and annotate the metadata exactly as shown. See the :ref:`fiber photometry section of the Conversion Gallery ` for how to construct each one. If you already know the shape and only want something to fill in, the same structure is printed as a YAML and a JSON file at :ref:`metadata_templates`. A **trace** is one column of that series, one fluorescence time series, and it is the unit everything below describes: one trace, one row. By itself a trace is just a column of numbers. Nothing records which optical fiber it came from, where in the brain, at what excitation wavelength, or which indicator it reports. That description is the provenance you add. Here we use `ndx-fiber-photometry `_, which stores the metadata for each trace in a ``FiberPhotometryTable``: each trace of the ``FiberPhotometryResponseSeries`` links to one row of the table, and the row carries that trace's provenance. Annotating a recording is building that table and pointing each trace at its row. Each row has these columns: .. list-table:: :header-rows: 1 :widths: 27 43 12 18 * - Column - What it records - Required - Value or link * - ``location`` - Brain region where the fiber sits - Yes - value * - ``excitation_wavelength_in_nm`` - Excitation wavelength driving this trace - Yes - value * - ``emission_wavelength_in_nm`` - Emission wavelength collected - Yes - value * - ``optical_fiber`` - The implanted optical fiber - Yes - link to ``OpticalFiber`` * - ``excitation_source`` - The light source - Yes - link to ``ExcitationSource`` * - ``photodetector`` - The detector - Yes - link to ``Photodetector`` * - ``indicator`` - The fluorescent indicator - Yes - link to ``Indicator`` * - ``coordinates`` - Stereotactic coordinates (AP, ML, DV in mm) - No - value * - ``notes`` - Free-text notes about the trace - No - value * - ``excitation_filter`` - Filter on the excitation path - No - link to ``OpticalFilter`` * - ``emission_filter`` - Filter on the emission path - No - link to ``OpticalFilter`` * - ``dichroic_mirror`` - Dichroic mirror in the light path - No - link to ``DichroicMirror`` * - ``commanded_voltage_series`` - Commanded-voltage drive signal - No - link to ``CommandedVoltageSeries`` A value is stored on the row itself; a link points to a device or indicator defined elsewhere in the metadata and can be shared across rows. This guide fills only the required columns. The link, for a one-fiber signal + isosbestic recording, looks like this. One table serves the whole file, and every series points into it: .. code-block:: text FiberPhotometryResponseSeries465 (time × 1 trace) column 0 ──▶ row "vta_465" via fiber_photometry_table_region FiberPhotometryResponseSeries405 (time × 1 trace) column 0 ──▶ row "vta_405" FiberPhotometryTable row location exc/em optical_fiber excitation_source photodetector indicator ─────── ──────── ─────── ────────────── ─────────────────── ───────────────── ──────────────── vta_465 VTA 465/525 links to fiber links to source 465 links to detector links to GCaMP6s vta_405 VTA 405/525 links to fiber links to source 405 links to detector links to GCaMP6s How to Annotate a Single Fiber at a Single Wavelength ----------------------------------------------------- The simplest case is one fiber at one excitation wavelength: one trace, one table row, and one series pointing at it. We build it in four steps, the row, the indicator it read, the hardware that recorded it, and that hardware's models, then write the file. Each step below shows the **whole script so far** with the new lines highlighted, so the last block is the complete, runnable script. **Create the row and point the series at it.** Start from the interface, passing an explicit ``metadata_key`` to name the response-series entry (the key under ``metadata["FiberPhotometry"]`` where this series' metadata lives). A table row then describes one trace, one fiber at one excitation wavelength: fill in the values it holds itself, its location and wavelengths, and point the series at the row through ``fiber_photometry_table_region``: .. code-block:: python :emphasize-lines: 15-19, 22 from neuroconv.tools.testing import MockFiberPhotometryInterface metadata_key = "calcium_signal" interface = MockFiberPhotometryInterface( excitation_wavelengths_in_nm=465.0, metadata_key=metadata_key ) metadata = interface.get_metadata() fiber_photometry = metadata["FiberPhotometry"] row_key = "vta_465" fiber_photometry["FiberPhotometryTable"] = { "name": "fiber_photometry_table", "description": "One fiber, one wavelength.", "rows": { row_key: { "location": "VTA", "excitation_wavelength_in_nm": 465.0, "emission_wavelength_in_nm": 525.0, }, }, } fiber_photometry[metadata_key]["fiber_photometry_table_region"] = [row_key] ``row_key`` does two different jobs here. As the key in the table's ``rows`` dict it **names the row**, the row's data is stored under it. In ``fiber_photometry[metadata_key]["fiber_photometry_table_region"] = [row_key]`` it is used as a **reference**: this is where the series is linked to the row, by pointing its region at that key. Using the same variable for both guarantees the link lands on exactly the row we just named. The 465 appears twice, and only because the source here is synthetic: the mock has to be told which excitation drove the trace it is inventing, and the row then records that as provenance. With a real interface the wavelength is a fact about the recording, so it is stated once, in the row. .. admonition:: The file so far :class: note The series points at a one-row table, and the row holds the trace's own values: its location and the excitation and emission wavelengths. .. code-block:: text acquisition └── FiberPhotometryResponseSeries ──▶ FiberPhotometryTable · row "vta_465" FiberPhotometryTable └── vta_465 location=VTA excitation=465 nm emission=525 nm The file now describes where and at what wavelengths the trace was recorded, and the series is bound to that row. The other half of *what* the trace measured is the sensor being read, the indicator, so we name that next. **Add the indicator and point the row at it.** The indicator is the fluorescent sensor the trace reads, GCaMP6s here, a genetically encoded calcium indicator. Define it, then set the row's ``indicator_metadata_key`` to reference it: .. code-block:: python :emphasize-lines: 24-31 from neuroconv.tools.testing import MockFiberPhotometryInterface metadata_key = "calcium_signal" interface = MockFiberPhotometryInterface( excitation_wavelengths_in_nm=465.0, metadata_key=metadata_key ) metadata = interface.get_metadata() fiber_photometry = metadata["FiberPhotometry"] row_key = "vta_465" fiber_photometry["FiberPhotometryTable"] = { "name": "fiber_photometry_table", "description": "One fiber, one wavelength.", "rows": { row_key: { "location": "VTA", "excitation_wavelength_in_nm": 465.0, "emission_wavelength_in_nm": 525.0, }, }, } fiber_photometry[metadata_key]["fiber_photometry_table_region"] = [row_key] indicator_key = "gcamp" fiber_photometry["FiberPhotometryIndicators"] = { indicator_key: { "name": "gcamp", "label": "GCaMP6s", }, } fiber_photometry["FiberPhotometryTable"]["rows"][row_key]["indicator_metadata_key"] = indicator_key .. admonition:: The file so far :class: note The indicator is defined, and the row now names it, recording what the trace's fluorescence reports. .. code-block:: text acquisition └── FiberPhotometryResponseSeries ──▶ FiberPhotometryTable · row "vta_465" FiberPhotometryTable └── vta_465 location=VTA excitation=465 nm emission=525 nm └── indicator → gcamp · "GCaMP6s" That completes *what* the trace measured. Now we describe *how* it was measured: the hardware that read the indicator, so a downstream analyst can follow each trace back to the exact fiber, light source, and detector it came from. **Add the devices and point the row at them.** A device is the specific physical unit used in this recording, and it holds the facts about that unit as it was used here. The clearest example is the optical fiber's ``fiber_insertion``: where in the brain it was implanted, its stereotactic coordinates and depth. That is often the most experimentally important metadata in the whole chain, it is what lets a downstream analyst say which brain region each trace reports. An excitation source can carry the power and intensity it was driven at, and a photodetector its gain, the per-recording configuration a reader needs to interpret or reproduce the measurement. Define the three devices in the top-level ``Devices`` registry, then set the row's reference keys to point at them: .. code-block:: python :emphasize-lines: 33-58 from neuroconv.tools.testing import MockFiberPhotometryInterface metadata_key = "calcium_signal" interface = MockFiberPhotometryInterface( excitation_wavelengths_in_nm=465.0, metadata_key=metadata_key ) metadata = interface.get_metadata() fiber_photometry = metadata["FiberPhotometry"] row_key = "vta_465" fiber_photometry["FiberPhotometryTable"] = { "name": "fiber_photometry_table", "description": "One fiber, one wavelength.", "rows": { row_key: { "location": "VTA", "excitation_wavelength_in_nm": 465.0, "emission_wavelength_in_nm": 525.0, }, }, } fiber_photometry[metadata_key]["fiber_photometry_table_region"] = [row_key] indicator_key = "gcamp" fiber_photometry["FiberPhotometryIndicators"] = { indicator_key: { "name": "gcamp", "label": "GCaMP6s", }, } fiber_photometry["FiberPhotometryTable"]["rows"][row_key]["indicator_metadata_key"] = indicator_key optical_fiber_key = "optical_fiber" excitation_source_key = "excitation_source_465" photodetector_key = "photodetector" metadata["Devices"] = { optical_fiber_key: { "type": "OpticalFiber", "name": "optical_fiber", "fiber_insertion": { "depth_in_mm": 4.0, "insertion_position_ap_in_mm": 3.0, }, }, excitation_source_key: { "type": "ExcitationSource", "name": "excitation_source_465", }, photodetector_key: { "type": "Photodetector", "name": "photodetector", }, } row = fiber_photometry["FiberPhotometryTable"]["rows"][row_key] row["optical_fiber_metadata_key"] = optical_fiber_key row["excitation_source_metadata_key"] = excitation_source_key row["photodetector_metadata_key"] = photodetector_key .. admonition:: The file so far :class: note The row now resolves to the three physical devices that recorded the trace; only the reusable hardware specifications are still missing. .. code-block:: text acquisition └── FiberPhotometryResponseSeries ──▶ FiberPhotometryTable · row "vta_465" FiberPhotometryTable └── vta_465 location=VTA excitation=465 nm emission=525 nm ├── optical_fiber → optical_fiber ├── excitation_source → excitation_source_465 ├── photodetector → photodetector └── indicator → gcamp · "GCaMP6s" The devices say which units were used and how, but not their specifications, the fiber's numerical aperture, the light source type, the detector type. Those are identical across every recording that used the same equipment, so they belong in a shared device model rather than being repeated on each device. We add the models next, attach one to each device, and write the file. **Add the device models, attach them, and write the file.** Each model in the top-level ``DeviceModels`` registry carries the make and specifications of a piece of hardware; the ``type`` field names its concrete ndx-ophys-devices class. Define the models, point each device at its model with ``device_model_metadata_key``, and convert. This last block is the complete, runnable script: .. code-block:: python :emphasize-lines: 60-86 from neuroconv.tools.testing import MockFiberPhotometryInterface metadata_key = "calcium_signal" interface = MockFiberPhotometryInterface( excitation_wavelengths_in_nm=465.0, metadata_key=metadata_key ) metadata = interface.get_metadata() fiber_photometry = metadata["FiberPhotometry"] row_key = "vta_465" fiber_photometry["FiberPhotometryTable"] = { "name": "fiber_photometry_table", "description": "One fiber, one wavelength.", "rows": { row_key: { "location": "VTA", "excitation_wavelength_in_nm": 465.0, "emission_wavelength_in_nm": 525.0, }, }, } fiber_photometry[metadata_key]["fiber_photometry_table_region"] = [row_key] indicator_key = "gcamp" fiber_photometry["FiberPhotometryIndicators"] = { indicator_key: { "name": "gcamp", "label": "GCaMP6s", }, } fiber_photometry["FiberPhotometryTable"]["rows"][row_key]["indicator_metadata_key"] = indicator_key optical_fiber_key = "optical_fiber" excitation_source_key = "excitation_source_465" photodetector_key = "photodetector" metadata["Devices"] = { optical_fiber_key: { "type": "OpticalFiber", "name": "optical_fiber", "fiber_insertion": { "depth_in_mm": 4.0, "insertion_position_ap_in_mm": 3.0, }, }, excitation_source_key: { "type": "ExcitationSource", "name": "excitation_source_465", }, photodetector_key: { "type": "Photodetector", "name": "photodetector", }, } row = fiber_photometry["FiberPhotometryTable"]["rows"][row_key] row["optical_fiber_metadata_key"] = optical_fiber_key row["excitation_source_metadata_key"] = excitation_source_key row["photodetector_metadata_key"] = photodetector_key optical_fiber_model_key = "optical_fiber_model" excitation_source_model_key = "excitation_source_model" photodetector_model_key = "photodetector_model" metadata["DeviceModels"] = { optical_fiber_model_key: { "type": "OpticalFiberModel", "name": "optical_fiber_model", "manufacturer": "Doric Lenses", "numerical_aperture": 0.48, }, excitation_source_model_key: { "type": "ExcitationSourceModel", "name": "excitation_source_model", "manufacturer": "Doric Lenses", "source_type": "LED", "excitation_mode": "one-photon", }, photodetector_model_key: { "type": "PhotodetectorModel", "name": "photodetector_model", "manufacturer": "Doric Lenses", "detector_type": "photodiode", }, } metadata["Devices"][optical_fiber_key]["device_model_metadata_key"] = optical_fiber_model_key metadata["Devices"][excitation_source_key]["device_model_metadata_key"] = excitation_source_model_key metadata["Devices"][photodetector_key]["device_model_metadata_key"] = photodetector_model_key nwbfile = interface.create_nwbfile(metadata=metadata) .. admonition:: Resulting structure :class: tip .. code-block:: text acquisition └── FiberPhotometryResponseSeries data (100,) ──▶ FiberPhotometryTable · row "vta_465" FiberPhotometryTable (1 row) └── vta_465 location=VTA excitation=465 nm emission=525 nm ├── optical_fiber → optical_fiber (model: optical_fiber_model) ├── excitation_source → excitation_source_465 (model: excitation_source_model) ├── photodetector → photodetector (model: photodetector_model) └── indicator → gcamp · "GCaMP6s" Each ``*_metadata_key`` in the row is resolved to the actual NWB object at write time, so you name each fiber, source, detector, and indicator once in its own block and reference it by key from the row. How to Annotate a Signal and Isosbestic Control ----------------------------------------------- The near-universal GCaMP setup records one fiber at two excitation wavelengths, the calcium-dependent signal (465 nm) and an isosbestic control (405 nm). That is two traces, so the table gets **two rows**, one per trace, differing in their excitation wavelength and excitation source. The two traces do **not** go into one series. ndx-fiber-photometry `recommends `_ one series per excitation and emission wavelength, with one column per fiber, so each wavelength gets its own ``FiberPhotometryResponseSeries``. Both series point into the same table, each at its own row. One interface writes one series, so this is two interfaces, each with its own ``metadata_key``, combined in a converter: .. code-block:: python :emphasize-lines: 5-16, 84-90 from neuroconv import ConverterPipe from neuroconv.tools.testing import MockFiberPhotometryInterface # One interface per excitation wavelength, each writing its own series. signal_key = "gcamp_vta_465" control_key = "gcamp_vta_405" converter = ConverterPipe( data_interfaces={ "signal": MockFiberPhotometryInterface( excitation_wavelengths_in_nm=465.0, metadata_key=signal_key ), "control": MockFiberPhotometryInterface( excitation_wavelengths_in_nm=405.0, metadata_key=control_key ), } ) metadata = converter.get_metadata() # The metadata keys that wire the blocks together. Each is used both where its object is defined and # where another block references it, so the linking is explicit rather than matched by eye. optical_fiber_key = "optical_fiber" signal_source_key = "excitation_source_465" control_source_key = "excitation_source_405" photodetector_key = "photodetector" indicator_key = "gcamp" signal_row_key = "vta_465" control_row_key = "vta_405" # One fiber and one detector, but two excitation sources: the 465 nm signal and the 405 nm isosbestic. metadata["Devices"] = { optical_fiber_key: { "type": "OpticalFiber", "name": "optical_fiber", "fiber_insertion": { "depth_in_mm": 4.0, "insertion_position_ap_in_mm": 3.0, }, }, signal_source_key: { "type": "ExcitationSource", "name": "excitation_source_465", }, control_source_key: { "type": "ExcitationSource", "name": "excitation_source_405", }, photodetector_key: { "type": "Photodetector", "name": "photodetector", }, } fiber_photometry = metadata["FiberPhotometry"] fiber_photometry["FiberPhotometryIndicators"] = { indicator_key: { "name": "gcamp", "label": "GCaMP6s", }, } # Both traces share the same site, emission wavelength, indicator, fiber, and detector. shared_row_metadata = { "location": "VTA", "emission_wavelength_in_nm": 525.0, "indicator_metadata_key": indicator_key, "optical_fiber_metadata_key": optical_fiber_key, "photodetector_metadata_key": photodetector_key, } fiber_photometry["FiberPhotometryTable"] = { "name": "fiber_photometry_table", "description": "One fiber, signal + isosbestic control.", "rows": { signal_row_key: { **shared_row_metadata, "excitation_wavelength_in_nm": 465.0, "excitation_source_metadata_key": signal_source_key, }, control_row_key: { **shared_row_metadata, "excitation_wavelength_in_nm": 405.0, "excitation_source_metadata_key": control_source_key, }, }, } # Each series is named, described, and pointed at its own single row. fiber_photometry[signal_key]["name"] = "FiberPhotometryResponseSeries465" fiber_photometry[signal_key]["description"] = "GCaMP6s calcium signal in VTA, 465 nm excitation." fiber_photometry[signal_key]["fiber_photometry_table_region"] = [signal_row_key] fiber_photometry[control_key]["name"] = "FiberPhotometryResponseSeries405" fiber_photometry[control_key]["description"] = "Isosbestic control in VTA, 405 nm excitation." fiber_photometry[control_key]["fiber_photometry_table_region"] = [control_row_key] nwbfile = converter.create_nwbfile(metadata=metadata) Two traces, so two rows. Everything the two traces have in common lives in ``shared_row_metadata``, the site, emission wavelength, indicator, fiber, and detector, so each row spreads ``shared_row_metadata`` and then adds only the two fields that differ: its ``excitation_wavelength_in_nm`` and its ``excitation_source_metadata_key``. That is exactly what the isosbestic setup is: one measurement site read at two excitation wavelengths. The metadata blocks are unchanged by the split. There is one ``Devices`` registry, one ``FiberPhotometryTable``, and one indicator, all built exactly as in the previous section, because the converter merges the metadata of both interfaces before writing. Only the last block differs: each interface's own entry, addressed by its ``metadata_key``, names its series and points it at its one row (highlighted). .. admonition:: Resulting structure :class: tip .. code-block:: text acquisition ├── FiberPhotometryResponseSeries465 data (100,) ──▶ FiberPhotometryTable · row vta_465 └── FiberPhotometryResponseSeries405 data (100,) ──▶ FiberPhotometryTable · row vta_405 FiberPhotometryTable (2 rows, shared by both series) ├── vta_465 excitation=465 nm src → excitation_source_465 (the calcium signal) └── vta_405 excitation=405 nm src → excitation_source_405 (the isosbestic control) How to Annotate Multiple Fibers in Different Locations ------------------------------------------------------ The previous section multiplied the table rows along the *excitation-wavelength* axis: one fiber, two wavelengths, and so two series. This section multiplies them along the *spatial* axis instead: two physically separate fibers in two regions (here DMS, the dorsomedial striatum, and DLS, the dorsolateral striatum), each measuring the same indicator at the same wavelength. Each fiber gets its own ``OpticalFiber`` instance and its own table row, so the rows now differ in ``optical_fiber`` and ``location`` where the signal/isosbestic rows differed in ``excitation_wavelength``. The two axes are not handled the same way, and this is the point where that matters. Extra *wavelengths* become extra series, as above. Extra *fibers* become extra columns of one series, which is exactly the layout ndx-fiber-photometry recommends: ``[ntime, nfibers]`` at one wavelength. So here one interface is enough, and its region lists both rows. .. code-block:: python :emphasize-lines: 67-68, 72-73, 78 from neuroconv.tools.testing import MockFiberPhotometryInterface metadata_key = "gcamp_striatum" interface = MockFiberPhotometryInterface( excitation_wavelengths_in_nm=465.0, num_fibers=2, metadata_key=metadata_key ) metadata = interface.get_metadata() # The metadata keys that wire the blocks together. dms_fiber_key = "optical_fiber_dms" dls_fiber_key = "optical_fiber_dls" excitation_source_key = "excitation_source_465" photodetector_key = "photodetector" indicator_key = "gcamp" dms_row_key = "dms_465" dls_row_key = "dls_465" # Two optical fibers, one per region; a shared excitation source and detector. metadata["Devices"] = { dms_fiber_key: { "type": "OpticalFiber", "name": "optical_fiber_dms", "fiber_insertion": { "depth_in_mm": 4.2, "insertion_position_ap_in_mm": 0.8, }, }, dls_fiber_key: { "type": "OpticalFiber", "name": "optical_fiber_dls", "fiber_insertion": { "depth_in_mm": 4.0, "insertion_position_ap_in_mm": 0.5, }, }, excitation_source_key: { "type": "ExcitationSource", "name": "excitation_source_465", }, photodetector_key: { "type": "Photodetector", "name": "photodetector", }, } fiber_photometry = metadata["FiberPhotometry"] fiber_photometry["FiberPhotometryIndicators"] = { indicator_key: { "name": "gcamp", "label": "GCaMP6s", }, } # Both fibers share the same wavelengths, indicator, excitation source, and detector. shared_row_metadata = { "excitation_wavelength_in_nm": 465.0, "emission_wavelength_in_nm": 525.0, "indicator_metadata_key": indicator_key, "excitation_source_metadata_key": excitation_source_key, "photodetector_metadata_key": photodetector_key, } fiber_photometry["FiberPhotometryTable"] = { "name": "fiber_photometry_table", "description": "Two fibers in two regions.", "rows": { dms_row_key: { **shared_row_metadata, "location": "DMS", "optical_fiber_metadata_key": dms_fiber_key, }, dls_row_key: { **shared_row_metadata, "location": "DLS", "optical_fiber_metadata_key": dls_fiber_key, }, }, } fiber_photometry[metadata_key]["description"] = "GCaMP6s in DMS and DLS." fiber_photometry[metadata_key]["fiber_photometry_table_region"] = [dms_row_key, dls_row_key] nwbfile = interface.create_nwbfile(metadata=metadata) Two fibers, so two rows. This time ``shared_row_metadata`` holds the wavelengths, indicator, excitation source, and detector, and each row adds only its ``location`` and its ``optical_fiber_metadata_key`` (highlighted). At the level of the rows that is the mirror image of the signal/isosbestic case: there the wavelength differed and the fiber was shared, here the fiber differs and the wavelength is shared, so the very field that moved into ``shared_row_metadata`` is the one that was per-row before. The series are what break the symmetry, one here against two there. The ``fiber_photometry_table_region`` list ``["dms_465", "dls_465"]`` maps column 0 (the DMS fiber) to ``dms_465`` and column 1 (the DLS fiber) to ``dls_465``. .. admonition:: Resulting structure :class: tip .. code-block:: text FiberPhotometryTable (2 rows) ├── dms_465 location=DMS fiber → optical_fiber_dms └── dls_465 location=DLS fiber → optical_fiber_dls Real setups usually combine both axes, and the two combine cleanly: N fibers each recorded at a signal and an isosbestic wavelength give 2N rows, one per (fiber, wavelength) trace, written as two series of N columns each. One interface per wavelength, ``num_fibers=N`` on each, one shared table. .. _how_to_annotate_from_a_template: How to Annotate from a Template ------------------------------- The sections above build the chain by hand, one block at a time, so that each piece and each link between them is visible. Once you know the shape, you do not have to type it again. ``get_metadata_template()`` returns the whole thing already assembled, sized to the traces this interface writes, with every cross-reference resolved and every field only you can answer set to ``None``: .. code-block:: python from neuroconv.tools.testing import MockFiberPhotometryInterface interface = MockFiberPhotometryInterface( excitation_wavelengths_in_nm=465.0, metadata_key="calcium_signal" ) metadata = interface.get_metadata_template() What comes back is printed in full, in both YAML and JSON, at :ref:`fiber_photometry_metadata_template`. This is a discovery aid, and it answers the question the earlier sections answer in prose: what does this file need from me? **The blanks are the checklist.** What comes back ``None`` is exactly what the source could not tell us, and everything else is already done, in particular the ``*_metadata_key`` cross-references and the series' ``fiber_photometry_table_region``, which are the tedious part to reconstruct by hand. It also shows what it does not require. The dichroic mirror, the two filters and the three device models are optional, and they appear so that you know the writer accepts them at all; drop the ones this recording did not use. Every entry's ``name`` comes back blank, so keeping one costs naming it, which is what stops an offered entry reaching the file because nobody looked at it. ``trace_0`` and the device keys are handles rather than names in the file, so rename them freely, as long as the table region keeps naming the same rows. Filling in the one-fiber recording from the first section: .. code-block:: python :emphasize-lines: 9-21 from neuroconv.tools.testing import MockFiberPhotometryInterface interface = MockFiberPhotometryInterface( excitation_wavelengths_in_nm=465.0, metadata_key="calcium_signal" ) metadata = interface.get_metadata_template() fiber_photometry = metadata["FiberPhotometry"] row = fiber_photometry["FiberPhotometryTable"]["rows"]["trace_0"] row["location"] = "VTA" row["excitation_wavelength_in_nm"] = 465.0 row["emission_wavelength_in_nm"] = 525.0 indicator = fiber_photometry["FiberPhotometryIndicators"]["indicator"] indicator["name"] = "indicator" indicator["label"] = "GCaMP6s" metadata["Devices"]["optical_fiber_0"]["fiber_insertion"] = { "depth_in_mm": 4.0, "insertion_position_ap_in_mm": 3.0, } for device_key in ("optical_fiber_0", "excitation_source", "photodetector"): metadata["Devices"][device_key]["name"] = device_key # This rig had no filters and no dichroic mirror, and we do not know the catalog models. del metadata["DeviceModels"] for unused_device in ("dichroic_mirror", "excitation_filter", "emission_filter"): del metadata["Devices"][unused_device] for device_key in ("optical_fiber_0", "excitation_source", "photodetector"): del metadata["Devices"][device_key]["device_model_metadata_key"] for unused_field in ("coordinates", "notes", "dichroic_mirror_metadata_key", "excitation_filter_metadata_key", "emission_filter_metadata_key"): del row[unused_field] del fiber_photometry["calcium_signal"]["description"] nwbfile = interface.create_nwbfile(metadata=metadata) That writes the same file as the first section. Which route to take is a matter of taste: the template saves you remembering the nesting and wiring the references by hand, while building the dictionary yourself keeps only what you need in front of you.