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
40 changes: 39 additions & 1 deletion clients/vfbquery-client/src/vfbquery_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def _raise_server_warnings(path: str, payload) -> None:
#: uses ``connections``. Without the second name its envelope falls through to
#: the "a dict is one row" branch below and the caller gets a 1x3 frame of
#: nested lists instead of the connectivity table.
_ROW_KEYS = ("rows", "connections")
_ROW_KEYS = ("rows", "connections", "neurotransmitters")

@classmethod
def _to_df(cls, payload) -> pd.DataFrame:
Expand Down Expand Up @@ -321,6 +321,44 @@ def get_neuron_connectivity(self, neuron_id: str) -> pd.DataFrame:
return self._to_df(self._get("run_query", id=neuron_id,
query_type="NeuronNeuronConnectivityQuery"))

def get_predicted_neurotransmitters(self, neuron_type: str,
aggregate: bool = True,
split_by_dataset: bool = False,
exclude_dbs: Optional[Iterable[str]] = None,
min_confidence: float = 0.0,
) -> pd.DataFrame:
"""Predicted neurotransmitter(s) for a type (GET /get_predicted_neurotransmitters).

**A type includes its subclasses**, as on
:meth:`get_connected_neurons_by_type`. Predictions come from per-instance
``capable_of`` edges carrying a confidence.

``aggregate`` (default) returns flat per-class rows with ``instances``,
``percent_of_class`` and ``mean_confidence``; ``aggregate=False`` returns
one row per neuron. ``split_by_dataset`` (aggregate only) adds a
``dataset`` column and one row per dataset. The neurotransmitter is a GO
secretion term (``nt_id``/``nt_label``). ``exclude_dbs`` behaves as on
:meth:`get_connected_neurons_by_type` (``[]`` for all datasets).
"""
dbs = None if exclude_dbs is None else ",".join(exclude_dbs)
return self._to_df(self._get("get_predicted_neurotransmitters",
neuron_type=neuron_type,
aggregate=str(aggregate).lower(),
split_by_dataset=str(split_by_dataset).lower(),
exclude_dbs=dbs,
min_confidence=min_confidence))

def get_known_neurotransmitters(self, neuron_type: str) -> pd.DataFrame:
"""Known (curated) neurotransmitter(s) for a type and its subclasses
(GET /get_known_neurotransmitters).

Ontology classification rather than per-instance prediction, so no
confidence. One row per ``(cell_type, nt)``; the neurotransmitter is a GO
secretion term. Empty when the ontology asserts none.
"""
return self._to_df(self._get("get_known_neurotransmitters",
neuron_type=neuron_type))

# ---- similarity ------------------------------------------------------
def get_similar_neurons(self, neuron_id: str) -> pd.DataFrame:
"""NBLAST morphological matches (run_query SimilarMorphologyTo)."""
Expand Down
40 changes: 40 additions & 0 deletions docs/http-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Call the service host directly when you need to see the current answer rather th
| `/get_term_info` | Everything VFB holds about one term: name, synonyms, definition, relationships, images, xrefs, NT predictions, publications. |
| `/run_query` | Any of the ~40 named query types — instances, subclasses, parts, connectivity, NBLAST, expression, single-cell. The workhorse. |
| `/query_connectivity` | Connectivity between two *types*, aggregated, across connectome datasets. |
| `/get_predicted_neurotransmitters` | Predicted neurotransmitter(s) for a type, per instance or aggregated (with mean confidence), optionally split by dataset. |
| `/get_known_neurotransmitters` | Known (curated) neurotransmitter(s) for a type and its subclasses, from ontology classification. |
| `/search` | Free-text search over the ontology, ranked the way the website ranks it. |
| `/xref` | VFB id ↔ external accession, both directions. |
| `/facets` | Every type name `/search`'s type filters accept, with term counts. |
Expand Down Expand Up @@ -197,6 +199,44 @@ The cost of this default is that a plain query does not reproduce a published he
do that, name the dataset you want by excluding the others, or pass `exclude_dbs=` to get everything
and deduplicate yourself. `/list_connectome_datasets` gives the symbols.

## `/get_predicted_neurotransmitters`

```
GET /get_predicted_neurotransmitters?neuron_type=Tm9
```

Predicted neurotransmitter(s) for a neuron type — itself or any subclass — from per-instance
prediction edges (an asserted `capable_of` to a GO secretion term, carrying a confidence). Only
neurons the pipeline could predict contribute; those it could not (e.g. too few presynapses) are
absent.

