diff --git a/src/test/test_neuron_region_connectivity.py b/src/test/test_neuron_region_connectivity.py index 88c8b75..804fec4 100644 --- a/src/test/test_neuron_region_connectivity.py +++ b/src/test/test_neuron_region_connectivity.py @@ -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 @@ -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( @@ -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 @@ -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 diff --git a/src/vfbquery/graph_builder.py b/src/vfbquery/graph_builder.py index de70631..cc06788 100644 --- a/src/vfbquery/graph_builder.py +++ b/src/vfbquery/graph_builder.py @@ -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: diff --git a/src/vfbquery/vfb_queries.py b/src/vfbquery/vfb_queries.py index db00572..5a548d4 100644 --- a/src/vfbquery/vfb_queries.py +++ b/src/vfbquery/vfb_queries.py @@ -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 @@ -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 @@ -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) @@ -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, @@ -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, @@ -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),