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:

from neuroconv.tools.testing import MockFiberPhotometryInterface

interface = MockFiberPhotometryInterface(num_fibers=2)
nwbfile = interface.create_nwbfile()

Resulting structure

acquisition
└── FiberPhotometryResponseSeries    data (100, 2)     (two traces, nothing describing them)

The examples here use 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 TDTFiberPhotometryInterface, DoricFiberPhotometryInterface or any other, with the arguments its format needs, and annotate the metadata exactly as shown. See the 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 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:

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:

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:

 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.

The file so far

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.

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:

 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

The file so far

The indicator is defined, and the row now names it, recording what the trace’s fluorescence reports.

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:

 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

The file so far

The row now resolves to the three physical devices that recorded the trace; only the reusable hardware specifications are still missing.

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:

 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)

Resulting structure

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:

 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).

Resulting structure

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.

 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.

Resulting structure

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#

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:

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 Fiber Photometry.

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:

 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.