Create your own
Lesson illustration

Resolving Neuron Identifiers by Data Version

Welcome back. In the previous lesson, you configured an authenticated Python environment, selected the Public FlyWire datastack, chose an available materialization version, and saved that context as provenance. You now have the essential inputs needed for a reproducible lookup:

  • a dataset and datastack;
  • a fixed MATERIALIZATION_VERSION;
  • coordinates understood as in nanometers.

This lesson makes the next distinction operational: a biological neuron may be referred to informally by a “neuron ID,” but FlyWire queries operate on root IDs, which name a particular segmentation state. You will resolve a supplied root ID to the root ID valid at your chosen materialization, assess whether that resolution is unambiguous, and save the result as a compact record for later annotation and synapse queries.

Plan for roughly 35–45 minutes, including running the notebook cells with one genuine root ID.


A root ID is a versioned reconstruction reference

A FlyWire root ID is not a permanent barcode for a neuron. It identifies a set of supervoxels that were connected in a particular version of the segmentation.

When proofreading changes the segmentation:

  • a merge makes a new root ID representing the combined object;
  • a split makes new root IDs representing the separated objects;
  • the former root IDs remain addressable as historical objects, but may no longer be valid for a later query snapshot.

This is a useful distinction for software work: the root ID is closer to an immutable versioned object reference than to a stable database primary key. The biological cell is the intended entity; the root ID is one time-dependent reconstruction of it.

A primer on the FlyWire segmentation - fafbseg 3.2.2 documentation

Read the relevant parts of the fafbseg documentation to establish why root IDs change and how timestamp-based resolution works. The code examples use an older public materialization number; treat that number as historical documentation, not as a value to copy into your notebook.

In “FlyWire root IDs - the details,” read from the root ID model, then continue through the discussion of immutable IDs and the function list. Focus on the fact that edits create new root IDs rather than modifying an existing one. Next, read “Materializations and the CAVE” from the materialization explanation. Notice that CAVE relates coordinate-based records to root IDs at a snapshot. Finally, continue in the root-ID discussion where the documentation asks how to check whether IDs match a materialization. Read from the timestamp check through the update_ids() example. Pay particular attention to the fact that update_ids() can target a specified timestamp rather than only the present segmentation.

The practical consequence is simple but important:

A root ID is suitable for a materialized-table query only if it was valid at the materialization being queried, or if you have explicitly resolved it to the root valid at that materialization.


What “resolve against a materialization” means

Suppose you have:

  • SOURCE_ROOT_ID: a root ID copied from a task, a colleague, or a FlyWire inspection session;
  • MATERIALIZATION_VERSION: the snapshot selected in the previous lesson.

Your goal is to produce a result with this meaning:

“For the Public FlyWire datastack at materialization , this source root ID corresponds to this resolved root ID, with this mapping confidence and these caveats.”

There are two related checks:

OperationQuestion answeredWhy it matters
is_latest_root(..., timestamp="mat_M")Was this exact root ID active at materialization ?Determines whether the supplied ID can already be used directly.
update_ids(..., timestamp="mat_M")Which root ID does this source root map to at materialization ?Produces the ID to use for materialized annotation and synapse queries.

The name is_latest_root can be misleading at first. With no timestamp, “latest” means current live segmentation. With timestamp="mat_M", it means current at the specified historical snapshot.

Similarly, update_ids() returns a column named new_id, but “new” means the destination of the mapping request, not necessarily a chronologically newer reconstruction. If your target is an older materialization, new_id is the ID valid at that older snapshot.

The diagram distinguishes coordinate-based annotation records from the segment-table root IDs produced during materialization: an annotation point is stored by location, and materialization associates that location with a root ID at a particular segmentation snapshot.

The diagram explains why this versioning design works. An annotation can retain a physical point location even as proofreading alters segmentation. At materialization time, CAVE resolves that point’s supervoxel into the root ID valid in that snapshot. Your notebook should follow the same discipline.


Resolve one root ID in your notebook

Continue in the notebook from the prior lesson, where you defined DATASET, DATASTACK, and MATERIALIZATION_VERSION. Do not replace the selected materialization with a hard-coded value from documentation.

First, set your source root ID. Use a root ID from a course task, a shared example, or a legitimate selection in FlyWire. Keep it as an integer in Python; wrapping the decimal text in int(...) avoids accidental conversion through floating-point notation.

from fafbseg import flywire
import pandas as pd

# Replace this with a root ID you were given or inspected.
# Keep the digits inside quotes, then convert directly to Python int.
SOURCE_ROOT_ID = int("720575940625431866")

