Skip to content

Commit 8c2cbd5

Browse files
authored
Merge pull request #63 from VirtualFlyBrain/fix/instance-class-query-inheritance
Match legacy class->instance query inheritance in term_info
2 parents 537d284 + bb1072f commit 8c2cbd5

2 files changed

Lines changed: 158 additions & 98 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""
2+
Test suite for class->instance query inheritance in term_info.
3+
4+
The legacy term-info builder (uk.ac.vfb.geppetto VFBProcessTermInfoCachedJson,
5+
gate ~line 1757) brought a class's full query menu down onto Individuals of a
6+
fixed set of anatomical / expression-pattern types, running each query on the
7+
parent class (QueryChecker.check(query, classVariable)). The VFBquery port had
8+
replaced that type-based gate with a Technique == "computer graphic" heuristic,
9+
which only caught painted domains and dropped confocal instances such as
10+
expression-pattern images (R40G10, VFB_00020530) and splits (VFB_00069525).
11+
12+
The reinstated behaviour: an Individual of one of the inherited types shows
13+
exactly the queries its parent class shows, anchored on the class. These tests
14+
assert that parity: instance inherited-menu ⊇ class menu, and every inherited
15+
query runs on the class rather than the individual.
16+
"""
17+
18+
import unittest
19+
import sys
20+
import os
21+
22+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
23+
24+
from vfbquery.vfb_queries import get_term_info
25+
26+
27+
def _menu(term_info):
28+
"""query_id -> anchored short_form for each query in a term_info menu."""
29+
out = {}
30+
for q in (term_info or {}).get("Queries", []) or []:
31+
if isinstance(q, dict):
32+
out[q.get("query")] = q.get("takes", {}).get("default", {}).get("short_form")
33+
return out
34+
35+
36+
class TestExpressionPatternIndividualQueries(unittest.TestCase):
37+
"""R40G10 expression-pattern image inherits its class's menu."""
38+
39+
EP_INDIVIDUAL = "VFB_00020530" # R40G10 in the adult brain (confocal)
40+
EP_CLASS = "VFBexp_FBtp0060056" # P{GMR40G10-GAL4} expression pattern
41+
42+
@classmethod
43+
def setUpClass(cls):
44+
cls.ind = get_term_info(cls.EP_INDIVIDUAL, preview=False)
45+
cls.cls = get_term_info(cls.EP_CLASS, preview=False)
46+
47+
def test_is_expression_pattern_individual(self):
48+
if not self.ind:
49+
self.skipTest("term_info unavailable (no live VFB backend)")
50+
self.assertTrue(self.ind.get("IsIndividual"))
51+
self.assertIn("Expression_pattern", self.ind.get("SuperTypes", []))
52+
53+
def test_instance_menu_includes_everything_the_class_offers(self):
54+
if not self.ind or not self.cls:
55+
self.skipTest("term_info unavailable (no live VFB backend)")
56+
class_menu = _menu(self.cls)
57+
ind_menu = _menu(self.ind)
58+
# Queries the class offers must all appear on the instance...
59+
missing = set(class_menu) - set(ind_menu)
60+
self.assertFalse(missing, f"instance is missing class queries: {sorted(missing)}")
61+
# ...and each such inherited query must run on the class, not the instance.
62+
for qid in class_menu:
63+
self.assertEqual(ind_menu[qid], self.EP_CLASS,
64+
f"{qid} on the instance should anchor on {self.EP_CLASS}")
65+
66+
def test_expected_ep_queries_present(self):
67+
if not self.ind:
68+
self.skipTest("term_info unavailable (no live VFB backend)")
69+
ind_menu = _menu(self.ind)
70+
for qid in ("AnatomyExpressedIn", "epFrag", "ListAllAvailableImages",
71+
"NeuronsPartHere", "PartsOf", "SubclassesOf"):
72+
self.assertIn(qid, ind_menu, f"expected {qid} inherited onto the EP instance")
73+
self.assertEqual(ind_menu[qid], self.EP_CLASS)
74+
75+
def test_no_query_is_anchored_on_the_individual(self):
76+
"""Inherited class queries run on the class; none should target the VFB_ instance."""
77+
for qid, anchor in _menu(self.ind).items():
78+
self.assertNotEqual(anchor, self.EP_INDIVIDUAL,
79+
f"{qid} should not run on the individual")
80+
81+
82+
class TestSplitIndividualQueries(unittest.TestCase):
83+
"""A confocal split-GAL4 image was also missed by the technique gate."""
84+
85+
SPLIT_INDIVIDUAL = "VFB_00069525" # JRC_SS00810 in the Adult Brain
86+
87+
def test_split_individual_inherits_ep_queries(self):
88+
ind = get_term_info(self.SPLIT_INDIVIDUAL, preview=False)
89+
if not ind:
90+
self.skipTest("term_info unavailable (no live VFB backend)")
91+
self.assertTrue(ind.get("IsIndividual"))
92+
self.assertIn("Split", ind.get("SuperTypes", []))
93+
ind_menu = _menu(ind)
94+
# AnatomyExpressedIn is the defining expression-pattern query; it must be
95+
# present and anchored on a class (VFBexp*), not the VFB_ individual.
96+
self.assertIn("AnatomyExpressedIn", ind_menu)
97+
self.assertNotEqual(ind_menu["AnatomyExpressedIn"], self.SPLIT_INDIVIDUAL)
98+
self.assertTrue(str(ind_menu["AnatomyExpressedIn"]).startswith("VFBexp"))
99+
100+
101+
if __name__ == "__main__":
102+
unittest.main(verbosity=2)

