@@ -888,12 +888,21 @@ def term_info_parse_object(results, short_form):
888888 if termInfo ["SuperTypes" ] and contains_all_tags (termInfo ["SuperTypes" ], ["Class" , "Expression_pattern" ]):
889889 q = epFrag_to_schema (termInfo ["Name" ], {"short_form" : vfbTerm .term .core .short_form })
890890 queries .append (q )
891-
892- # ExpressionOverlapsHere query - for anatomical regions
893- # Matches XMI criteria: Class + Anatomy
894- # Returns expression patterns that overlap with the anatomical region
895- if termInfo ["SuperTypes" ] and contains_all_tags (termInfo ["SuperTypes" ], ["Class" , "Anatomy" ]):
896- q = ExpressionOverlapsHere_to_schema (termInfo ["Name" ], {"short_form" : vfbTerm .term .core .short_form })
891+
892+ # AnatomyExpressedIn query - for expression patterns / fragments
893+ # Matches XMI criteria: Class + Expression_pattern OR
894+ # Class + Expression_pattern_fragment
895+ # Returns anatomy classes where this expression pattern is expressed.
896+ # Renamed from ExpressionOverlapsHere in v1.13.7 — the legacy emit
897+ # below (Class+Anatomy) was a misdirection: that path was offering
898+ # an inverse-direction query to anatomy entities, where it returned
899+ # zero. The forward-direction "transgene expression here" lookup
900+ # for anatomy entities is owned by TransgeneExpressionHere.
901+ if termInfo ["SuperTypes" ] and (
902+ contains_all_tags (termInfo ["SuperTypes" ], ["Class" , "Expression_pattern" ]) or
903+ contains_all_tags (termInfo ["SuperTypes" ], ["Class" , "Expression_pattern_fragment" ])
904+ ):
905+ q = AnatomyExpressedIn_to_schema (termInfo ["Name" ], {"short_form" : vfbTerm .term .core .short_form })
897906 queries .append (q )
898907
899908 # anatScRNAseqQuery query - for anatomical regions with scRNAseq data
@@ -1113,9 +1122,11 @@ def term_info_parse_object(results, short_form):
11131122 q = ImagesNeurons_to_schema (parent_label , {"short_form" : parent_short_form })
11141123 queries .append (q )
11151124
1116- if "Anatomy" in parent .types :
1117- # ExpressionOverlapsHere query
1118- q = ExpressionOverlapsHere_to_schema (parent_label , {"short_form" : parent_short_form })
1125+ if "Expression_pattern" in parent .types or "Expression_pattern_fragment" in parent .types :
1126+ # AnatomyExpressedIn query (renamed from ExpressionOverlapsHere
1127+ # in v1.13.7 — the previous emit gated on "Anatomy" was
1128+ # forward-direction and a misdirection for this query).
1129+ q = AnatomyExpressedIn_to_schema (parent_label , {"short_form" : parent_short_form })
11191130 queries .append (q )
11201131
11211132 if "Anatomy" in parent .types and "hasScRNAseq" in parent .types :
@@ -1740,25 +1751,40 @@ def epFrag_to_schema(name, take_default):
17401751 return Query (query = query , label = label , function = function , takes = takes , preview = preview , preview_columns = preview_columns )
17411752
17421753
1743- def ExpressionOverlapsHere_to_schema (name , take_default ):
1754+ def AnatomyExpressedIn_to_schema (name , take_default ):
17441755 """
1745- Schema for ExpressionOverlapsHere query.
1746- Finds expression patterns that overlap with a specified anatomical region.
1747-
1756+ Schema for AnatomyExpressedIn query (renamed from ExpressionOverlapsHere
1757+ in v1.13.7 to reflect its actual inverse-direction semantics).
1758+
1759+ Given an expression pattern, returns the anatomy classes in which the
1760+ pattern's Individuals overlap or are part_of anatomy Individuals.
1761+
17481762 XMI Source: https://raw.githubusercontent.com/VirtualFlyBrain/geppetto-vfb/master/model/vfb.xmi
1749-
1763+
17501764 Matching criteria from XMI:
1751- - Class + Anatomy
1752-
1753- Query chain: Neo4j anat_2_ep_query → process
1754- Cypher query: MATCH (ep:Class:Expression_pattern)<-[ar:overlaps|part_of]-(anoni:Individual)-[:INSTANCEOF]->(anat:Class)
1755- WHERE anat.short_form = $id
1765+ - Class + Expression_pattern
1766+ - Class + Expression_pattern_fragment
1767+
1768+ Cypher query:
1769+ MATCH (ep:Class:Expression_pattern)
1770+ <-[ar:overlaps|part_of]-(anoni:Individual)
1771+ -[:INSTANCEOF]->(anat:Class:Anatomy)
1772+ WHERE ep.short_form = $id
1773+
1774+ Backward compat: the legacy `ExpressionOverlapsHere` query_type is
1775+ still accepted by ha_api.QUERY_TYPE_MAP and dispatches to the same
1776+ underlying function — pre-existing bookmarked URLs continue to work.
17561777 """
1757- query = "ExpressionOverlapsHere "
1758- label = f"Expression patterns overlapping { name } "
1778+ query = "AnatomyExpressedIn "
1779+ label = f"Anatomy where { name } is expressed "
17591780 function = "get_expression_overlaps_here"
17601781 takes = {
1761- "short_form" : {"$and" : ["Class" , "Anatomy" ]},
1782+ "short_form" : {
1783+ "$or" : [
1784+ {"$and" : ["Class" , "Expression_pattern" ]},
1785+ {"$and" : ["Class" , "Expression_pattern_fragment" ]},
1786+ ]
1787+ },
17621788 "default" : take_default ,
17631789 }
17641790 preview = 5
@@ -1767,6 +1793,11 @@ def ExpressionOverlapsHere_to_schema(name, take_default):
17671793 return Query (query = query , label = label , function = function , takes = takes , preview = preview , preview_columns = preview_columns )
17681794
17691795
1796+ # Deprecated alias — kept so any direct importer of the old name keeps
1797+ # working. New code should call AnatomyExpressedIn_to_schema directly.
1798+ ExpressionOverlapsHere_to_schema = AnatomyExpressedIn_to_schema
1799+
1800+
17701801def anatScRNAseqQuery_to_schema (name , take_default ):
17711802 """
17721803 Schema for anatScRNAseqQuery query.
@@ -2764,86 +2795,90 @@ def get_individual_neuron_inputs(neuron_short_form: str, return_dataframe=True,
27642795 return results
27652796
27662797
2767- def get_expression_overlaps_here (anatomy_short_form : str , return_dataframe = True , limit : int = - 1 ):
2768- """
2769- Retrieve expression patterns that overlap with the specified anatomical region.
2770-
2771- This implements the ExpressionOverlapsHere query from the VFB XMI specification.
2772- Finds expression patterns where individual instances overlap with or are part of the anatomy.
2773-
2774- :param anatomy_short_form: Short form identifier of the anatomical region (e.g., 'FBbt_00003982')
2775- :param return_dataframe: Returns pandas DataFrame if True, otherwise returns formatted dict (default: True)
2776- :param limit: Maximum number of results to return (default: -1 for all results)
2777- :return: Expression patterns with overlap relationships, publications, and images
2778- :rtype: pandas.DataFrame or dict
2798+ def get_expression_overlaps_here (expression_pattern_short_form : str , return_dataframe = True , limit : int = - 1 ):
2799+ """Anatomy classes overlapped by the specified expression pattern.
2800+
2801+ INVERSE direction of TransgeneExpressionHere — given an expression
2802+ pattern, return the anatomy classes whose Individuals are overlapped
2803+ by (or part_of) the expression pattern's Individuals. Matches the
2804+ XMI ExpressionOverlapsHere CompoundRefQuery's description
2805+ ("Anatomy $NAME is expressed in") and its matchingCriteria
2806+ (Class + Expression_pattern).
2807+
2808+ Up to v1.13.5 this function shipped as the FORWARD direction
2809+ (anatomy -> expression patterns), duplicating
2810+ get_transgene_expression_here exactly and returning 0 for any actual
2811+ expression pattern input — a migration regression from the legacy
2812+ XMI which had a separate inverse query "Query for anatomy from
2813+ expression" wired in dataSources[0]. v1.13.6 flips this function to
2814+ the inverse semantics so v2's ExpressionOverlapsHere on an expression
2815+ pattern (e.g. VFBexp_FBtp0001321 P{GAL4-per.BS}) returns the 50+
2816+ anatomy classes where the pattern is expressed.
2817+
2818+ Column shape is unchanged (id / name / tags / pubs) so v2's Geppetto
2819+ processor renders the table identically — only the column meaning
2820+ flips: id is now the anatomy short_form, name is the anatomy label.
2821+
2822+ :param expression_pattern_short_form: short_form of an
2823+ Expression_pattern Class (e.g. 'VFBexp_FBtp0001321')
2824+ :param return_dataframe: pandas DataFrame if True else formatted dict
2825+ :param limit: -1 for all results, otherwise cap on row count
27792826 """
2780-
2781- # Count query: count distinct expression patterns
27822827 count_query = f"""
2783- MATCH (ep:Class:Expression_pattern)<-[ar:overlaps|part_of]-(anoni:Individual)-[:INSTANCEOF]->(anat:Class)
2784- WHERE anat .short_form = '{ anatomy_short_form } '
2785- RETURN COUNT(DISTINCT ep ) AS total_count
2828+ MATCH (ep:Class:Expression_pattern)<-[ar:overlaps|part_of]-(anoni:Individual)-[:INSTANCEOF]->(anat:Class:Anatomy )
2829+ WHERE ep .short_form = '{ expression_pattern_short_form } '
2830+ RETURN COUNT(DISTINCT anat ) AS total_count
27862831 """
2787-
2832+
27882833 count_results = vc .nc .commit_list ([count_query ])
27892834 count_df = pd .DataFrame .from_records (get_dict_cursor ()(count_results ))
27902835 total_count = count_df ['total_count' ][0 ] if not count_df .empty else 0
2791-
2792- # Main query: get expression patterns with details
2836+
27932837 main_query = f"""
2794- MATCH (ep:Class:Expression_pattern)<-[ar:overlaps|part_of]-(anoni:Individual)-[:INSTANCEOF]->(anat:Class)
2795- WHERE anat .short_form = '{ anatomy_short_form } '
2838+ MATCH (ep:Class:Expression_pattern)<-[ar:overlaps|part_of]-(anoni:Individual)-[:INSTANCEOF]->(anat:Class:Anatomy )
2839+ WHERE ep .short_form = '{ expression_pattern_short_form } '
27962840 WITH DISTINCT collect(DISTINCT ar.pub[0]) as pubs, anat, ep
27972841 UNWIND pubs as p
27982842 OPTIONAL MATCH (pub:pub {{ short_form: p}})
2799- WITH anat, ep, collect({{
2800- core: {{ short_form: pub.short_form, label: coalesce(pub.label,''), iri: pub.iri, types: labels(pub), symbol: coalesce(pub.symbol[0], '') }},
2801- PubMed: coalesce(pub.PMID[0], ''),
2802- FlyBase: coalesce(([]+pub.FlyBase)[0], ''),
2803- DOI: coalesce(pub.DOI[0], '')
2843+ WITH anat, ep, collect({{
2844+ core: {{ short_form: pub.short_form, label: coalesce(pub.label,''), iri: pub.iri, types: labels(pub), symbol: coalesce(pub.symbol[0], '') }},
2845+ PubMed: coalesce(pub.PMID[0], ''),
2846+ FlyBase: coalesce(([]+pub.FlyBase)[0], ''),
2847+ DOI: coalesce(pub.DOI[0], '')
28042848 }}) as pubs
2805- RETURN
2806- ep .short_form AS id,
2807- apoc.text.format("[%s](%s)", [ep .label, ep .short_form]) AS name,
2808- apoc.text.join(ep .uniqueFacets, '|') AS tags,
2849+ RETURN
2850+ anat .short_form AS id,
2851+ apoc.text.format("[%s](%s)", [anat .label, anat .short_form]) AS name,
2852+ apoc.text.join(coalesce(anat .uniqueFacets, []) , '|') AS tags,
28092853 pubs
2810- ORDER BY ep .label
2854+ ORDER BY anat .label
28112855 """
2812-
2856+
28132857 if limit != - 1 :
28142858 main_query += f" LIMIT { limit } "
2815-
2816- # Execute the query
2859+
28172860 results = vc .nc .commit_list ([main_query ])
2818-
2819- # Convert to DataFrame
28202861 df = pd .DataFrame .from_records (get_dict_cursor ()(results ))
2821-
2822- # Encode markdown links
2862+
28232863 if not df .empty :
2824- columns_to_encode = ['name' ]
2825- df = encode_markdown_links (df , columns_to_encode )
2826-
2864+ df = encode_markdown_links (df , ['name' ])
2865+
28272866 if return_dataframe :
28282867 return df
2829- else :
2830- formatted_results = {
2831- "headers" : {
2832- "id" : {"title" : "ID" , "type" : "selection_id" , "order" : - 1 },
2833- "name" : {"title" : "Expression Pattern" , "type" : "markdown" , "order" : 0 },
2834- "tags" : {"title" : "Tags" , "type" : "tags" , "order" : 1 },
2835- "pubs" : {"title" : "Publications" , "type" : "metadata" , "order" : 2 }
2836- },
2837- "rows" : [
2838- {
2839- key : row [key ]
2840- for key in ["id" , "name" , "tags" , "pubs" ]
2841- }
2842- for row in safe_to_dict (df , sort_by_id = False )
2843- ],
2844- "count" : total_count
2845- }
2846- return formatted_results
2868+
2869+ return {
2870+ "headers" : {
2871+ "id" : {"title" : "ID" , "type" : "selection_id" , "order" : - 1 },
2872+ "name" : {"title" : "Anatomy" , "type" : "markdown" , "order" : 0 },
2873+ "tags" : {"title" : "Tags" , "type" : "tags" , "order" : 1 },
2874+ "pubs" : {"title" : "Publications" , "type" : "metadata" , "order" : 2 },
2875+ },
2876+ "rows" : [
2877+ {key : row [key ] for key in ["id" , "name" , "tags" , "pubs" ]}
2878+ for row in safe_to_dict (df , sort_by_id = False )
2879+ ],
2880+ "count" : total_count ,
2881+ }
28472882
28482883
28492884def contains_all_tags (lst : List [str ], tags : List [str ]) -> bool :
0 commit comments