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
102 changes: 102 additions & 0 deletions src/test/test_expression_pattern_individual_queries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""
Test suite for class->instance query inheritance in term_info.

The legacy term-info builder (uk.ac.vfb.geppetto VFBProcessTermInfoCachedJson,
gate ~line 1757) brought a class's full query menu down onto Individuals of a
fixed set of anatomical / expression-pattern types, running each query on the
parent class (QueryChecker.check(query, classVariable)). The VFBquery port had
replaced that type-based gate with a Technique == "computer graphic" heuristic,
which only caught painted domains and dropped confocal instances such as
expression-pattern images (R40G10, VFB_00020530) and splits (VFB_00069525).

The reinstated behaviour: an Individual of one of the inherited types shows
exactly the queries its parent class shows, anchored on the class. These tests
assert that parity: instance inherited-menu ⊇ class menu, and every inherited
query runs on the class rather than the individual.
"""

import unittest
import sys
import os

sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))

from vfbquery.vfb_queries import get_term_info


def _menu(term_info):
"""query_id -> anchored short_form for each query in a term_info menu."""
out = {}
for q in (term_info or {}).get("Queries", []) or []:
if isinstance(q, dict):
out[q.get("query")] = q.get("takes", {}).get("default", {}).get("short_form")
return out


class TestExpressionPatternIndividualQueries(unittest.TestCase):
"""R40G10 expression-pattern image inherits its class's menu."""

EP_INDIVIDUAL = "VFB_00020530" # R40G10 in the adult brain (confocal)
EP_CLASS = "VFBexp_FBtp0060056" # P{GMR40G10-GAL4} expression pattern

@classmethod
def setUpClass(cls):
cls.ind = get_term_info(cls.EP_INDIVIDUAL, preview=False)
cls.cls = get_term_info(cls.EP_CLASS, preview=False)

def test_is_expression_pattern_individual(self):
if not self.ind:
self.skipTest("term_info unavailable (no live VFB backend)")
self.assertTrue(self.ind.get("IsIndividual"))
self.assertIn("Expression_pattern", self.ind.get("SuperTypes", []))

def test_instance_menu_includes_everything_the_class_offers(self):
if not self.ind or not self.cls:
self.skipTest("term_info unavailable (no live VFB backend)")
class_menu = _menu(self.cls)
ind_menu = _menu(self.ind)
# Queries the class offers must all appear on the instance...
missing = set(class_menu) - set(ind_menu)
self.assertFalse(missing, f"instance is missing class queries: {sorted(missing)}")
# ...and each such inherited query must run on the class, not the instance.
for qid in class_menu:
self.assertEqual(ind_menu[qid], self.EP_CLASS,
f"{qid} on the instance should anchor on {self.EP_CLASS}")

def test_expected_ep_queries_present(self):
if not self.ind:
self.skipTest("term_info unavailable (no live VFB backend)")
ind_menu = _menu(self.ind)
for qid in ("AnatomyExpressedIn", "epFrag", "ListAllAvailableImages",
"NeuronsPartHere", "PartsOf", "SubclassesOf"):
self.assertIn(qid, ind_menu, f"expected {qid} inherited onto the EP instance")
self.assertEqual(ind_menu[qid], self.EP_CLASS)

def test_no_query_is_anchored_on_the_individual(self):
"""Inherited class queries run on the class; none should target the VFB_ instance."""
for qid, anchor in _menu(self.ind).items():
self.assertNotEqual(anchor, self.EP_INDIVIDUAL,
f"{qid} should not run on the individual")


class TestSplitIndividualQueries(unittest.TestCase):
"""A confocal split-GAL4 image was also missed by the technique gate."""

SPLIT_INDIVIDUAL = "VFB_00069525" # JRC_SS00810 in the Adult Brain

def test_split_individual_inherits_ep_queries(self):
ind = get_term_info(self.SPLIT_INDIVIDUAL, preview=False)
if not ind:
self.skipTest("term_info unavailable (no live VFB backend)")
self.assertTrue(ind.get("IsIndividual"))
self.assertIn("Split", ind.get("SuperTypes", []))
ind_menu = _menu(ind)
# AnatomyExpressedIn is the defining expression-pattern query; it must be
# present and anchored on a class (VFBexp*), not the VFB_ individual.
self.assertIn("AnatomyExpressedIn", ind_menu)
self.assertNotEqual(ind_menu["AnatomyExpressedIn"], self.SPLIT_INDIVIDUAL)
self.assertTrue(str(ind_menu["AnatomyExpressedIn"]).startswith("VFBexp"))