| Parameter | |
|---|---|
| `neuron_type` | **Required.** Neuron type label, synonym or FBbt id. Means itself *and its subclasses*, as on `/query_connectivity`. |
| `aggregate` | `true` (default) returns flat per-class rows `{cell_type_id, cell_type, nt_id, nt_label, instances, percent_of_class, mean_confidence}`; `false` returns per-instance rows `{cell_type_id, cell_type, neuron_id, neuron_name, nt_id, nt_label, confidence, references, dataset}`. |
| `split_by_dataset` | `true` (aggregate only) emits one row per `(cell_type, nt, dataset)` and adds a `dataset` column, so agreement across connectomes is visible. |
| `exclude_dbs` | As on `/query_connectivity`; defaults to `hb,fafb`. Echoed back as `excluded_dbs`. |
| `min_confidence` | Drop predictions below this confidence (0..1). |
| `force_refresh` | Bypass the cache. |

The neurotransmitter is reported as its **GO secretion term** (`nt_id`, e.g. `GO_0014055`
"acetylcholine secretion, neurotransmission") — the same id space as `/get_known_neurotransmitters`.
Because the pipeline assigns a single neurotransmitter per neuron, `percent_of_class` sums to ~100%
across the neurotransmitters of a cell type.

## `/get_known_neurotransmitters`

```
GET /get_known_neurotransmitters?neuron_type=Tm9
```

Known (curated) neurotransmitter(s) for a neuron type and its subclasses, from the ontology's
classification rather than per-instance predictions — so **no confidence**. One row per
`(cell_type, nt)`: `{cell_type_id, cell_type, nt_id, nt_label}`, with `nt_id` a GO secretion term.
Empty when the ontology asserts no neurotransmitter for the type. Read from the materialised
`SUBCLASSOF` + `capable_of` structure (a neuron class links to a neurotransmitter-type class that
carries a `capable_of` edge to the GO term), which is fast and independent of live reasoning.

## `/get_hierarchy`