TARGET_TIMESTAMP = f"mat_{MATERIALIZATION_VERSION}"

print("Dataset:", DATASET)
print("Datastack:", DATASTACK)
print("Target materialization:", MATERIALIZATION_VERSION)
print("Timestamp argument:", TARGET_TIMESTAMP)
print("Source root ID:", SOURCE_ROOT_ID)

Now ask whether the source ID itself was active at that materialization.

was_active_at_target = bool(
    flywire.is_latest_root(
        [SOURCE_ROOT_ID],
        timestamp=TARGET_TIMESTAMP,
    )[0]
)

print(
    f"Was source root ID active at materialization "
    f"{MATERIALIZATION_VERSION}? {was_active_at_target}"
)

Interpret the result narrowly:

  • True: the supplied root ID was valid at this materialization. It is already compatible with tables materialized at this version.
  • False: do not use the supplied ID directly in a materialized annotation or connectivity query. It may predate or postdate the snapshot, or it may represent a reconstruction state that did not exist at that snapshot.

Next, request the actual mapping.

mapping = flywire.update_ids(
    [SOURCE_ROOT_ID],
    timestamp=TARGET_TIMESTAMP,
)

display(mapping)

The result is normally a small DataFrame with columns like:

ColumnInterpretation
old_idYour supplied source root ID
new_idThe root ID to use at the requested timestamp
confidenceA score describing the mapping returned by the service
changedWhether the target root differs from the supplied root

The exact printed formatting can vary between package versions. What matters is the semantic interpretation: the new_id is your candidate resolved root ID for this materialization.

Do not omit timestamp=TARGET_TIMESTAMP. This call is valid Python without the argument, but its default behavior is to map toward the current segmentation. That would answer a different question and break the versioned analysis you set up last lesson.


Treat resolution as a decision, not merely a returned number

A returned ID is not automatically a safe one-neuron analysis target. Make the decision explicit.

Add the following cell to extract candidate mappings and classify the result without silently selecting an uncertain candidate.

required_columns = {"old_id", "new_id", "confidence", "changed"}
missing_columns = required_columns.difference(mapping.columns)

if missing_columns:
    raise RuntimeError(
        f"Unexpected update_ids result; missing columns: {sorted(missing_columns)}"
    )

candidates = mapping.loc[mapping["new_id"].notna()].copy()

def root_id_as_text(value):
    """Represent large FlyWire IDs safely in JSON and spreadsheet exports."""
    if pd.isna(value):
        return None
    return str(int(value))

candidate_records = []

for _, row in candidates.iterrows():
    candidate_records.append(
        {
            "resolved_root_id": root_id_as_text(row["new_id"]),
            "confidence": (
                None if pd.isna(row["confidence"])
                else float(row["confidence"])
            ),
            "changed": (
                None if pd.isna(row["changed"])
                else bool(row["changed"])
            ),
        }
    )

if len(candidate_records) == 0:
    resolution_decision = "unresolved_no_candidate"
elif len(candidate_records) > 1:
    resolution_decision = "unresolved_multiple_candidates"
elif candidate_records[0]["confidence"] is None:
    resolution_decision = "resolved_candidate_without_confidence"
elif candidate_records[0]["confidence"] < 1.0:
    resolution_decision = "resolved_candidate_needs_review"
else:
    resolution_decision = "resolved_unique_candidate"

print("Resolution decision:", resolution_decision)
display(pd.DataFrame(candidate_records))

This is deliberately conservative. A confidence below is not proof that the mapping is wrong; it is a signal that the source and target segmentation states do not support treating the result as an unqualified, one-to-one neuron identity.

Use these working rules for the rest of the course:

Resolution outcomeWhat to do next
resolved_unique_candidateUse the resolved root ID for queries at this materialization.
resolved_candidate_needs_reviewRecord the candidate, but obtain spatial corroboration before treating it as the same whole neuron.
unresolved_multiple_candidatesDo not choose a candidate arbitrarily. The source reconstruction may span objects that differ at the target version.
unresolved_no_candidateRecheck dataset, materialization, and the source ID. Seek an anchor location or a known root ID from the target snapshot.

This caution matters most around splits. A source root can represent a large object that later became several objects; conversely, a later root may map back to an earlier object that contained additional material. In either case, choosing a single ID without recording the mapping context can turn a reconstruction-history issue into a false biological conclusion.


Use a spatial anchor when ID lineage alone is insufficient

A root-ID mapping tells you about segmentation lineage. A known coordinate tells you which physical part of the reconstruction you mean.

