Skip to content

Commit ca94f17

Browse files
committed
show subclass results in class connectivity queries
1 parent 1ec1b74 commit ca94f17

3 files changed

Lines changed: 398 additions & 138 deletions

File tree

src/test/test_downstream_class_connectivity.py

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ def test_row_has_expected_keys(self):
3939
assert result["rows"], "Expected at least one row"
4040
row = result["rows"][0]
4141
expected_keys = {
42-
"id", "downstream_class", "total_n", "connected_n",
43-
"percent_connected", "pairwise_connections", "total_weight", "avg_weight",
42+
"id", "query_id", "upstream_class", "downstream_class",
43+
"total_n", "connected_n", "percent_connected",
44+
"pairwise_connections", "total_weight", "avg_weight",
4445
}
4546
assert expected_keys.issubset(row.keys())
4647

@@ -87,8 +88,9 @@ def test_dataframe_has_expected_columns(self):
8788
TEST_CLASS, return_dataframe=True, limit=1, force_refresh=True
8889
)
8990
expected_cols = {
90-
"id", "downstream_class", "total_n", "connected_n",
91-
"percent_connected", "pairwise_connections", "total_weight", "avg_weight",
91+
"id", "query_id", "upstream_class", "downstream_class",
92+
"total_n", "connected_n", "percent_connected",
93+
"pairwise_connections", "total_weight", "avg_weight",
9294
}
9395
assert expected_cols.issubset(set(df.columns))
9496