src/vfbquery/vfb_queries.py

Lines changed: 56 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,104 +1346,62 @@ def term_info_parse_object(results, short_form):
13461346
"default_args": {"fbco_id": sf},
13471347
})
13481348

1349-
# For individuals that are painted domains of anatomical regions, add parent class queries
1350-
if termInfo["IsIndividual"] and termInfo["Technique"] and any('computer' in t.lower() for t in termInfo["Technique"]):
1351-
anatomical_classes = []
1352-
1353-
# Check parents
1354-
if vfbTerm.parents:
1355-
for parent in vfbTerm.parents:
1356-
if parent.types and "Class" in parent.types and "Anatomy" in parent.types:
1357-
anatomical_classes.append(parent)
1358-
1359-
# Check relationships for anatomical classes
1360-
if vfbTerm.relationships:
1361-
for rel in vfbTerm.relationships:
1362-
if hasattr(rel, 'object') and rel.object and hasattr(rel.object, 'types') and rel.object.types:
1363-
if "Class" in rel.object.types and "Anatomy" in rel.object.types:
1364-
anatomical_classes.append(rel.object)
1365-
1366-
# Remove duplicates based on short_form
1367-
seen = set()
1368-
unique_anatomical_classes = []
1369-
for cls in anatomical_classes:
1370-
if cls.short_form not in seen:
1371-
seen.add(cls.short_form)
1372-
unique_anatomical_classes.append(cls)
1373-
1374-
for parent in unique_anatomical_classes:
1375-
parent_short_form = parent.short_form
1376-
parent_label = parent.label if parent.label else parent_short_form
1377-
1378-
# Add queries based on parent types
1379-
if "Anatomy" in parent.types or "Synaptic_neuropil" in parent.types or "Synaptic_neuropil_domain" in parent.types:
1380-
# NeuronsPartHere query
1381-
q = NeuronsPartHere_to_schema(parent_label, {"short_form": parent_short_form})
1382-
queries.append(q)
1383-
1384-
# NeuronsSynaptic query
1385-
q = NeuronsSynaptic_to_schema(parent_label, {"short_form": parent_short_form})
1386-
queries.append(q)
1387-
1388-
# NeuronsPresynapticHere query
1389-
q = NeuronsPresynapticHere_to_schema(parent_label, {"short_form": parent_short_form})
1390-
queries.append(q)
1391-
1392-
# NeuronsPostsynapticHere query
1393-
q = NeuronsPostsynapticHere_to_schema(parent_label, {"short_form": parent_short_form})
1394-
queries.append(q)
1395-
1396-
# TractsNervesInnervatingHere query
1397-
q = TractsNervesInnervatingHere_to_schema(parent_label, {"short_form": parent_short_form})
1398-
queries.append(q)
1399-
1400-
# LineageClonesIn query
1401-
q = LineageClonesIn_to_schema(parent_label, {"short_form": parent_short_form})
1402-
queries.append(q)
1403-
1404-
# ImagesNeurons query
1405-
q = ImagesNeurons_to_schema(parent_label, {"short_form": parent_short_form})
1406-
queries.append(q)
1407-
1408-
if "Expression_pattern" in parent.types or "Expression_pattern_fragment" in parent.types:
1409-
# AnatomyExpressedIn query — anatomy classes where this
1410-
# expression pattern is expressed.
1411-
q = AnatomyExpressedIn_to_schema(parent_label, {"short_form": parent_short_form})
1412-
queries.append(q)
1413-
1414-
if "Anatomy" in parent.types and "hasScRNAseq" in parent.types:
1415-
# anatScRNAseqQuery query
1416-
q = anatScRNAseqQuery_to_schema(parent_label, {"short_form": parent_short_form})
1417-
queries.append(q)
1418-
1419-
if "Neuron_projection_bundle" in parent.types:
1420-
# NeuronClassesFasciculatingHere query
1421-
q = NeuronClassesFasciculatingHere_to_schema(parent_label, {"short_form": parent_short_form})
1422-
queries.append(q)
1423-
1424-
if "Neuroblast" in parent.types:
1425-
# ImagesThatDevelopFrom query
1426-
q = ImagesThatDevelopFrom_to_schema(parent_label, {"short_form": parent_short_form})
1427-
queries.append(q)
1428-
1429-
if "Expression_pattern" in parent.types:
1430-
# epFrag query
1431-
q = epFrag_to_schema(parent_label, {"short_form": parent_short_form})
1432-
queries.append(q)
1433-
1434-
if "Nervous_system" in parent.types and ("Anatomy" in parent.types or "Neuron" in parent.types):
1435-
# TransgeneExpressionHere query
1436-
q = TransgeneExpressionHere_to_schema(parent_label, {"short_form": parent_short_form})
1437-
queries.append(q)
1438-
1439-
# PartsOf query - for any Class
1440-
q = PartsOf_to_schema(parent_label, {"short_form": parent_short_form})
1441-
queries.append(q)
1442-
1443-
# SubclassesOf query - for any Class
1444-
q = SubclassesOf_to_schema(parent_label, {"short_form": parent_short_form})
1445-
queries.append(q)
1446-
1349+
# Bring the parent class's query menu down onto selected Individual
1350+
# instances, reproducing the legacy term-info builder
1351+
# (VFBProcessTermInfoCachedJson, gate ~line 1757): an Individual of one
1352+
# of these anatomical / expression-pattern types inherits every
1353+
# class-anchored query its parent class matches, run on the class (not
1354+
# the individual). Replaces an earlier Technique == "computer graphic"
1355+
# gate that only caught painted domains and silently dropped confocal
1356+
# instances (expression patterns, splits, tracts, muscle).
1357+
#
1358+
# The predicate table mirrors the class-menu conditions above; keep the
1359+
# two in sync (a future refactor should share a single source).
1360+
inherit_instance_types = (
1361+
"Painted_domain", "Synaptic_neuropil", "Synaptic_neuropil_domain",
1362+
"Synaptic_neuropil_subdomain", "Neuron_projection_bundle", "Split",
1363+
"Expression_pattern", "Muscle",
1364+
)
1365+
instance_super_types = termInfo["SuperTypes"] or []
1366+
if termInfo["IsIndividual"] and any(t in instance_super_types for t in inherit_instance_types):
1367+
# Legacy anchored on the first parent Class (classVariable = parents[0]).
1368+
inherit_parent = next(
1369+
(p for p in (vfbTerm.parents or []) if p.types and "Class" in p.types),
1370+
None,
1371+
)
1372+
if inherit_parent is not None:
1373+
p_types = set(inherit_parent.types or [])
1374+
p_ref = {"short_form": inherit_parent.short_form}
1375+
p_label = inherit_parent.label if inherit_parent.label else inherit_parent.short_form
1376+
# (schema builder, predicate over the parent class's type set).
1377+
# Predicates copy the Class-gated conditions used for the term's
1378+
# own menu above so an instance shows exactly what its class shows.
1379+
inheritable_class_queries = (
1380+
(ListAllAvailableImages_to_schema, lambda p: {"Class", "Anatomy"} <= p),
1381+
(NeuronsPartHere_to_schema, lambda p: "Class" in p and "Neuron" not in p and ("Synaptic_neuropil" in p or "Anatomy" in p)),
1382+
(NeuronsSynaptic_to_schema, lambda p: "Class" in p and ("Synaptic_neuropil" in p or "Visual_system" in p or "Synaptic_neuropil_domain" in p)),
1383+
(NeuronsPresynapticHere_to_schema, lambda p: "Class" in p and ("Synaptic_neuropil" in p or "Visual_system" in p or "Synaptic_neuropil_domain" in p)),
1384+
(NeuronsPostsynapticHere_to_schema, lambda p: "Class" in p and ("Synaptic_neuropil" in p or "Visual_system" in p or "Synaptic_neuropil_domain" in p)),
1385+
(ImagesNeurons_to_schema, lambda p: "Class" in p and ("Synaptic_neuropil" in p or "Synaptic_neuropil_domain" in p)),
1386+
(TractsNervesInnervatingHere_to_schema, lambda p: "Class" in p and ("Synaptic_neuropil" in p or "Synaptic_neuropil_domain" in p)),
1387+
(LineageClonesIn_to_schema, lambda p: "Class" in p and ("Synaptic_neuropil" in p or "Synaptic_neuropil_domain" in p)),
1388+
(NeuronClassesFasciculatingHere_to_schema, lambda p: "Class" in p and "Neuron_projection_bundle" in p),
1389+
(ComponentsOf_to_schema, lambda p: {"Class", "Clone"} <= p),
1390+
(ImagesThatDevelopFrom_to_schema, lambda p: {"Class", "Neuroblast"} <= p),
1391+
(epFrag_to_schema, lambda p: {"Class", "Expression_pattern"} <= p),
1392+
(AnatomyExpressedIn_to_schema, lambda p: "Class" in p and ("Expression_pattern" in p or "Expression_pattern_fragment" in p)),
1393+
(anatScRNAseqQuery_to_schema, lambda p: {"Class", "Anatomy", "hasScRNAseq"} <= p),
1394+
(TransgeneExpressionHere_to_schema, lambda p: "Class" in p and "Nervous_system" in p and ("Anatomy" in p or "Neuron" in p)),
1395+
(TargetNeurons_to_schema, lambda p: {"Class", "Split"} <= p),
1396+
(DownstreamClassConnectivity_to_schema, lambda p: {"Class", "Neuron"} <= p),
1397+
(UpstreamClassConnectivity_to_schema, lambda p: {"Class", "Neuron"} <= p),
1398+
(PartsOf_to_schema, lambda p: "Class" in p and "Neuron" not in p),
1399+
(SubclassesOf_to_schema, lambda p: "Class" in p),
1400+
)
1401+
for schema_fn, predicate in inheritable_class_queries:
1402+
if predicate(p_types):
1403+
queries.append(schema_fn(p_label, p_ref))
1404+
14471405
# Add Publications to the termInfo object
14481406
if vfbTerm.pubs and len(vfbTerm.pubs) > 0:
14491407
publications = []

0 commit comments

Comments
 (0)