Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions src/test/test_neuron_region_connectivity.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def test_row_has_expected_keys(self):
)
assert result["rows"], "Expected at least one row"
row = result["rows"][0]
expected_keys = {"id", "region", "presynaptic_terminals", "postsynaptic_terminals", "tags"}
expected_keys = {"id", "region", "presynaptic_terminals", "downstream_synapses", "postsynaptic_terminals", "tags"}
assert expected_keys.issubset(row.keys())

@pytest.mark.integration
Expand All @@ -47,8 +47,26 @@ def test_headers_present(self):
assert "headers" in result
assert "region" in result["headers"]
assert "presynaptic_terminals" in result["headers"]
assert "downstream_synapses" in result["headers"]
assert "postsynaptic_terminals" in result["headers"]

@pytest.mark.integration
def test_count_columns_populated(self):
"""Regression: synapse-count columns must not be all-None.

The Cypher previously read edge keys that never existed (`pre`/`post`
instead of the real `Tbars`/`downstream`/`upstream`), so every count
came back None. This neuron carries T-bar counts, so all three columns
must yield at least one real integer.
"""
result = get_neuron_region_connectivity(TEST_NEURON, return_dataframe=False)
rows = result["rows"]
assert rows, "Expected at least one row"
for col in ("presynaptic_terminals", "downstream_synapses", "postsynaptic_terminals"):
populated = [r[col] for r in rows if r.get(col) is not None]
assert populated, f"{col} was None for every row (edge-key regression)"
assert all(isinstance(v, int) for v in populated), f"{col} should be integers"

@pytest.mark.integration
def test_limit_respected(self):
result = get_neuron_region_connectivity(
Expand All @@ -74,7 +92,7 @@ def test_dataframe_has_expected_columns(self):
df = get_neuron_region_connectivity(
TEST_NEURON, return_dataframe=True, limit=1
)
expected_cols = {"id", "region", "presynaptic_terminals", "postsynaptic_terminals", "tags"}
expected_cols = {"id", "region", "presynaptic_terminals", "downstream_synapses", "postsynaptic_terminals", "tags"}
assert expected_cols.issubset(set(df.columns))

@pytest.mark.integration
Expand All @@ -96,4 +114,19 @@ def test_schema_generation(self):
assert schema.preview == 5
assert "region" in schema.preview_columns
assert "presynaptic_terminals" in schema.preview_columns
assert "downstream_synapses" in schema.preview_columns
assert "postsynaptic_terminals" in schema.preview_columns


class TestNeuronRegionConnectivityAttachment:
@pytest.mark.integration
def test_query_attached_to_term_info(self):
"""Regression: the query must be offered on a neuron's term-info page.

NeuronRegionConnectivityQuery_to_schema existed and worked, but was
never called from get_term_info, so the query never appeared on any
term. The gate is has_region_connectivity in the neuron's SuperTypes.
"""
term_info = get_term_info(TEST_NEURON)
query_names = {q.get("query") for q in term_info.get("Queries", [])}
assert "NeuronRegionConnectivityQuery" in query_names
5 changes: 4 additions & 1 deletion src/vfbquery/graph_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,10 @@ def graph_from_neuron_region(rows, primary_id, primary_label=None):
"group": assign_group(tags_for_group, info.get("label", "")),
})

pre = r.get("presynaptic_terminals", 0) or 0
# Fall back to downstream synapse count when T-bar counts are absent
# (some datasets still being loaded have no Tbars), so the edge weight
# still reflects presynaptic output rather than collapsing to 0.
pre = (r.get("presynaptic_terminals") or r.get("downstream_synapses") or 0)
post = r.get("postsynaptic_terminals", 0) or 0
weight = pre + post
if weight > 0:
Expand Down
43 changes: 28 additions & 15 deletions src/vfbquery/vfb_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -1109,7 +1109,14 @@ def term_info_parse_object(results, short_form):
if contains_all_tags(termInfo["SuperTypes"], ["Individual", "Neuron", "has_neuron_connectivity"]):
q = NeuronNeuronConnectivityQuery_to_schema(termInfo["Name"], {"short_form": vfbTerm.term.core.short_form})
queries.append(q)


# NeuronRegionConnectivity query - synaptic terminal counts per brain
# region. Gated on has_region_connectivity, mirroring the
# NeuronNeuronConnectivity block above.
if contains_all_tags(termInfo["SuperTypes"], ["Individual", "Neuron", "has_region_connectivity"]):
q = NeuronRegionConnectivityQuery_to_schema(termInfo["Name"], {"short_form": vfbTerm.term.core.short_form})
queries.append(q)