If you have a trusted point on the neuron, such as a soma, nucleus-associated point, or annotation point that visibly lies within the intended neurite, resolve the location directly at the target materialization:

# Replace with a validated point inside the intended neuron.
# Coordinate order is [x, y, z] in nanometers.
ANCHOR_LOCATION_NM = [75350, 60162, 3162]

anchor_root_at_target = int(
    flywire.locs_to_segments(
        [ANCHOR_LOCATION_NM],
        timestamp=TARGET_TIMESTAMP,
    )[0]
)

print("Root at anchor location and target materialization:")
print(anchor_root_at_target)

The coordinate must be a point inside the reconstruction of interest, not merely near it in the 2D viewer. In dense neuropil, nearby pixels can belong to entirely different neurites.

Compare this anchor result with the ID-based candidate:

if len(candidate_records) == 1:
    resolved_root_id = int(candidate_records[0]["resolved_root_id"])

    if resolved_root_id == anchor_root_at_target:
        print("Anchor location corroborates the resolved root ID.")
    else:
        print(
            "Anchor does not match the ID-based candidate. "
            "Keep this case unresolved and inspect the reconstruction."
        )
else:
    print(
        "No unique ID-based candidate exists. "
        "The anchor identifies only the component containing this location."
    )

A matching anchor gives useful evidence that the resolved root contains the intended physical part of the neuron at the target materialization. It does not by itself prove the entire reconstruction is correct; that belongs to proofreading and morphology review. But it is an appropriate verification step before requesting metadata and synapses.


Save a resolution record beside your provenance

Root IDs are large integers, commonly much larger than the largest integer JavaScript can represent exactly:

Python integers preserve them exactly, but a JSON consumer written in JavaScript may silently round a numeric root ID. Since you have front-end experience, this is a particularly easy failure to prevent: serialize root IDs as decimal strings in JSON, then explicitly parse them only in environments that safely support the required integer range.

Create a compact record that links this resolution to your prior provenance file.

import json
from datetime import datetime, timezone
from pathlib import Path

resolution_record = {
    "resolved_utc": datetime.now(timezone.utc).isoformat(),
    "flywire_dataset_release": DATASET,
    "datastack": DATASTACK,
    "materialization_version": int(MATERIALIZATION_VERSION),
    "timestamp_argument": TARGET_TIMESTAMP,
    "source_root_id": str(SOURCE_ROOT_ID),
    "source_was_active_at_target": was_active_at_target,
    "candidate_mappings": candidate_records,
    "decision": resolution_decision,
    "anchor_location_nm_xyz": None,
    "anchor_root_id_at_target": None,
}

# Include these only if you ran the anchor-location check above.
if "ANCHOR_LOCATION_NM" in globals():
    resolution_record["anchor_location_nm_xyz"] = [
        int(v) for v in ANCHOR_LOCATION_NM
    ]

if "anchor_root_at_target" in globals():
    resolution_record["anchor_root_id_at_target"] = str(anchor_root_at_target)

resolution_path = Path("flywire_root_resolution.json")

with resolution_path.open("w", encoding="utf-8") as f:
    json.dump(resolution_record, f, indent=2)

print(f"Wrote resolution record to: {resolution_path.resolve()}")

For a usable result, your record should state all of the following:

  • source root ID;
  • resolved root ID or unresolved status;
  • Public dataset and datastack;
  • materialization version and mat_<version> timestamp argument;
  • mapping confidence;
  • whether a trusted coordinate corroborated the result;
  • the decision you made about whether the ID can be used downstream.

Avoid describing the result merely as “the neuron’s ID.” A better statement is:

“At materialization , the source root ID was resolved to root ID , with the recorded mapping evidence.”

That wording remains correct if a future FlyWire release changes the live reconstruction.


Key takeaways

A FlyWire root ID denotes a reconstruction state, not an eternal neuron identity. To work reproducibly with materialized annotations and synapse tables:

  1. Pin the target materialization first.
  2. Check whether the supplied root was active at that snapshot with is_latest_root(..., timestamp=...).
  3. Resolve it with update_ids(..., timestamp=...).
  4. Treat low-confidence, missing, or multiple mappings as review cases rather than forcing a choice.
  5. When available, corroborate the result with a validated in-neuron coordinate resolved at the same timestamp.
  6. Save root IDs as strings in JSON and record the full version context.

In the next lesson, you will use a resolved root ID to retrieve annotations and basic metadata, while keeping the materialization and annotation provenance explicit.

Can't find a good explanation? Sign up and we'll make it for you

Sign up