Skip to content

Commit eeacddb

Browse files
committed
Add class-level downstream and upstream connectivity queries
Implements VFB3 equivalent of the geppetto-vfb class connectivity queries. New schema functions: - DownstreamClassConnectivity_to_schema(): triggered for Class + Neuron terms - UpstreamClassConnectivity_to_schema(): triggered for Class + Neuron terms New implementation functions: - get_downstream_class_connectivity(short_form): reads Solr field 'downstream_connectivity_query', parses the JSON array of vfb_query objects and returns rows with columns: downstream_class, total_n, connected_n, percent_connected, pairwise_connections, total_weight, avg_weight - get_upstream_class_connectivity(short_form): reads Solr field 'upstream_connectivity_query', same columns but upstream_class Both functions are decorated with @with_solr_cache and support return_dataframe and limit parameters. Matching criteria in term_info_parse_object(): Implements VFB3 equivalent of the geppetto-vfb class connectivity queries. New schema functions: - DownstreamClassConnectivity_to_schema(): triggered for Class + Neuro New schema functions: - DownstreamClassConnectivity_to_schema(): triggerame- DownstreamClassConTh- UpstreamClassConnectivity_to_schema(): triggered for Class + Neuron terms d New implementation functions: - get_downstream_class_connectivity(short_ft b- get_downstream_class_conne
1 parent f1d06bf commit eeacddb

2 files changed

Lines changed: 215 additions & 2 deletions

File tree

src/vfbquery/solr_result_cache.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,7 @@ def wrapper(*args, **kwargs):
628628
'neurons_presynaptic', 'neurons_postsynaptic',
629629
'expression_overlaps_here', 'anatomy_scrnaseq', 'aligned_datasets', 'terms_for_pub',
630630
'individual_neuron_inputs', 'cluster_expression', 'expression_cluster', 'scrnaseq_dataset_data',
631-
'painted_domains']
631+
'painted_domains', 'downstream_class_connectivity_query', 'upstream_class_connectivity_query']
632632

633633
# For neuron_neuron_connectivity_query, only cache when all parameters are defaults
634634
if query_type == 'neuron_neuron_connectivity_query':
@@ -666,7 +666,8 @@ def wrapper(*args, **kwargs):
666666
'neuron_region_connectivity_query', 'instances', 'templates', 'images_neurons',
667667
'images_that_develop_from', 'expression_pattern_fragments', 'expression_overlaps_here',
668668
'anatomy_scrnaseq', 'aligned_datasets', 'terms_for_pub', 'individual_neuron_inputs',
669-
'cluster_expression', 'expression_cluster', 'scrnaseq_dataset_data', 'painted_domains']
669+
'cluster_expression', 'expression_cluster', 'scrnaseq_dataset_data', 'painted_domains',
670+
'downstream_class_connectivity_query', 'upstream_class_connectivity_query']
670671
if query_type in dataframe_query_types:
671672
return_dataframe = kwargs.get('return_dataframe', True) # Default is True
672673
cache_term_id = f"{cache_term_id}_dataframe_{return_dataframe}"

src/vfbquery/vfb_queries.py

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -935,6 +935,14 @@ def term_info_parse_object(results, short_form):
935935
q = TransgeneExpressionHere_to_schema(termInfo["Name"], {"short_form": vfbTerm.term.core.short_form})
936936
queries.append(q)
937937

938+
# Class connectivity queries — downstream and upstream partner neuron classes
939+
# Matches criteria: Class + Neuron (neuron class with indexed connectivity data)
940+
if termInfo["SuperTypes"] and contains_all_tags(termInfo["SuperTypes"], ["Class", "Neuron"]):
941+
q = DownstreamClassConnectivity_to_schema(termInfo["Name"], {"short_form": vfbTerm.term.core.short_form})
942+
queries.append(q)
943+
q = UpstreamClassConnectivity_to_schema(termInfo["Name"], {"short_form": vfbTerm.term.core.short_form})
944+
queries.append(q)
945+
938946
# For individuals that are painted domains of anatomical regions, add parent class queries
939947
if termInfo["IsIndividual"] and termInfo["Technique"] and any('computer' in t.lower() for t in termInfo["Technique"]):
940948
anatomical_classes = []
@@ -1434,6 +1442,44 @@ def NeuronRegionConnectivityQuery_to_schema(name, take_default):
14341442
return Query(query=query, label=label, function=function, takes=takes, preview=preview, preview_columns=preview_columns)
14351443

14361444

