Hello again. Last lesson established the versioned identity of your target: you selected a materialization, resolved a source root ID against it, and—where possible—corroborated the result with a physical coordinate. Keep that resolved ID and its materialization context intact: this lesson uses them to ask, “What metadata and labels are available for this reconstruction at that snapshot?”
You will retrieve two distinct kinds of information:
- Hierarchical annotations: a relatively structured one-row profile, including fields such as cell class, putative cell type, hemisphere, lineage, neurotransmitter information, and cross-database identifiers.
- Community annotations: contributed tags and associated records, which can be useful evidence but may be duplicated, incomplete, or inconsistent.
By the end, you will have a compact, exportable metadata record for one resolved neuron—ready for synapse queries in the next lesson. Plan for about 40 minutes.
Metadata is not a single source of truth
A reconstruction and its metadata answer different questions.
- The segmentation says which voxels belong to a reconstructed object at a particular materialization.
- A hierarchical annotation describes what researchers think that object is: for example, its broad class, side, morphology group, or likely neurotransmitter.
- A community annotation is a contributed label attached at a location, often recording a useful human interpretation or a cross-reference.
Those layers are related but should not be collapsed into one claim. A cell can be well reconstructed but untyped; it can have a confident predicted neurotransmitter without a literature-backed transmitter label; it can have several community tags that use different naming conventions.
The FlyWire interface brings several of these layers together: imagery and a 3D reconstruction support inspection, while graph, synapse, and table panels support interpretation and analysis.