# NeuronsPartHere query - for anatomical regions (neuropils, ganglia, etc.)
# Gate: Class + (Synaptic_neuropil OR Anatomy), but NOT Cell.
# Excluded for cell classes (neurons, glia, neuroblasts): "neurons with some
Expand Down Expand Up @@ -1953,14 +1960,16 @@ def NeuronNeuronConnectivityQuery_to_schema(name, take_default):
"""
Schema for neuron_neuron_connectivity_query.
Finds neurons connected to the specified neuron.
Matching criteria from XMI: Connected_neuron
Matching criteria: Individual + Neuron + has_neuron_connectivity
(the XMI's legacy "Connected_neuron" facet; the SOLR/pdb label is
has_neuron_connectivity).
Query chain: Neo4j compound query → process
"""
query = "NeuronNeuronConnectivityQuery"
label = f"Neurons connected to {name}"
function = "get_neuron_neuron_connectivity"
takes = {
"short_form": {"$and": ["Individual", "Connected_neuron"]},
"short_form": {"$and": ["Individual", "has_neuron_connectivity"]},
"default": take_default,
}
preview = 5
Expand All @@ -1972,18 +1981,20 @@ def NeuronRegionConnectivityQuery_to_schema(name, take_default):
"""
Schema for neuron_region_connectivity_query.
Shows connectivity to regions from a specified neuron.
Matching criteria from XMI: Region_connectivity
Matching criteria: Individual + Neuron + has_region_connectivity
(the XMI's legacy "Region_connectivity" facet; the SOLR/pdb label is
has_region_connectivity).
Query chain: Neo4j compound query → process
"""
query = "NeuronRegionConnectivityQuery"
label = f"Connectivity per region for {name}"
function = "get_neuron_region_connectivity"
takes = {
"short_form": {"$and": ["Individual", "Region_connectivity"]},
"short_form": {"$and": ["Individual", "has_region_connectivity"]},
"default": take_default,
}
preview = 5
preview_columns = ["id", "region", "presynaptic_terminals", "postsynaptic_terminals", "tags"]
preview_columns = ["id", "region", "presynaptic_terminals", "downstream_synapses", "postsynaptic_terminals", "tags"]
return Query(query=query, label=label, function=function, takes=takes, preview=preview, preview_columns=preview_columns)


Expand Down Expand Up @@ -3961,7 +3972,7 @@ def get_neuron_neuron_connectivity(short_form: str, return_dataframe=True, limit

This implements the neuron_neuron_connectivity_query from the VFB XMI specification.
Query chain (from XMI): Neo4j compound query → process
Matching criteria: Individual + Connected_neuron
Matching criteria: Individual + Neuron + has_neuron_connectivity

Uses synapsed_to relationships to find partner neurons.
Returns inputs (upstream) and outputs (downstream) connection information,
Expand Down Expand Up @@ -4128,8 +4139,9 @@ def get_neuron_region_connectivity(short_form: str, return_dataframe=True, limit
target.short_form AS id,
apoc.text.format("[%s](%s)", [target.label, target.short_form]) AS region,
type,
synapse_counts.`pre` AS presynaptic_terminals,
synapse_counts.`post` AS postsynaptic_terminals,
toInteger(synapse_counts.`Tbars`[0]) AS presynaptic_terminals,
toInteger(synapse_counts.`downstream`[0]) AS downstream_synapses,
toInteger(synapse_counts.`upstream`[0]) AS postsynaptic_terminals,
apoc.text.join(coalesce(target.uniqueFacets, []), '|') AS tags,
REPLACE(apoc.text.format("[%s](%s)", [CASE WHEN template_anat.symbol[0] <> '' THEN template_anat.symbol[0] ELSE template_anat.label END, template_anat.short_form]), '[null](null)', '') AS template,
coalesce(technique.label, '') AS technique,
Expand All @@ -4150,15 +4162,16 @@ def get_neuron_region_connectivity(short_form: str, return_dataframe=True, limit
'id': {'title': 'Region ID', 'type': 'selection_id', 'order': -1},
'region': {'title': 'Brain Region', 'type': 'markdown', 'order': 0},
'type': {'title': 'Type', 'type': 'text', 'order': 1},
'presynaptic_terminals': {'title': 'Presynaptic Terminals', 'type': 'number', 'order': 2},
'postsynaptic_terminals': {'title': 'Postsynaptic Terminals','type': 'number', 'order': 3},
'template': {'title': 'Template', 'type': 'markdown', 'order': 4},
'technique': {'title': 'Imaging Technique', 'type': 'text', 'order': 5},
'tags': {'title': 'Tags', 'type': 'tags', 'order': 6},
'presynaptic_terminals': {'title': 'Presynaptic Terminals (T-bars)', 'type': 'number', 'order': 2},
'downstream_synapses': {'title': 'Downstream Synapses', 'type': 'number', 'order': 3},
'postsynaptic_terminals': {'title': 'Postsynaptic Terminals','type': 'number', 'order': 4},
'template': {'title': 'Template', 'type': 'markdown', 'order': 5},
'technique': {'title': 'Imaging Technique', 'type': 'text', 'order': 6},
'tags': {'title': 'Tags', 'type': 'tags', 'order': 7},
'thumbnail': {'title': 'Thumbnail', 'type': 'markdown', 'order': 9},
},
'rows': [
{k: row.get(k) for k in ['id', 'region', 'type', 'presynaptic_terminals', 'postsynaptic_terminals', 'template', 'technique', 'tags', 'thumbnail']}
{k: row.get(k) for k in ['id', 'region', 'type', 'presynaptic_terminals', 'downstream_synapses', 'postsynaptic_terminals', 'template', 'technique', 'tags', 'thumbnail']}
for row in rows
],
'count': len(rows),
Expand Down
Loading