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 condition | Partner is stored in |
|---|---|---|
| Incoming synapse | post == R | pre |
| Outgoing synapse | pre == R | post |
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:
- A synapse count is not a count of unique partners. One partner can form many synapses with the selected neuron.
- 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_recordorflywire_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:
| Column | Meaning for this lesson |
|---|---|
pre | Root ID of the presynaptic neuron |
post | Root ID of the postsynaptic neuron |
cleft_score | A score associated with the detected synapse/cleft |
pre_x, pre_y, pre_z | Location fields for the presynaptic side |
post_x, post_y, post_z | Location fields for the postsynaptic side |
id | Synapse 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:
directionis"incoming";postequalsTARGET_ROOT_ID;partner_root_idequalspre.
For an outgoing row, confirm the converse:
directionis"outgoing";preequalsTARGET_ROOT_ID;partner_root_idequalspost.
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 result | Possible interpretation | Appropriate next step |
|---|---|---|
| Many incoming and outgoing rows | Normal connectivity result | Preserve it with materialization provenance. |
| Rows only in one direction | Possible biological asymmetry or a limited reconstruction | Keep the result; later inspect morphology and annotations before drawing conclusions. |
| Zero rows | A genuinely unconnected result is possible, but so are version, root-ID, access, or filtering issues | Recheck the resolved ID and materialization before reporting zero connectivity. |
| Warning about an outdated root ID | The root did not represent the same segmentation object at the requested snapshot | Resolve or update the ID for that materialization, then rerun. |
| Unexpectedly enormous result | The target may be a very large object, a merged reconstruction, or an incorrect ID | Return 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 bypre == 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 ispre. - A row is outgoing when the target appears in
pre; its partner ispost. - 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