```
Expand Down
111 changes: 110 additions & 1 deletion src/test/test_vfb_connectivity.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
"""
import pytest

from vfbquery.vfb_connectivity import list_connectome_datasets, query_connectivity
from vfbquery.vfb_connectivity import (
list_connectome_datasets, query_connectivity,
get_predicted_neurotransmitters, get_known_neurotransmitters,
)

#: A small, stable pair: 8 and 30 connectivity individuals respectively, one
#: class each, no subclasses. Cheap enough to query several times — the two
Expand Down Expand Up @@ -335,3 +338,109 @@ def test_nonexistent_type_returns_warning(self):
def test_no_types_raises_error(self):
with pytest.raises(ValueError, match="At least one"):
query_connectivity()


# ---------------------------------------------------------------------------
# Neurotransmitter queries
#
# Tm9 is the stable fixture: a well-characterised, uncontroversially
# cholinergic optic-lobe type (FBbt_00003797) with two subtypes (Tm9a/Tm9b),
# so it exercises both the aggregation and the subclass behaviour while staying
# small — the aggregate is a couple of rows and the known-NT answer a handful.
# ---------------------------------------------------------------------------
NT_TYPE = "FBbt_00003797" # transmedullary neuron Tm9
NT_TYPE_LABEL = "transmedullary neuron Tm9"
ACH = "GO_0014055" # acetylcholine secretion, neurotransmission


@pytest.fixture(scope="module")
def predicted_tm9():
return get_predicted_neurotransmitters(NT_TYPE)


@pytest.fixture(scope="module")
def known_tm9():
return get_known_neurotransmitters(NT_TYPE)


class TestPredictedNeurotransmitters:
@pytest.mark.integration
def test_tm9_is_predominantly_cholinergic(self, predicted_tm9):
rows = predicted_tm9["neurotransmitters"]
assert rows, "expected at least one predicted NT for Tm9"
# Rows are sorted per cell type by descending instances, so the first
# row for Tm9 is its dominant prediction.
top = max(rows, key=lambda r: r["instances"])
assert top["nt_id"] == ACH
assert top["percent_of_class"] >= 90

@pytest.mark.integration
def test_aggregate_row_shape(self, predicted_tm9):
for r in predicted_tm9["neurotransmitters"]:
assert r["nt_id"].startswith("GO_")
assert r["nt_label"]
assert isinstance(r["instances"], int) and r["instances"] > 0
assert 0 <= r["percent_of_class"] <= 100
assert r["mean_confidence"] is None or 0.0 <= r["mean_confidence"] <= 1.0
# aggregate (unsplit) rows carry no dataset column
assert "dataset" not in r

@pytest.mark.integration
def test_per_instance_shape(self):
result = get_predicted_neurotransmitters(NT_TYPE, aggregate=False)
rows = result["neurotransmitters"]
assert rows
r = rows[0]
assert r["neuron_id"] and r["nt_id"].startswith("GO_")
assert r["confidence"] is None or 0.0 <= r["confidence"] <= 1.0
assert "dataset" in r

@pytest.mark.integration
def test_split_by_dataset_adds_dataset_column(self):
result = get_predicted_neurotransmitters(NT_TYPE, split_by_dataset=True)
rows = result["neurotransmitters"]
assert rows
assert all("dataset" in r for r in rows)

@pytest.mark.integration
def test_min_confidence_filters(self, predicted_tm9):
strict = get_predicted_neurotransmitters(NT_TYPE, aggregate=False,
min_confidence=0.99)
default = get_predicted_neurotransmitters(NT_TYPE, aggregate=False)
assert strict["count"] <= default["count"]
assert all(r["confidence"] is None or r["confidence"] >= 0.99
for r in strict["neurotransmitters"])

@pytest.mark.integration
def test_nonexistent_type_returns_warning(self):
result = get_predicted_neurotransmitters(
"xyzzy_nonexistent_neuron_type_99999")
assert result["count"] == 0
assert len(result["warnings"]) > 0


class TestKnownNeurotransmitters:
@pytest.mark.integration
def test_tm9_known_cholinergic(self, known_tm9):
pairs = {(r["cell_type_id"], r["nt_id"])
for r in known_tm9["neurotransmitters"]}
assert (NT_TYPE, ACH) in pairs

@pytest.mark.integration
def test_includes_subclasses(self, known_tm9):
# Tm9a / Tm9b are subtypes of Tm9 and should appear as their own rows.
cell_types = {r["cell_type_id"] for r in known_tm9["neurotransmitters"]}
assert len(cell_types) > 1

@pytest.mark.integration
def test_row_shape(self, known_tm9):
for r in known_tm9["neurotransmitters"]:
assert set(r) == {"cell_type_id", "cell_type", "nt_id", "nt_label"}
assert r["nt_id"].startswith("GO_")

@pytest.mark.integration
def test_nonexistent_type_returns_warning(self):
result = get_known_neurotransmitters(
"xyzzy_nonexistent_neuron_type_99999")
assert result["count"] == 0
assert len(result["warnings"]) > 0
4 changes: 3 additions & 1 deletion src/vfbquery/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
from .solr_result_cache import get_solr_cache
from .flybase_stocks import resolve_entity, find_stocks
from .flybase_combo_pubs import resolve_combination, find_combo_publications
from .vfb_connectivity import list_connectome_datasets, query_connectivity
from .vfb_connectivity import (list_connectome_datasets, query_connectivity,
get_predicted_neurotransmitters,
get_known_neurotransmitters)
from .graph_builder import build_graph, batch_lookup_ids
from .catmaid_client import (catmaid, CatmaidInstance, list_catmaid_instances,
list_catmaid_commands, run_catmaid_command)
Expand Down
43 changes: 43 additions & 0 deletions src/vfbquery/api_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,49 @@
FORCE_REFRESH_PARAM,
],
},
{
"path": "/get_predicted_neurotransmitters",
"summary": "Predicted neurotransmitter(s) for a neuron type",
"description": (
"Predicted neurotransmitters for a neuron type (or any "
"subclass), from per-instance prediction edges. Aggregated "
"to flat per-class rows by default; optionally split by "
"dataset. Reported as GO secretion terms with mean "
"confidence."),
"params": [
{"name": "neuron_type", "required": True,
"doc": "Neuron type (label, synonym or FBbt id)",
"example": "Tm9"},
{"name": "aggregate",
"doc": "false returns per-instance rows (default true "
"aggregates to the class)", "example": ""},
{"name": "split_by_dataset",
"doc": "true adds a dataset column and one row per dataset",
"example": ""},
{"name": "exclude_dbs",
"doc": "Datasets to leave out (comma-separated)",
"example": ""},
{"name": "min_confidence",
"doc": "Drop predictions below this confidence (0..1)",
"example": ""},
FORCE_REFRESH_PARAM,
],
},
{
"path": "/get_known_neurotransmitters",
"summary": "Known (curated) neurotransmitter(s) for a neuron type",
"description": (
"Curated neurotransmitters for a neuron type and its "
"subclasses, from ontology classification (no confidence). "
"One row per (cell type, GO secretion term); empty when the "
"ontology asserts none."),
"params": [
{"name": "neuron_type", "required": True,
"doc": "Neuron type (label, synonym or FBbt id)",
"example": "Tm9"},
FORCE_REFRESH_PARAM,
],
},
],
},
{
Expand Down
Loading
Loading