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 |
|---|---|---|---|
|
Brain region where the fiber sits |
Yes |
value |
|
Excitation wavelength driving this trace |
Yes |
value |
|
Emission wavelength collected |
Yes |
value |
|
The implanted optical fiber |
Yes |
link to |
|
The light source |
Yes |
link to |
|
The detector |
Yes |
link to |
|
The fluorescent indicator |
Yes |
link to |
|
Stereotactic coordinates (AP, ML, DV in mm) |
No |
value |
|
Free-text notes about the trace |
No |
value |
|
Filter on the excitation path |
No |
link to |
|
Filter on the emission path |
No |
link to |
|
Dichroic mirror in the light path |
No |
link to |
|
Commanded-voltage drive signal |
No |
link to |
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.