For notebook analysis, treat the metadata lookup as a versioned join:
| Layer | Version or provenance to record | Main role |
|---|---|---|
| Segmentation | MATERIALIZATION_VERSION | Defines the root ID and reconstruction snapshot. |
| Hierarchical annotation table | Annotation release, tag, or reported commit | Supplies structured biological metadata. |
| Community annotation table | Materialization and query date | Supplies contributed tags and their records. |
The first layer was pinned in the prior lesson. The key new point is that the hierarchical annotation repository has its own version. The fafbseg function can use an available default, but a finished analysis should record precisely which annotation release or commit was used.
fafbseg.flywire.search_annotations - fafbseg 3.2.2 documentation
Read the fafbseg documentation reference for search_annotations. It establishes the distinction between the annotation-table version and the segmentation materialization, then shows the expected one-neuron lookup pattern.
In the API reference, read the overview beginning with the annotation source note. Then, in the Parameters section, read the annotation_version and materialization descriptions, especially the materialization options. Finish with the root-ID example in the Examples section. Notice that the function returns a pandas DataFrame, even when one row is expected.
Retrieve the structured hierarchical annotation
Start from the notebook state created in the previous lesson. This example deliberately refuses to continue unless the previous root-resolution decision was unambiguous. If your case needed review, return to the 3D viewer and your spatial anchor before treating it as a single-neuron profile.
from fafbseg import flywire
import pandas as pd
# These should already exist from the previous lesson.
# DATASET = "public"
# MATERIALIZATION_VERSION = ...
# resolution_decision = ...
# candidate_records = ...
if resolution_decision != "resolved_unique_candidate":
raise RuntimeError(
"This notebook requires a uniquely resolved root ID. "
"Review the resolution evidence before continuing."
)
RESOLVED_ROOT_ID = int(candidate_records[0]["resolved_root_id"])
print("Dataset:", DATASET)
print("Materialization:", MATERIALIZATION_VERSION)
print("Resolved root ID:", RESOLVED_ROOT_ID)
Now query the hierarchical annotation table. Pass the materialization explicitly. Leaving it as the default "auto" may be convenient during exploration, but it delegates a reproducibility decision to the library.
# None means that fafbseg selects its configured default annotation version.
# For a final project, replace None with a specific recorded release tag
# once you know which annotation release you want to use.
ANNOTATION_VERSION = None
metadata = flywire.search_annotations(
RESOLVED_ROOT_ID,
materialization=int(MATERIALIZATION_VERSION),
annotation_version=ANNOTATION_VERSION,
dataset=DATASET,
)
print("Rows returned:", len(metadata))
display(metadata)
A root-ID lookup normally returns one row. Still, do not immediately write metadata.iloc[0]: first test the result’s cardinality.
if len(metadata) == 0:
raise LookupError(
"No hierarchical annotation row was found. "
"This does not prove the reconstruction is invalid or non-neuronal. "
"Check the root ID, materialization, and annotation-table version."
)
if len(metadata) > 1:
raise RuntimeError(
"Expected one hierarchical annotation row for this root ID, "
f"but received {len(metadata)}. Inspect the returned rows before choosing one."
)
metadata_row = metadata.iloc[0]
if "root_id" in metadata.columns:
returned_root_id = int(metadata_row["root_id"])
if returned_root_id != RESOLVED_ROOT_ID:
raise RuntimeError(
"Returned metadata root ID differs from the resolved root ID. "
"Do not silently combine these records; inspect version settings."
)
print("One matching hierarchical annotation row retrieved.")
This check separates three very different situations:
| Result | Interpretation | Appropriate response |
|---|---|---|
| One row, matching root ID | Normal single-neuron profile | Inspect and record its fields. |
| Zero rows | No structured annotation is available under the chosen query context | Preserve the empty result as a valid finding; do not invent a type. |
| More than one row or mismatched root | A version or identity assumption needs inspection | Stop and investigate before profiling. |
An empty result is particularly important to interpret correctly. The absence of a hierarchical row means no retrieved structured annotation, not “this is not a neuron,” “this neuron has no function,” or “the reconstruction is wrong.”
Read metadata as evidence with different strengths
The returned DataFrame often has many columns. Displaying everything horizontally is awkward, so transpose a selected group of fields into a vertical inspection view.
core_fields = [
"root_id",
"supervoxel_id",
"nucleus_id",
"flow",
"super_class",
"cell_class",
"cell_sub_class",
"cell_type",
"hemibrain_type",
"morphology_group",
"top_nt",
"top_nt_conf",
"known_nt",
"known_nt_source",
"side",
"nerve",
"vfb_id",
"fbbt_id",
"status",
]
available_core_fields = [
column for column in core_fields
if column in metadata.columns
]
metadata_summary_view = (
metadata.loc[:, available_core_fields]
.iloc[0]
.to_frame(name="value")
)
display(metadata_summary_view)
The exact set of columns can evolve with the annotation table, but their roles tend to fall into a few useful groups:
| Group | Typical fields | How to interpret them |
|---|---|---|
| Reconstruction anchors | root_id, supervoxel_id, nucleus_id | Links from the annotation record back to segmentation and nucleus-associated data. These are identifiers, not biological labels. |
| Broad identity | flow, super_class, cell_class, cell_sub_class | Higher-level organizational descriptions. A missing value means unassigned or unavailable, not necessarily negative evidence. |
| Type and morphology | cell_type, hemibrain_type, morphology_group | More specific labels. Distinct type fields can reflect different source vocabularies rather than disagreement. |
| Neurotransmitter information | top_nt, top_nt_conf, known_nt, known_nt_source | Keep the estimated top label and its confidence distinct from a known label and its cited source. |
| Anatomy and links | side, nerve, vfb_id, fbbt_id | Laterality, nerve association, and identifiers that connect FlyWire records to external vocabularies or databases. |
| Curation state | status | A field that may carry workflow or curation information when populated. Do not infer a meaning from a blank value. |
A robust interpretation is field-specific. For example:
hemibrain_typemay provide a useful legacy or cross-dataset name even whencell_typeis empty.top_nt="acetylcholine"with a hightop_nt_confis useful metadata, but it is not the same kind of evidence asknown_ntaccompanied byknown_nt_source.side="left"is a direct categorical field in the annotation record; it is not something you should infer manually from the sign or magnitude of a coordinate.
Avoid “filling in” missing labels from intuition. Your notebook should preserve the distinction between unknown, not applicable, and not present in this annotation release whenever the data source does not make that distinction explicit.
Handle locations carefully: annotation voxels versus viewer nanometers
The search_annotations API documentation states that its coordinate columns use the native nm voxel space. This differs from the coordinates in nanometers used in the location-resolution work from the prior lesson.
For an annotation-table coordinate , the physical location in nanometers is:
Do not paste pos_x, pos_y, and pos_z directly into a tool expecting nanometers.
VOXEL_SIZE_NM = {"x": 4, "y": 4, "z": 40}
def annotation_xyz_voxels_to_nm(row, prefix):
"""Convert fields such as pos_x, pos_y, pos_z into nanometers."""
columns = [f"{prefix}_{axis}" for axis in ("x", "y", "z")]
if not all(column in row.index for column in columns):
return None
raw = row[columns]
if raw.isna().any():
return None
return [
int(raw[f"{prefix}_x"]) * VOXEL_SIZE_NM["x"],
int(raw[f"{prefix}_y"]) * VOXEL_SIZE_NM["y"],
int(raw[f"{prefix}_z"]) * VOXEL_SIZE_NM["z"],
]
locations_nm = {
"pos_nm_xyz": annotation_xyz_voxels_to_nm(metadata_row, "pos"),
"soma_nm_xyz": annotation_xyz_voxels_to_nm(metadata_row, "soma"),
}
locations_nm
Use these positions appropriately:
pos_nm_xyzis a location associated with the annotation-table record.soma_nm_xyz, if present, is a soma-associated location.- Neither coordinate alone certifies that the complete morphology is correct. They are useful navigation and corroboration points.
This coordinate conversion is an easy source of subtle bugs in software pipelines. A coordinate that looks plausible in raw voxel units may land far from the intended neuron if the receiving API expects nanometers.
Retrieve community tags without treating them as a vote
Next, retrieve community annotations for the same resolved root and materialization.
community = flywire.search_community_annotations(
RESOLVED_ROOT_ID,
materialization=int(MATERIALIZATION_VERSION),
dataset=DATASET,
)
print("Community annotation rows:", len(community))
community_columns_to_show = [
column
for column in [
"tag",
"user",
"user_name",
"created",
"pt_root_id",
"root_id",
"pt_position_x",
"pt_position_y",
"pt_position_z",
]
if column in community.columns
]
display(community.loc[:, community_columns_to_show].head(20))
Unlike the hierarchical lookup, multiple community rows are expected. They may contain:
- repeated tags supplied by different users or groups;
- multiple labels for the same neuron;
- a point location associated with the annotation;
- creation information and contributor fields;
- labels using informal, historical, or project-specific terminology.
Create a compact list of the unique tag strings while retaining the complete original table for provenance.
if "tag" not in community.columns:
unique_community_tags = []
else:
unique_community_tags = sorted(
community.loc[community["tag"].notna(), "tag"]
.astype(str)
.unique()
.tolist()
)
print("Unique community tag strings:")
for tag in unique_community_tags:
print("-", tag)
Do not split a comma-separated tag string automatically and treat each fragment as an independently validated label. A tag may encode a compound statement, such as a class plus a type plus a transmitter. Preserve the original text in your saved record.
A sensible evidence hierarchy for the current task is:
- Use the resolved root ID and materialization as the identity anchor.
- Use the hierarchical annotation row as the structured profile.
- Use community tags as contributed context and leads for inspection.
- Where labels conflict, record the conflict rather than selecting the most appealing answer.
The annotation tutorial’s examples show exactly why this distinction helps: one neuron can have several community records, while the hierarchical annotation query is designed to yield one consolidated row.
Working with annotations - fafbseg 3.2.2 documentation
Read the annotation tutorial’s side-by-side examples of community and hierarchical searches. The examples make the different row cardinalities and metadata fields concrete.
In the Working with annotations tutorial, begin at the example introduced by the two annotation queries. Compare the columns in the community table with the single hierarchical row that follows. Focus on the fields shown after the hierarchical result, especially cell-class, type, neurotransmitter, side, and external-ID fields. The root ID and materialization numbers in the documentation are examples only; keep using your own pinned values.
Save a small, safe neuron-metadata record
Your next lesson will query incoming and outgoing synapses. Save a profile record now so that the synapse results remain attached to the same identity and version context.
As in the previous lesson, root and supervoxel identifiers are saved as strings. This prevents loss of precision if you later load the JSON in a JavaScript or browser-based tool.
import json
from datetime import datetime, timezone
from importlib.metadata import version as package_version
from pathlib import Path
def value_or_none(value):
"""Convert pandas missing values to JSON null; preserve labels as strings."""
if pd.isna(value):
return None
return str(value)
label_fields = [
"flow",
"super_class",
"cell_class",
"cell_sub_class",
"cell_type",
"hemibrain_type",
"morphology_group",
"top_nt",
"known_nt",
"known_nt_source",
"side",
"nerve",
"vfb_id",
"fbbt_id",
"status",
]
profile_labels = {
field: value_or_none(metadata_row[field])
for field in label_fields
if field in metadata_row.index
}
top_nt_conf = None
if "top_nt_conf" in metadata_row.index and pd.notna(metadata_row["top_nt_conf"]):
top_nt_conf = float(metadata_row["top_nt_conf"])
profile_record = {
"queried_utc": datetime.now(timezone.utc).isoformat(),
"dataset": DATASET,
"materialization_version": int(MATERIALIZATION_VERSION),
"resolved_root_id": str(RESOLVED_ROOT_ID),
"annotation_version_requested": ANNOTATION_VERSION,
"fafbseg_version": package_version("fafbseg"),
"hierarchical_annotation_rows": int(len(metadata)),
"supervoxel_id": (
value_or_none(metadata_row["supervoxel_id"])
if "supervoxel_id" in metadata_row.index
else None
),
"nucleus_id": (
value_or_none(metadata_row["nucleus_id"])
if "nucleus_id" in metadata_row.index
else None
),
"locations_nm_xyz": locations_nm,
"labels": profile_labels,
"top_nt_conf": top_nt_conf,
"community_annotation_row_count": int(len(community)),
"community_tag_strings": unique_community_tags,
}
profile_path = Path("flywire_single_neuron_metadata.json")
with profile_path.open("w", encoding="utf-8") as f:
json.dump(profile_record, f, indent=2)
print(f"Saved metadata profile to: {profile_path.resolve()}")
Before treating this as a final research artifact, add the exact annotation release tag or commit reported by fafbseg to your notebook notes or provenance file. If the initial query used ANNOTATION_VERSION = None, rerun it with a specific version once you have identified the release you intend to preserve.
A concise notebook statement for this stage might read:
At materialization , resolved root ID has the recorded hierarchical annotation fields and community tags. Structured labels are reported with their source fields; missing values and any label disagreements are preserved rather than inferred.
Key takeaways
You have now built the metadata layer of a reproducible single-neuron profile:
search_annotations()retrieves the structured hierarchical profile for a resolved root ID.- Query cardinality is evidence: one row is expected, zero rows means no retrieved structured annotation, and multiple rows require investigation.
- Segmentation materialization and annotation-table version are separate provenance dimensions.
- Annotation-table coordinates are in nm voxel units, so convert them before using tools that expect nanometers.
- Community annotations can provide valuable context, but they are multi-row contributed records rather than a single authoritative classification.
- Save identifiers as strings and preserve missing or conflicting information rather than silently normalizing it away.
Next, you will use this same resolved root ID and materialization to retrieve its incoming and outgoing synapses, turning a biological profile into an initial connectivity profile.
Can't find a good explanation? Sign up and we'll make it for you
Sign up