From 6348f408b5cb7af1c670e532b2f5d4bd805346ca Mon Sep 17 00:00:00 2001 From: Robbie Court Date: Fri, 23 Oct 2026 21:25:16 +0100 Subject: [PATCH] Include ontology classes in TermsForPub The has_reference branch matched only Individuals, but FBbt classes cite papers for their definitions and synonyms. Anatomy papers such as Li et al. 2020 (FBrf0248215) are cited only by classes, so the query returned nothing. Match Individuals and Classes, excluding Deprecated terms. Show the SUBCLASSOF parent(s) as Type for class rows. Report why each term cites the pub (Definition, Synonym, Text-mined, Reference, Expression) from the edge typ. Add regression tests for both kinds. --- .../test_publication_transgene_queries.py | 21 ++++++++ src/vfbquery/vfb_queries.py | 48 ++++++++++++++----- 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/src/test/test_publication_transgene_queries.py b/src/test/test_publication_transgene_queries.py index 866f521..99cdb82 100644 --- a/src/test/test_publication_transgene_queries.py +++ b/src/test/test_publication_transgene_queries.py @@ -56,6 +56,27 @@ def test_get_terms_for_pub_formatted(self): self.assertIn('rows', result) self.assertTrue(result['rows'], f"{self.pub_term} should have referenced terms") + def test_terms_for_pub_includes_classes(self): + """Ontology classes citing a pub (definition/synonym refs) are listed. + + Li et al. 2020 (FBrf0248215, mushroom body connectome) is cited only + by FBbt classes; matching just Individuals returned 0 rows. + """ + result = get_terms_for_pub('FBrf0248215', return_dataframe=False) + rows = {r['id']: r for r in result['rows']} + self.assertIn('FBbt_00005801', rows, "mushroom body cites FBrf0248215 for its definition") + mb = rows['FBbt_00005801'] + self.assertIn('Definition', mb['reference_type']) + self.assertIn('FBbt_00041000', mb['type'], "Class rows show their SUBCLASSOF parent(s) as type") + self.assertNotIn('[null]', mb['type']) + + def test_terms_for_pub_keeps_individuals(self): + """Individual (DataSet) references survive alongside Class ones.""" + result = get_terms_for_pub(self.pub_term, return_dataframe=False) + ids = {r['id'] for r in result['rows']} + self.assertIn('Aso2014', ids, "the Aso2014 DataSet references FBrf0227179") + self.assertTrue(any(i.startswith('FBbt_') for i in ids), "FBbt classes also cite FBrf0227179") + def test_get_transgene_expression_here(self): """Test get_transgene_expression_here query""" result = get_transgene_expression_here(self.anatomy_term, return_dataframe=True, limit=10) diff --git a/src/vfbquery/vfb_queries.py b/src/vfbquery/vfb_queries.py index 1c8d659..93289ec 100644 --- a/src/vfbquery/vfb_queries.py +++ b/src/vfbquery/vfb_queries.py @@ -7331,9 +7331,10 @@ def get_terms_for_pub(pub_short_form: str, return_dataframe=True, limit: int = - """ # A publication is cited two different ways in the graph, and the legacy # TermsForPub only saw the first: - # 1. Reference — a term has a direct (:term)-[:has_reference]->(:pub) - # edge (datasets, images, anatomy the paper is the - # source/citation for). + # 1. Reference — a term (Individual or Class) has a direct + # (:term)-[:has_reference]->(:pub) edge: datasets and + # images the paper is the source for, and ontology + # classes citing it for a definition or synonym. # 2. Expression — the pub is recorded as a `pub` array PROPERTY on an # overlaps/part_of relationship of an expression-pattern # individual (the same model AnatomyExpressedIn / @@ -7346,17 +7347,34 @@ def get_terms_for_pub(pub_short_form: str, return_dataframe=True, limit: int = - # the relationship `pub` property (no node path exists — the pub node has # no edges), so it is the expensive leg; the whole query is cached. - # Source 1: direct has_reference terms, with image enrichment for - # channel-image primaries (one representative image via the CALL). + # Source 1: direct has_reference terms. Both Individuals (datasets, + # images, neurons cited for a synonym) AND Classes carry these edges — + # FBbt classes cite papers for their definitions and synonyms, and for + # anatomy papers (e.g. Li et al. 2020, FBrf0248215, the mushroom body + # connectome) every citing term is a Class. Matching only + # `primary:Individual` returned 0 for such papers. + # + # Each edge's `typ` says why the term cites the pub ('def', 'syn', 'nlp'; + # null on DataSet references) and feeds the "Reference type" column. + # Type is the INSTANCEOF class for an Individual, the direct SUBCLASSOF + # parent(s) for a Class. The image enrichment only ever matches + # channel-image Individuals; Classes leave those cells blank. + # Obsolete (Deprecated) terms are excluded. ref_query = f""" - MATCH (:pub:Individual {{short_form:'{pub_short_form}'}})<-[:has_reference]-(primary:Individual) - OPTIONAL MATCH (primary)-[:INSTANCEOF]->(typ:Class) - WITH DISTINCT primary, typ + MATCH (:pub:Individual {{short_form:'{pub_short_form}'}})<-[ref:has_reference]-(primary) + WHERE (primary:Individual OR primary:Class) AND NOT primary:Deprecated + WITH primary, collect(DISTINCT coalesce(ref.typ, '')) AS ref_typs + CALL {{ + WITH primary + OPTIONAL MATCH (primary)-[:INSTANCEOF|SUBCLASSOF]->(typ:Class) + WITH typ ORDER BY typ.label + RETURN collect(CASE WHEN typ IS NULL THEN null ELSE apoc.text.format("[%s](%s)", [typ.label, typ.short_form]) END) AS types + }} CALL {{ WITH primary OPTIONAL MATCH (primary)-[irw:in_register_with]->(template:Individual)-[:depicts]->(template_anat:Individual) OPTIONAL MATCH (primary)-[:is_specified_output_of]->(technique:Class) - WITH primary, template, template_anat, technique, irw + WITH template, template_anat, technique, irw LIMIT 1 RETURN template, template_anat, technique, irw }} @@ -7364,8 +7382,14 @@ def get_terms_for_pub(pub_short_form: str, return_dataframe=True, limit: int = - primary.short_form AS id, apoc.text.format("[%s](%s)", [primary.label, primary.short_form]) AS name, apoc.text.join(coalesce(primary.uniqueFacets, []), '|') AS tags, - REPLACE(apoc.text.format("[%s](%s)", [typ.label, typ.short_form]), '[null](null)', '') AS type, - 'Reference' AS reference_type, + apoc.text.join(types, '; ') AS type, + apoc.text.join( + apoc.coll.sort(apoc.coll.toSet( + [t IN ref_typs | CASE t WHEN 'def' THEN 'Definition' + WHEN 'syn' THEN 'Synonym' + WHEN 'nlp' THEN 'Text-mined' + ELSE 'Reference' END] + )), '; ') AS reference_type, 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, REPLACE(apoc.text.format("[![%s](%s '%s')](%s)", [coalesce(primary.label, 'image') + " aligned to " + CASE WHEN template_anat.symbol[0] <> '' THEN template_anat.symbol[0] ELSE template_anat.label END, REPLACE(REPLACE(COALESCE(irw.thumbnail[0], ''), 'thumbnailT.png', 'thumbnail.png'),'http://','https://'), coalesce(primary.label, 'image') + " aligned to " + CASE WHEN template_anat.symbol[0] <> '' THEN template_anat.symbol[0] ELSE template_anat.label END, template_anat.short_form + "," + primary.short_form]), "[![null]( 'null')](null)", "") AS thumbnail @@ -7399,7 +7423,7 @@ def get_terms_for_pub(pub_short_form: str, return_dataframe=True, limit: int = - 'name': 'first', 'tags': 'first', 'type': 'first', - 'reference_type': lambda s: '; '.join(sorted({x for x in s if x})), + 'reference_type': lambda s: '; '.join(sorted({p for x in s if x for p in x.split('; ')})), 'template': 'first', 'technique': 'first', 'thumbnail': 'first',