if __name__ == "__main__":
unittest.main(verbosity=2)
154 changes: 56 additions & 98 deletions src/vfbquery/vfb_queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -1346,104 +1346,62 @@ def term_info_parse_object(results, short_form):
"default_args": {"fbco_id": sf},
})

# For individuals that are painted domains of anatomical regions, add parent class queries
if termInfo["IsIndividual"] and termInfo["Technique"] and any('computer' in t.lower() for t in termInfo["Technique"]):
anatomical_classes = []

# Check parents
if vfbTerm.parents:
for parent in vfbTerm.parents:
if parent.types and "Class" in parent.types and "Anatomy" in parent.types:
anatomical_classes.append(parent)

# Check relationships for anatomical classes
if vfbTerm.relationships:
for rel in vfbTerm.relationships:
if hasattr(rel, 'object') and rel.object and hasattr(rel.object, 'types') and rel.object.types:
if "Class" in rel.object.types and "Anatomy" in rel.object.types:
anatomical_classes.append(rel.object)

# Remove duplicates based on short_form
seen = set()
unique_anatomical_classes = []
for cls in anatomical_classes:
if cls.short_form not in seen:
seen.add(cls.short_form)
unique_anatomical_classes.append(cls)

for parent in unique_anatomical_classes:
parent_short_form = parent.short_form
parent_label = parent.label if parent.label else parent_short_form

# Add queries based on parent types
if "Anatomy" in parent.types or "Synaptic_neuropil" in parent.types or "Synaptic_neuropil_domain" in parent.types:
# NeuronsPartHere query
q = NeuronsPartHere_to_schema(parent_label, {"short_form": parent_short_form})
queries.append(q)

# NeuronsSynaptic query
q = NeuronsSynaptic_to_schema(parent_label, {"short_form": parent_short_form})
queries.append(q)

# NeuronsPresynapticHere query
q = NeuronsPresynapticHere_to_schema(parent_label, {"short_form": parent_short_form})
queries.append(q)

# NeuronsPostsynapticHere query
q = NeuronsPostsynapticHere_to_schema(parent_label, {"short_form": parent_short_form})
queries.append(q)

# TractsNervesInnervatingHere query
q = TractsNervesInnervatingHere_to_schema(parent_label, {"short_form": parent_short_form})
queries.append(q)

# LineageClonesIn query
q = LineageClonesIn_to_schema(parent_label, {"short_form": parent_short_form})
queries.append(q)

# ImagesNeurons query
q = ImagesNeurons_to_schema(parent_label, {"short_form": parent_short_form})
queries.append(q)

if "Expression_pattern" in parent.types or "Expression_pattern_fragment" in parent.types:
# AnatomyExpressedIn query — anatomy classes where this
# expression pattern is expressed.
q = AnatomyExpressedIn_to_schema(parent_label, {"short_form": parent_short_form})
queries.append(q)

if "Anatomy" in parent.types and "hasScRNAseq" in parent.types:
# anatScRNAseqQuery query
q = anatScRNAseqQuery_to_schema(parent_label, {"short_form": parent_short_form})
queries.append(q)

if "Neuron_projection_bundle" in parent.types:
# NeuronClassesFasciculatingHere query
q = NeuronClassesFasciculatingHere_to_schema(parent_label, {"short_form": parent_short_form})
queries.append(q)

if "Neuroblast" in parent.types:
# ImagesThatDevelopFrom query
q = ImagesThatDevelopFrom_to_schema(parent_label, {"short_form": parent_short_form})
queries.append(q)

if "Expression_pattern" in parent.types:
# epFrag query
q = epFrag_to_schema(parent_label, {"short_form": parent_short_form})
queries.append(q)

if "Nervous_system" in parent.types and ("Anatomy" in parent.types or "Neuron" in parent.types):
# TransgeneExpressionHere query
q = TransgeneExpressionHere_to_schema(parent_label, {"short_form": parent_short_form})
queries.append(q)

# PartsOf query - for any Class
q = PartsOf_to_schema(parent_label, {"short_form": parent_short_form})
queries.append(q)

# SubclassesOf query - for any Class
q = SubclassesOf_to_schema(parent_label, {"short_form": parent_short_form})
queries.append(q)