@@ -129,7 +131,7 @@ def test_parent_class_appears_with_sensible_counts(self, result):
129131
"""
130132
from vfbquery.vfb_queries import vc, get_dict_cursor
131133

132-
rows = result["rows"]
134+
rows = [r for r in result["rows"] if r["query_id"] == TEST_CLASS]
133135
ids = [r["id"] for r in rows]
134136
assert ids, "Expected at least one row to test against"
135137

@@ -147,7 +149,7 @@ def test_parent_class_appears_with_sensible_counts(self, result):
147149
parent_id = pairs[0]["parent"]
148150
child_id = pairs[0]["child"]
149151
parent_row = next(r for r in rows if r["id"] == parent_id)
150-
# Sum connected_n across all descendant rows (not just the one returned).
152+
# Sum connected_n across all descendant rows.
151153
desc_q = (
152154
"MATCH (p:Class {short_form: '%s'})<-[:SUBCLASSOF*1..]-(c:Class) "
153155
"WHERE c.short_form IN %s "
@@ -169,18 +171,67 @@ def test_parent_class_appears_with_sensible_counts(self, result):
169171
)
170172

171173
@pytest.mark.integration
172-
def test_total_n_is_constant_across_rows(self, result):
173-
"""`total_n` is the queried-side instance count and must be the same
174-
for every output row (regression for the previous summed-across-
175-
subclasses value).
174+
def test_total_n_constant_within_each_query_class(self, result):
175+
"""In the downstream direction the presynaptic side is the queried
176+
class, so (matching VFB_connect's normalization) `total_n` is the
177+
queried (sub)class instance count: constant within each query block (it
178+
varies between blocks), and `connected_n` never exceeds it.
176179
"""
180+
from collections import defaultdict
181+
177182
rows = result["rows"]
178183
assert rows, "Expected at least one row"
179-
total_ns = {r["total_n"] for r in rows}
180-
assert len(total_ns) == 1, (
181-
f"Expected total_n to be constant across rows, got: {total_ns}"
184+
by_query = defaultdict(set)
185+
for r in rows:
186+
assert r["connected_n"] <= r["total_n"], (
187+
f"connected_n={r['connected_n']} > total_n={r['total_n']} "
188+
f"for {r['id']}"
189+
)
190+
by_query[r["query_id"]].add(r["total_n"])
191+
for qid, totals in by_query.items():
192+
assert len(totals) == 1, (
193+
f"Expected total_n constant within block {qid}, got: {totals}"
194+
)
195+
assert next(iter(totals)) > 0
196+
197+
@pytest.mark.integration
198+
def test_includes_subclass_breakdown(self, result):
199+
"""The result should contain the input term's own rows plus a block of
200+
rows for each subclass that has connectivity instances. Any non-input
201+
query_id must be a genuine subclass of the input term.
202+
"""
203+
from vfbquery.vfb_queries import vc, get_dict_cursor
204+
205+
rows = result["rows"]
206+
query_ids = {r["query_id"] for r in rows}
207+
assert TEST_CLASS in query_ids, "Expected the input term's own rows"
208+
209+
# Full subclass closure (incl. the input term itself).
210+
q = (
211+
"MATCH (sub:Class)-[:SUBCLASSOF*0..]->(:Class {short_form: '%s'}) "
212+
"RETURN collect(DISTINCT sub.short_form) AS ids" % TEST_CLASS
213+
)
214+
subtree_rows = get_dict_cursor()(vc.nc.commit_list([q]))
215+
subtree = set(subtree_rows[0]["ids"]) if subtree_rows else set()
216+
offenders = [q for q in query_ids if q not in subtree]
217+
assert not offenders, (
218+
f"query_id(s) not in the input term's subclass closure: {offenders}"
219+
)
220+
221+
# Subclasses of the input term that have connectivity instances.
222+
sub_q = (
223+
"MATCH (sub:Class)-[:SUBCLASSOF*1..]->(:Class {short_form: '%s'}) "
224+
"WHERE (sub)<-[:SUBCLASSOF*0..]-(:Class)<-[:INSTANCEOF]-"
225+
"(:Individual:has_neuron_connectivity) "
226+
"RETURN collect(DISTINCT sub.short_form) AS ids" % TEST_CLASS
227+
)
228+
sub_rows = get_dict_cursor()(vc.nc.commit_list([sub_q]))
229+
connected_subclasses = set(sub_rows[0]["ids"]) if sub_rows else set()
230+
if not connected_subclasses:
231+
pytest.skip("Input term has no connectivity-bearing subclasses")
232+
assert query_ids & connected_subclasses, (
233+
"Expected subclass breakdown rows but none were present"
182234
)
183-
assert next(iter(total_ns)) > 0
184235

185236
@pytest.mark.integration
186237
def test_no_rows_above_neuron_root(self, result):

src/test/test_upstream_class_connectivity.py

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ def test_row_has_expected_keys(self):
3939
assert result["rows"], "Expected at least one row"
4040
row = result["rows"][0]
4141
expected_keys = {
42-
"id", "upstream_class", "total_n", "connected_n",
43-
"percent_connected", "pairwise_connections", "total_weight", "avg_weight",
42+
"id", "query_id", "upstream_class", "downstream_class",
43+
"total_n", "connected_n", "percent_connected",
44+
"pairwise_connections", "total_weight", "avg_weight",
4445
}
4546
assert expected_keys.issubset(row.keys())
4647

@@ -87,8 +88,9 @@ def test_dataframe_has_expected_columns(self):
8788
TEST_CLASS, return_dataframe=True, limit=1, force_refresh=True
8889
)
8990
expected_cols = {
90-
"id", "upstream_class", "total_n", "connected_n",
91-
"percent_connected", "pairwise_connections", "total_weight", "avg_weight",
91+
"id", "query_id", "upstream_class", "downstream_class",
92+
"total_n", "connected_n", "percent_connected",
93+
"pairwise_connections", "total_weight", "avg_weight",
9294
}
9395
assert expected_cols.issubset(set(df.columns))
9496

@@ -126,10 +128,13 @@ def test_parent_class_appears_with_sensible_counts(self, result):
126128
"""A row keyed on a parent class should have connected_n at least as
127129
large as any of its descendant rows (set-union semantics) and at most
128130
the sum of descendant connected_n.
131+
132+
Restricted to the input term's own block so partner rows are not mixed
133+
across queried (sub)classes.
129134
"""
130135
from vfbquery.vfb_queries import vc, get_dict_cursor
131136

