Create your own
Lesson illustration

Querying a Neuron’s Incoming and Outgoing Synapses

Hello again. In the previous lesson, you treated a FlyWire neuron as a versioned object: a resolved root ID at one specific materialization, accompanied by structured metadata and community labels. That identity record is now the contract for this lesson. Do not substitute a newer “live” ID midway through the notebook.

This lesson retrieves the individual synapses connected to that neuron and separates them into incoming and outgoing sets. By the end, you will have a saved raw synapse table, clear direction labels, and enough provenance to summarize partners in the next lesson. Plan for roughly 40 minutes.


A synapse query is a directional table lookup

A FlyWire synapse table contains one row per detected synapse. For a selected neuron with root ID :

Relationship to Row conditionPartner is stored in
Incoming synapsepost == Rpre
Outgoing synapsepre == Rpost

The terms are directional:

  • The presynaptic neuron is the sender at that synapse.
  • The postsynaptic neuron is the receiver.
  • The neuron you selected may be presynaptic in some rows and postsynaptic in others.

This is easiest to view as a directed database edge. A synapse row records a relationship from its pre root ID to its post root ID; the selected neuron is either the source or the target of that row.

flywire.get_synapses() retrieves both categories when given one root ID. Your notebook’s job is then to classify the returned rows explicitly, rather than assuming the first column represents a partner in one fixed direction.

Two cautions matter from the outset:

  1. A synapse count is not a count of unique partners. One partner can form many synapses with the selected neuron.
  2. A synapse table is evidence from a specific connectome snapshot. The association of a synapse with a root ID depends on the segmentation and materialization version. It is not an immutable biological fact independent of reconstruction state.

Read the API behavior before querying

The fafbseg connectivity tutorial shows the raw synapse schema and, crucially, the consequences of querying an ID at the wrong materialization. Its example uses a historical public materialization version; use it to understand the workflow, not as a version to hard-code into your notebook.

Fetching connectivity - fafbseg 3.2.2 documentation

Read the “Fetching connectivity” tutorial in the fafbseg documentation. It introduces flywire.get_synapses() and shows why materialization-aware root IDs are essential for a meaningful result.

In the Synapses section, read from the first get_synapses example through the materialization and root-ID update examples, stopping before “Neurons & synapses.” Inspect the returned columns—especially pre, post, cleft_score, coordinate columns, and id. Focus closely on the outdated ID warning: an empty or inaccurate result can be caused by an ID/version mismatch, not merely by a neuron having no synapses.

The practical rule is simple: use the RESOLVED_ROOT_ID and MATERIALIZATION_VERSION you established earlier as a pair.


Fetch the raw synapse rows with an explicit snapshot

Begin from the previous notebook state. You should already have:

  • DATASET, typically "public" for the public release;
  • MATERIALIZATION_VERSION, recorded as an integer;
  • RESOLVED_ROOT_ID, resolved against that materialization;
  • profile_record or flywire_single_neuron_metadata.json, saved in the metadata lesson.

Use an explicit materialization. During exploration, materialization="auto" can be convenient, but your goal here is a reproducible single-neuron profile.

from fafbseg import flywire
import pandas as pd

TARGET_ROOT_ID = int(RESOLVED_ROOT_ID)
MAT_VERSION = int(MATERIALIZATION_VERSION)

synapses = flywire.get_synapses(
    TARGET_ROOT_ID,
    materialization=MAT_VERSION,
    dataset=DATASET,
)

print("Dataset:", DATASET)
print("Materialization:", MAT_VERSION)
print("Target root ID:", TARGET_ROOT_ID)
print("Raw synapse rows returned:", len(synapses))

display(synapses.head())

A typical result contains columns resembling:

ColumnMeaning for this lesson
preRoot ID of the presynaptic neuron
postRoot ID of the postsynaptic neuron
cleft_scoreA score associated with the detected synapse/cleft
pre_x, pre_y, pre_zLocation fields for the presynaptic side
post_x, post_y, post_zLocation fields for the postsynaptic side
idSynapse record identifier