# Bring the parent class's query menu down onto selected Individual
# instances, reproducing the legacy term-info builder
# (VFBProcessTermInfoCachedJson, gate ~line 1757): an Individual of one
# of these anatomical / expression-pattern types inherits every
# class-anchored query its parent class matches, run on the class (not
# the individual). Replaces an earlier Technique == "computer graphic"
# gate that only caught painted domains and silently dropped confocal
# instances (expression patterns, splits, tracts, muscle).
#
# The predicate table mirrors the class-menu conditions above; keep the
# two in sync (a future refactor should share a single source).
inherit_instance_types = (
"Painted_domain", "Synaptic_neuropil", "Synaptic_neuropil_domain",
"Synaptic_neuropil_subdomain", "Neuron_projection_bundle", "Split",
"Expression_pattern", "Muscle",
)
instance_super_types = termInfo["SuperTypes"] or []
if termInfo["IsIndividual"] and any(t in instance_super_types for t in inherit_instance_types):
# Legacy anchored on the first parent Class (classVariable = parents[0]).
inherit_parent = next(
(p for p in (vfbTerm.parents or []) if p.types and "Class" in p.types),
None,
)
if inherit_parent is not None:
p_types = set(inherit_parent.types or [])
p_ref = {"short_form": inherit_parent.short_form}
p_label = inherit_parent.label if inherit_parent.label else inherit_parent.short_form
# (schema builder, predicate over the parent class's type set).
# Predicates copy the Class-gated conditions used for the term's
# own menu above so an instance shows exactly what its class shows.
inheritable_class_queries = (
(ListAllAvailableImages_to_schema, lambda p: {"Class", "Anatomy"} <= p),
(NeuronsPartHere_to_schema, lambda p: "Class" in p and "Neuron" not in p and ("Synaptic_neuropil" in p or "Anatomy" in p)),
(NeuronsSynaptic_to_schema, lambda p: "Class" in p and ("Synaptic_neuropil" in p or "Visual_system" in p or "Synaptic_neuropil_domain" in p)),
(NeuronsPresynapticHere_to_schema, lambda p: "Class" in p and ("Synaptic_neuropil" in p or "Visual_system" in p or "Synaptic_neuropil_domain" in p)),
(NeuronsPostsynapticHere_to_schema, lambda p: "Class" in p and ("Synaptic_neuropil" in p or "Visual_system" in p or "Synaptic_neuropil_domain" in p)),
(ImagesNeurons_to_schema, lambda p: "Class" in p and ("Synaptic_neuropil" in p or "Synaptic_neuropil_domain" in p)),
(TractsNervesInnervatingHere_to_schema, lambda p: "Class" in p and ("Synaptic_neuropil" in p or "Synaptic_neuropil_domain" in p)),
(LineageClonesIn_to_schema, lambda p: "Class" in p and ("Synaptic_neuropil" in p or "Synaptic_neuropil_domain" in p)),
(NeuronClassesFasciculatingHere_to_schema, lambda p: "Class" in p and "Neuron_projection_bundle" in p),
(ComponentsOf_to_schema, lambda p: {"Class", "Clone"} <= p),
(ImagesThatDevelopFrom_to_schema, lambda p: {"Class", "Neuroblast"} <= p),
(epFrag_to_schema, lambda p: {"Class", "Expression_pattern"} <= p),
(AnatomyExpressedIn_to_schema, lambda p: "Class" in p and ("Expression_pattern" in p or "Expression_pattern_fragment" in p)),
(anatScRNAseqQuery_to_schema, lambda p: {"Class", "Anatomy", "hasScRNAseq"} <= p),
(TransgeneExpressionHere_to_schema, lambda p: "Class" in p and "Nervous_system" in p and ("Anatomy" in p or "Neuron" in p)),
(TargetNeurons_to_schema, lambda p: {"Class", "Split"} <= p),
(DownstreamClassConnectivity_to_schema, lambda p: {"Class", "Neuron"} <= p),
(UpstreamClassConnectivity_to_schema, lambda p: {"Class", "Neuron"} <= p),
(PartsOf_to_schema, lambda p: "Class" in p and "Neuron" not in p),
(SubclassesOf_to_schema, lambda p: "Class" in p),
)
for schema_fn, predicate in inheritable_class_queries:
if predicate(p_types):
queries.append(schema_fn(p_label, p_ref))

# Add Publications to the termInfo object
if vfbTerm.pubs and len(vfbTerm.pubs) > 0:
publications = []
Expand Down
Loading