132-
rows = result["rows"]
137+
rows = [r for r in result["rows"] if r["query_id"] == TEST_CLASS]
133138
ids = [r["id"] for r in rows]
134139
assert ids, "Expected at least one row to test against"
135140

@@ -166,17 +171,68 @@ def test_parent_class_appears_with_sensible_counts(self, result):
166171
)
167172

168173
@pytest.mark.integration
169-
def test_total_n_is_constant_across_rows(self, result):
170-
"""`total_n` is the queried-side instance count and must be the same
171-
for every output row.
174+
def test_total_n_is_per_partner(self, result):
175+
"""In the upstream direction the presynaptic side is the partner, so
176+
(matching VFB_connect's normalization) `total_n` describes the partner
177+
(`upstream_class`): it must be constant across every row referencing the
178+
same partner id, regardless of which queried (sub)class block it is in,
179+
and `connected_n` must never exceed it.
172180
"""
181+
from collections import defaultdict
182+
173183
rows = result["rows"]
174184
assert rows, "Expected at least one row"
175-
total_ns = {r["total_n"] for r in rows}
176-
assert len(total_ns) == 1, (
177-
f"Expected total_n to be constant across rows, got: {total_ns}"
185+
by_partner = defaultdict(set)
186+
for r in rows:
187+
assert r["connected_n"] <= r["total_n"], (
188+
f"connected_n={r['connected_n']} > total_n={r['total_n']} "
189+
f"for {r['id']}"
190+
)
191+
by_partner[r["id"]].add(r["total_n"])
192+
for pid, totals in by_partner.items():
193+
assert len(totals) == 1, (
194+
f"total_n varies for partner {pid}: {totals}"
195+
)
196+
assert next(iter(totals)) > 0
197+
198+
@pytest.mark.integration
199+
def test_includes_subclass_breakdown(self, result):
200+
"""The result should contain the input term's own rows plus a block of
201+
rows for each subclass that has connectivity instances. Any non-input
202+
query_id must be a genuine subclass of the input term.
203+
"""
204+
from vfbquery.vfb_queries import vc, get_dict_cursor
205+
206+
rows = result["rows"]
207+
query_ids = {r["query_id"] for r in rows}
208+
assert TEST_CLASS in query_ids, "Expected the input term's own rows"
209+
210+
# Full subclass closure (incl. the input term itself).
211+
q = (
212+
"MATCH (sub:Class)-[:SUBCLASSOF*0..]->(:Class {short_form: '%s'}) "
213+
"RETURN collect(DISTINCT sub.short_form) AS ids" % TEST_CLASS
214+
)
215+
subtree_rows = get_dict_cursor()(vc.nc.commit_list([q]))
216+
subtree = set(subtree_rows[0]["ids"]) if subtree_rows else set()
217+
offenders = [q for q in query_ids if q not in subtree]
218+
assert not offenders, (
219+
f"query_id(s) not in the input term's subclass closure: {offenders}"
220+
)
221+
222+
# Subclasses of the input term that have connectivity instances.
223+
sub_q = (
224+
"MATCH (sub:Class)-[:SUBCLASSOF*1..]->(:Class {short_form: '%s'}) "
225+
"WHERE (sub)<-[:SUBCLASSOF*0..]-(:Class)<-[:INSTANCEOF]-"
226+
"(:Individual:has_neuron_connectivity) "
227+
"RETURN collect(DISTINCT sub.short_form) AS ids" % TEST_CLASS
228+
)
229+
sub_rows = get_dict_cursor()(vc.nc.commit_list([sub_q]))
230+
connected_subclasses = set(sub_rows[0]["ids"]) if sub_rows else set()
231+
if not connected_subclasses:
232+
pytest.skip("Input term has no connectivity-bearing subclasses")
233+
assert query_ids & connected_subclasses, (
234+
"Expected subclass breakdown rows but none were present"
178235
)
179-
assert next(iter(total_ns)) > 0
180236

181237
@pytest.mark.integration
182238
def test_no_rows_above_neuron_root(self, result):

0 commit comments

Comments
 (0)