Avoid interpreting cleft_score as a direct biological strength or as proof that the associated reconstruction is correct. It is useful query metadata, but later biological conclusions still need to account for synapse detection and segmentation uncertainty.

Before dividing the result by direction, verify its schema and its relationship to the requested root ID.

required_columns = {"pre", "post", "id"}
missing_columns = required_columns.difference(synapses.columns)

if missing_columns:
    raise RuntimeError(
        "Unexpected synapse-table schema. Missing columns: "
        f"{sorted(missing_columns)}"
    )

unexpected_rows = synapses.loc[
    (synapses["pre"] != TARGET_ROOT_ID)
    & (synapses["post"] != TARGET_ROOT_ID)
]

if not unexpected_rows.empty:
    raise RuntimeError(
        "Some returned rows do not contain the requested target root ID. "
        "Stop and inspect dataset, materialization, and query settings."
    )

print("Schema and target-ID membership checks passed.")

This check may seem redundant for a library call, but it is a useful habit when you later concatenate, filter, or cache tables. A pipeline should make its directional assumptions inspectable.


Separate incoming from outgoing synapses

Now apply the definitions from the first table.

incoming_synapses = (
    synapses.loc[synapses["post"] == TARGET_ROOT_ID]
    .copy()
    .assign(
        direction="incoming",
        partner_root_id=lambda frame: frame["pre"],
    )
)

outgoing_synapses = (
    synapses.loc[synapses["pre"] == TARGET_ROOT_ID]
    .copy()
    .assign(
        direction="outgoing",
        partner_root_id=lambda frame: frame["post"],
    )
)

print("Incoming synapse rows:", len(incoming_synapses))
print("Outgoing synapse rows:", len(outgoing_synapses))

The partner_root_id column is deliberately derived differently in the two tables:

  • For an incoming row, the partner sent to your neuron, so the partner is pre.
  • For an outgoing row, your neuron sent to the partner, so the partner is post.

Create one direction-labelled table for inspection and for the next lesson.

directional_synapses = pd.concat(
    [incoming_synapses, outgoing_synapses],
    ignore_index=True,
)

preview_columns = [
    column
    for column in [
        "direction",
        "id",
        "partner_root_id",
        "pre",
        "post",
        "cleft_score",
        "pre_x",
        "pre_y",
        "pre_z",
        "post_x",
        "post_y",
        "post_z",
    ]
    if column in directional_synapses.columns
]

display(directional_synapses.loc[:, preview_columns].head(20))

At this stage, inspect several rows manually. For an incoming row, confirm that:

  • direction is "incoming";
  • post equals TARGET_ROOT_ID;
  • partner_root_id equals pre.

For an outgoing row, confirm the converse:

  • direction is "outgoing";
  • pre equals TARGET_ROOT_ID;
  • partner_root_id equals post.

This is a small check with large consequences. If the direction is reversed here, every later partner ranking will be reversed too.

Edge case: a self-connection

If the selected neuron appears in both pre and post for the same row, that row represents a self-connection in the table. It legitimately qualifies as both incoming and outgoing. The concatenated directional_synapses table will contain it once in each direction, which is appropriate for directional summaries. Keep the unmodified synapses table as the authoritative raw query result.


Interpret empty and surprising results carefully

A non-empty table is not automatically trustworthy, and an empty table is not automatically a biological finding.

Observed resultPossible interpretationAppropriate next step
Many incoming and outgoing rowsNormal connectivity resultPreserve it with materialization provenance.
Rows only in one directionPossible biological asymmetry or a limited reconstructionKeep the result; later inspect morphology and annotations before drawing conclusions.
Zero rowsA genuinely unconnected result is possible, but so are version, root-ID, access, or filtering issuesRecheck the resolved ID and materialization before reporting zero connectivity.
Warning about an outdated root IDThe root did not represent the same segmentation object at the requested snapshotResolve or update the ID for that materialization, then rerun.
Unexpectedly enormous resultThe target may be a very large object, a merged reconstruction, or an incorrect IDReturn to the viewer and metadata profile before analysis.