1445+
def DownstreamClassConnectivity_to_schema(name, take_default):
1446+
"""
1447+
Schema for downstream class connectivity query.
1448+
Shows which neuron classes receive synapses from this neuron class.
1449+
Matching criteria: Class + Neuron
1450+
Query chain: Solr downstream_connectivity_query field
1451+
"""
1452+
query = "DownstreamClassConnectivity"
1453+
label = f"Downstream connectivity classes for {name}"
1454+
function = "get_downstream_class_connectivity"
1455+
takes = {
1456+
"short_form": {"$and": ["Class", "Neuron"]},
1457+
"default": take_default,
1458+
}
1459+
preview = 5
1460+
preview_columns = ["downstream_class", "total_n", "connected_n", "percent_connected", "pairwise_connections", "total_weight", "avg_weight"]
1461+
return Query(query=query, label=label, function=function, takes=takes, preview=preview, preview_columns=preview_columns)
1462+
1463+
1464+
def UpstreamClassConnectivity_to_schema(name, take_default):
1465+
"""
1466+
Schema for upstream class connectivity query.
1467+
Shows which neuron classes send synapses to this neuron class.
1468+
Matching criteria: Class + Neuron
1469+
Query chain: Solr upstream_connectivity_query field
1470+
"""
1471+
query = "UpstreamClassConnectivity"
1472+
label = f"Upstream connectivity classes for {name}"
1473+
function = "get_upstream_class_connectivity"
1474+
takes = {
1475+
"short_form": {"$and": ["Class", "Neuron"]},
1476+
"default": take_default,
1477+
}
1478+
preview = 5
1479+
preview_columns = ["upstream_class", "total_n", "connected_n", "percent_connected", "pairwise_connections", "total_weight", "avg_weight"]
1480+
return Query(query=query, label=label, function=function, takes=takes, preview=preview, preview_columns=preview_columns)
1481+
1482+
14371483
def TractsNervesInnervatingHere_to_schema(name, take_default):
14381484
"""
14391485
Schema for TractsNervesInnervatingHere query.
@@ -2924,6 +2970,172 @@ def get_neuron_region_connectivity(short_form: str, return_dataframe=True, limit
29242970
}
29252971

29262972

2973+
@with_solr_cache('downstream_class_connectivity_query')
2974+
def get_downstream_class_connectivity(short_form: str, return_dataframe=True, limit: int = -1):
2975+
"""
2976+
Retrieves downstream connectivity classes for the specified neuron class.
2977+
2978+
Reads the downstream_connectivity_query Solr field, which contains a JSON array
2979+
of vfb_query-format objects populated by the neuron_downstream_connectivity_indexer.
2980+
Each element represents one (primary_class → downstream_class) connection summary.
2981+
2982+
Matching criteria: Class + Neuron
2983+
2984+
:param short_form: short form of the neuron class
2985+
:param return_dataframe: Returns pandas DataFrame if True, otherwise returns formatted dict
2986+
:param limit: maximum number of results to return (default -1, returns all results)
2987+
:return: Downstream partner neuron classes with connectivity statistics
2988+
"""
2989+
solr_field = 'downstream_connectivity_query'
2990+
try:
2991+
results = vfb_solr.search(f'id:{short_form}', fl=solr_field, rows=1)
2992+
except Exception as e:
2993+
print(f"Error querying Solr for downstream class connectivity: {e}")
2994+
if return_dataframe:
2995+
return pd.DataFrame()
2996+
return {'headers': {}, 'rows': [], 'count': 0}
2997+
2998+
if not results.hits or not results.docs or solr_field not in results.docs[0]:
2999+
if return_dataframe:
3000+
return pd.DataFrame()
3001+
return {'headers': {}, 'rows': [], 'count': 0}
3002+
3003+
raw = results.docs[0][solr_field]
3004+
field_json = raw[0] if isinstance(raw, list) else raw
3005+
try:
3006+
entries = json.loads(field_json)
3007+
except (json.JSONDecodeError, TypeError) as e:
3008+
print(f"Error parsing downstream_connectivity_query JSON for {short_form}: {e}")
3009+
if return_dataframe:
3010+
return pd.DataFrame()
3011+
return {'headers': {}, 'rows': [], 'count': 0}
3012+
3013+
if not isinstance(entries, list):
3014+
entries = [entries]
3015+
3016+
rows = []
3017+
for entry in entries:
3018+
cc = entry.get('class_connectivity', {})
3019+
obj = entry.get('object', {})
3020+
ds_id = obj.get('short_form', cc.get('downstream_class_id', ''))
3021+
ds_label = obj.get('label', cc.get('downstream_class', ''))
3022+
row = {
3023+
'id': ds_id,
3024+
'downstream_class': f"[{ds_label}]({ds_id})" if ds_id else ds_label,
3025+
'total_n': cc.get('total_upstream_count', ''),
3026+
'connected_n': cc.get('connected_upstream_count', ''),
3027+
'percent_connected': cc.get('percent_connected', ''),
3028+
'pairwise_connections': cc.get('pairwise_connections', ''),
3029+
'total_weight': cc.get('total_weight', ''),
3030+
'avg_weight': cc.get('average_weight', ''),
3031+
}
3032+
rows.append(row)
3033+
3034+
total_count = len(rows)
3035+
if limit != -1:
3036+
rows = rows[:limit]
3037+
3038+
if return_dataframe:
3039+
df = pd.DataFrame(rows)
3040+
df = encode_markdown_links(df, ['downstream_class'])
3041+
return df
3042+
3043+
headers = {
3044+
'id': {'title': 'ID', 'type': 'selection_id', 'order': -1},
3045+
'downstream_class': {'title': 'Downstream Class', 'type': 'markdown', 'order': 0},
3046+
'total_n': {'title': 'Total N', 'type': 'number', 'order': 1},
3047+
'connected_n': {'title': 'Connected N', 'type': 'number', 'order': 2},
3048+
'percent_connected': {'title': '% Connected', 'type': 'number', 'order': 3},
3049+
'pairwise_connections': {'title': 'Pairwise Connections', 'type': 'number', 'order': 4},
3050+
'total_weight': {'title': 'Total Weight', 'type': 'number', 'order': 5},
3051+
'avg_weight': {'title': 'Avg Weight', 'type': 'number', 'order': 6},
3052+
}
3053+
return {'headers': headers, 'rows': rows, 'count': total_count}
3054+
3055+
3056+
@with_solr_cache('upstream_class_connectivity_query')
3057+
def get_upstream_class_connectivity(short_form: str, return_dataframe=True, limit: int = -1):
3058+
"""
3059+
Retrieves upstream connectivity classes for the specified neuron class.
3060+
3061+
Reads the upstream_connectivity_query Solr field, which contains a JSON array
3062+
of vfb_query-format objects populated by the neuron_upstream_connectivity_indexer.
3063+
Each element represents one (upstream_class → primary_class) connection summary.
3064+
3065+
Matching criteria: Class + Neuron
3066+
3067+
:param short_form: short form of the neuron class
3068+
:param return_dataframe: Returns pandas DataFrame if True, otherwise returns formatted dict
3069+
:param limit: maximum number of results to return (default -1, returns all results)
3070+
:return: Upstream partner neuron classes with connectivity statistics
3071+
"""
3072+
solr_field = 'upstream_connectivity_query'
3073+
try:
3074+
results = vfb_solr.search(f'id:{short_form}', fl=solr_field, rows=1)
3075+
except Exception as e:
3076+
print(f"Error querying Solr for upstream class connectivity: {e}")
3077+
if return_dataframe:
3078+
return pd.DataFrame()
3079+
return {'headers': {}, 'rows': [], 'count': 0}
3080+
3081+
if not results.hits or not results.docs or solr_field not in results.docs[0]:
3082+
if return_dataframe:
3083+
return pd.DataFrame()
3084+
return {'headers': {}, 'rows': [], 'count': 0}
3085+
3086+
raw = results.docs[0][solr_field]
3087+
field_json = raw[0] if isinstance(raw, list) else raw
3088+
try:
3089+
entries = json.loads(field_json)
3090+
except (json.JSONDecodeError, TypeError) as e:
3091+
print(f"Error parsing upstream_connectivity_query JSON for {short_form}: {e}")
3092+
if return_dataframe:
3093+
return pd.DataFrame()
3094+
return {'headers': {}, 'rows': [], 'count': 0}
3095+
3096+
if not isinstance(entries, list):
3097+
entries = [entries]
3098+
3099+
rows = []
3100+
for entry in entries:
3101+
cc = entry.get('class_connectivity', {})
3102+
obj = entry.get('object', {})
3103+
us_id = obj.get('short_form', cc.get('upstream_class_id', ''))
3104+
us_label = obj.get('label', cc.get('upstream_class', ''))
3105+
row = {
3106+
'id': us_id,
3107+
'upstream_class': f"[{us_label}]({us_id})" if us_id else us_label,
3108+
'total_n': cc.get('total_upstream_count', ''),
3109+
'connected_n': cc.get('connected_upstream_count', ''),
3110+
'percent_connected': cc.get('percent_connected', ''),
3111+
'pairwise_connections': cc.get('pairwise_connections', ''),
3112+
'total_weight': cc.get('total_weight', ''),
3113+
'avg_weight': cc.get('average_weight', ''),
3114+
}
3115+
rows.append(row)
3116+
3117+
total_count = len(rows)
3118+
if limit != -1:
3119+
rows = rows[:limit]
3120+
3121+
if return_dataframe:
3122+
df = pd.DataFrame(rows)
3123+
df = encode_markdown_links(df, ['upstream_class'])
3124+
return df
3125+
3126+
headers = {
3127+
'id': {'title': 'ID', 'type': 'selection_id', 'order': -1},
3128+
'upstream_class': {'title': 'Upstream Class', 'type': 'markdown', 'order': 0},
3129+
'total_n': {'title': 'Total N', 'type': 'number', 'order': 1},
3130+
'connected_n': {'title': 'Connected N', 'type': 'number', 'order': 2},
3131+
'percent_connected': {'title': '% Connected', 'type': 'number', 'order': 3},
3132+
'pairwise_connections': {'title': 'Pairwise Connections', 'type': 'number', 'order': 4},
3133+
'total_weight': {'title': 'Total Weight', 'type': 'number', 'order': 5},
3134+
'avg_weight': {'title': 'Avg Weight', 'type': 'number', 'order': 6},
3135+
}
3136+
return {'headers': headers, 'rows': rows, 'count': total_count}
3137+
3138+
29273139
@with_solr_cache('images_neurons')
29283140
def get_images_neurons(short_form: str, return_dataframe=True, limit: int = -1):
29293141
"""

0 commit comments

Comments
 (0)