The CAVE materialization documentation explains the underlying database logic. fafbseg.get_synapses() gives you a convenient high-level API, while CAVE’s filter_equal_dict makes the relational condition explicit: post_pt_root_id selects incoming synapses, and the analogous presynaptic field selects outgoing ones.

Materialization — CAVEclient 1.0 documentation

Read this CAVEclient documentation section as a lower-level reference for the materialized synapse table. It is useful when a future project needs custom columns or spatially constrained queries rather than the convenient fafbseg wrapper.

In “Querying tables,” read the explanation beginning with the filtering model, then read the immediately following example that filters on post_pt_root_id. Notice that a synapse table can be extremely large, so the intended pattern is a bounded query for one neuron or cohort—not downloading the entire table.

For this course, keep flywire.get_synapses() as your default. Move to direct CAVE queries only when your analysis genuinely needs more control over selected columns, bounding boxes, or custom table behavior.


Preserve the raw evidence and query provenance

Save the raw table before any aggregation. The next lesson will use pandas to count synapses per partner, but the raw synapse records allow you to revisit a surprising partner, score, or location later.

CSV files can be opened by tools that silently convert large integers to floating-point values. Convert identifier columns to strings before export so root IDs and synapse IDs remain exact.

from datetime import datetime, timezone
from importlib.metadata import version as package_version
from pathlib import Path
import json

raw_synapses_for_export = synapses.copy()

identifier_columns = [
    column
    for column in ["id", "pre", "post"]
    if column in raw_synapses_for_export.columns
]

for column in identifier_columns:
    raw_synapses_for_export[column] = raw_synapses_for_export[column].map(str)

raw_synapse_path = Path("flywire_single_neuron_raw_synapses.csv")
raw_synapses_for_export.to_csv(raw_synapse_path, index=False)

synapse_query_record = {
    "queried_utc": datetime.now(timezone.utc).isoformat(),
    "dataset": DATASET,
    "materialization_version": MAT_VERSION,
    "target_root_id": str(TARGET_ROOT_ID),
    "fafbseg_version": package_version("fafbseg"),
    "query_function": "flywire.get_synapses",
    "query_parameters": {
        "materialization": MAT_VERSION,
        "dataset": DATASET,
        "filtered": "library default; not explicitly set",
    },
    "raw_synapse_row_count": int(len(synapses)),
    "incoming_synapse_row_count": int(len(incoming_synapses)),
    "outgoing_synapse_row_count": int(len(outgoing_synapses)),
    "raw_synapse_csv": str(raw_synapse_path),
}

provenance_path = Path("flywire_single_neuron_synapse_query.json")

with provenance_path.open("w", encoding="utf-8") as file:
    json.dump(synapse_query_record, file, indent=2)

print(f"Saved raw synapses to: {raw_synapse_path.resolve()}")
print(f"Saved query provenance to: {provenance_path.resolve()}")

A concise notebook note for this stage could read:

At materialization , root ID returned incoming and outgoing synapse rows. Incoming rows were identified by post == R; outgoing rows by pre == R. Raw records and exact query context were exported before partner-level aggregation.

That wording makes the direction rule, identity, and scope of the claim auditable.


Key takeaways

You can now retrieve a versioned neuron’s synaptic evidence rather than merely its labels:

  • flywire.get_synapses() returns individual synapse rows involving the requested root ID.
  • A row is incoming when the target appears in post; its partner is pre.
  • A row is outgoing when the target appears in pre; its partner is post.
  • Materialization version and root ID must remain paired; a mismatch can create empty or inaccurate results.
  • Save the raw table before summarizing it, and preserve large identifiers as strings when exporting.
  • Synapse counts are table-derived connectivity evidence, not automatically validated biological conclusions.

Next, you will use pandas to summarize these raw synapse rows, distinguish counts from unique partners, and rank the neuron’s major incoming and outgoing connectivity partners.

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

Sign up