bmtool 0.7.2__py3-none-any.whl → 0.7.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bmtool/bmplot/connections.py +156 -88
- bmtool/util/util.py +361 -87
- {bmtool-0.7.2.dist-info → bmtool-0.7.3.dist-info}/METADATA +1 -1
- {bmtool-0.7.2.dist-info → bmtool-0.7.3.dist-info}/RECORD +8 -8
- {bmtool-0.7.2.dist-info → bmtool-0.7.3.dist-info}/WHEEL +0 -0
- {bmtool-0.7.2.dist-info → bmtool-0.7.3.dist-info}/entry_points.txt +0 -0
- {bmtool-0.7.2.dist-info → bmtool-0.7.3.dist-info}/licenses/LICENSE +0 -0
- {bmtool-0.7.2.dist-info → bmtool-0.7.3.dist-info}/top_level.txt +0 -0
bmtool/bmplot/connections.py
CHANGED
@@ -12,7 +12,6 @@ import matplotlib.pyplot as plt
|
|
12
12
|
import numpy as np
|
13
13
|
import pandas as pd
|
14
14
|
from IPython import get_ipython
|
15
|
-
from neuron import h
|
16
15
|
|
17
16
|
from ..util import util
|
18
17
|
|
@@ -133,6 +132,7 @@ def total_connection_matrix(
|
|
133
132
|
title = "All Synapse .mod Files Used"
|
134
133
|
if synaptic_info == "3":
|
135
134
|
title = "All Synapse .json Files Used"
|
135
|
+
|
136
136
|
plot_connection_info(
|
137
137
|
text, num, source_labels, target_labels, title, syn_info=synaptic_info, save_file=save_file
|
138
138
|
)
|
@@ -981,104 +981,172 @@ def distance_delay_plot(
|
|
981
981
|
plt.show()
|
982
982
|
|
983
983
|
|
984
|
-
def
|
984
|
+
def plot_synapse_location(config: str, source: str, target: str, sids: str, tids: str) -> tuple:
|
985
985
|
"""
|
986
|
-
|
987
|
-
|
988
|
-
target_model: the name of the model_template used when building the BMTK node
|
989
|
-
source: The source BMTK network
|
990
|
-
target: The target BMTK network
|
991
|
-
"""
|
992
|
-
# Load mechanisms and template
|
993
|
-
|
994
|
-
util.load_templates_from_config(config)
|
995
|
-
|
996
|
-
# Load node and edge data
|
997
|
-
nodes, edges = util.load_nodes_edges_from_config(config)
|
998
|
-
nodes = nodes[source]
|
999
|
-
edges = edges[f"{source}_to_{target}"]
|
1000
|
-
|
1001
|
-
# Map target_node_id to model_template
|
1002
|
-
edges["target_model_template"] = edges["target_node_id"].map(nodes["model_template"])
|
1003
|
-
|
1004
|
-
# Map source_node_id to pop_name
|
1005
|
-
edges["source_pop_name"] = edges["source_node_id"].map(nodes["pop_name"])
|
1006
|
-
|
1007
|
-
edges = edges[edges["target_model_template"] == target_model]
|
1008
|
-
|
1009
|
-
# Create the cell model from target model
|
1010
|
-
cell = getattr(h, target_model.split(":")[1])()
|
1011
|
-
|
1012
|
-
# Create a mapping from section index to section name
|
1013
|
-
section_id_to_name = {}
|
1014
|
-
for idx, sec in enumerate(cell.all):
|
1015
|
-
section_id_to_name[idx] = sec.name()
|
1016
|
-
|
1017
|
-
# Add a new column with section names based on afferent_section_id
|
1018
|
-
edges["afferent_section_name"] = edges["afferent_section_id"].map(section_id_to_name)
|
1019
|
-
|
1020
|
-
# Get unique sections and source populations
|
1021
|
-
unique_pops = edges["source_pop_name"].unique()
|
1022
|
-
|
1023
|
-
# Filter to only include sections with data
|
1024
|
-
section_counts = edges["afferent_section_name"].value_counts()
|
1025
|
-
sections_with_data = section_counts[section_counts > 0].index.tolist()
|
1026
|
-
|
1027
|
-
# Create a figure with subplots for each section
|
1028
|
-
plt.figure(figsize=(8, 12))
|
986
|
+
Generates a connectivity matrix showing synaptic distribution across different cell sections.
|
987
|
+
Note does exclude gap junctions since they dont have an afferent id stored in the h5 file!
|
1029
988
|
|
1030
|
-
|
1031
|
-
|
1032
|
-
|
989
|
+
Parameters
|
990
|
+
----------
|
991
|
+
config : str
|
992
|
+
Path to BMTK config file
|
993
|
+
source : str
|
994
|
+
The source BMTK network name
|
995
|
+
target : str
|
996
|
+
The target BMTK network name
|
997
|
+
sids : str
|
998
|
+
Column name in nodes file containing source population identifiers
|
999
|
+
tids : str
|
1000
|
+
Column name in nodes file containing target population identifiers
|
1001
|
+
|
1002
|
+
Returns
|
1003
|
+
-------
|
1004
|
+
tuple
|
1005
|
+
(matplotlib.figure.Figure, matplotlib.axes.Axes) containing the plot
|
1006
|
+
|
1007
|
+
Raises
|
1008
|
+
------
|
1009
|
+
ValueError
|
1010
|
+
If required parameters are missing or invalid
|
1011
|
+
RuntimeError
|
1012
|
+
If template loading or cell instantiation fails
|
1013
|
+
"""
|
1014
|
+
import matplotlib.pyplot as plt
|
1015
|
+
import numpy as np
|
1016
|
+
from neuron import h
|
1017
|
+
|
1018
|
+
# Validate inputs
|
1019
|
+
if not all([config, source, target, sids, tids]):
|
1020
|
+
raise ValueError(
|
1021
|
+
"Missing required parameters: config, source, target, sids, and tids must be provided"
|
1022
|
+
)
|
1033
1023
|
|
1034
|
-
|
1035
|
-
|
1036
|
-
|
1024
|
+
try:
|
1025
|
+
# Load mechanisms and template
|
1026
|
+
util.load_templates_from_config(config)
|
1027
|
+
except Exception as e:
|
1028
|
+
raise RuntimeError(f"Failed to load templates from config: {str(e)}")
|
1037
1029
|
|
1038
|
-
|
1039
|
-
|
1030
|
+
try:
|
1031
|
+
# Load node and edge data
|
1032
|
+
nodes, edges = util.load_nodes_edges_from_config(config)
|
1033
|
+
if source not in nodes or f"{source}_to_{target}" not in edges:
|
1034
|
+
raise ValueError(f"Source '{source}' or target '{target}' networks not found in data")
|
1035
|
+
|
1036
|
+
target_nodes = nodes[target]
|
1037
|
+
source_nodes = nodes[source]
|
1038
|
+
edges = edges[f"{source}_to_{target}"]
|
1039
|
+
# Find edges with NaN afferent_section_id
|
1040
|
+
nan_edges = edges[edges["afferent_section_id"].isna()]
|
1041
|
+
# Print information about removed edges
|
1042
|
+
if not nan_edges.empty:
|
1043
|
+
unique_indices = sorted(list(set(nan_edges.index.tolist())))
|
1044
|
+
print(f"Removing {len(nan_edges)} edges with missing afferent_section_id")
|
1045
|
+
print(f"Unique indices removed: {unique_indices}")
|
1046
|
+
|
1047
|
+
# Filter out edges with NaN afferent_section_id
|
1048
|
+
edges = edges[edges["afferent_section_id"].notna()]
|
1049
|
+
|
1050
|
+
|
1051
|
+
except Exception as e:
|
1052
|
+
raise RuntimeError(f"Failed to load nodes and edges: {str(e)}")
|
1053
|
+
|
1054
|
+
# Map identifiers while checking for missing values
|
1055
|
+
edges["target_model_template"] = edges["target_node_id"].map(target_nodes["model_template"])
|
1056
|
+
edges["target_pop_name"] = edges["target_node_id"].map(target_nodes[tids])
|
1057
|
+
edges["source_pop_name"] = edges["source_node_id"].map(source_nodes[sids])
|
1058
|
+
|
1059
|
+
if edges["target_model_template"].isnull().any():
|
1060
|
+
print("Warning: Some target nodes missing model template")
|
1061
|
+
if edges["target_pop_name"].isnull().any():
|
1062
|
+
print("Warning: Some target nodes missing population name")
|
1063
|
+
if edges["source_pop_name"].isnull().any():
|
1064
|
+
print("Warning: Some source nodes missing population name")
|
1065
|
+
|
1066
|
+
# Get unique populations
|
1067
|
+
source_pops = edges["source_pop_name"].unique()
|
1068
|
+
target_pops = edges["target_pop_name"].unique()
|
1069
|
+
|
1070
|
+
# Initialize matrices
|
1071
|
+
num_connections = np.zeros((len(source_pops), len(target_pops)))
|
1072
|
+
text_data = np.empty((len(source_pops), len(target_pops)), dtype=object)
|
1073
|
+
|
1074
|
+
# Create mappings for indices
|
1075
|
+
source_pop_to_idx = {pop: idx for idx, pop in enumerate(source_pops)}
|
1076
|
+
target_pop_to_idx = {pop: idx for idx, pop in enumerate(target_pops)}
|
1077
|
+
|
1078
|
+
# Cache for section mappings to avoid recreating cells
|
1079
|
+
section_mappings = {}
|
1080
|
+
|
1081
|
+
# Calculate connectivity statistics
|
1082
|
+
for source_pop in source_pops:
|
1083
|
+
for target_pop in target_pops:
|
1084
|
+
# Filter edges for this source-target pair
|
1085
|
+
filtered_edges = edges[
|
1086
|
+
(edges["source_pop_name"] == source_pop) & (edges["target_pop_name"] == target_pop)
|
1087
|
+
]
|
1040
1088
|
|
1041
|
-
|
1042
|
-
|
1043
|
-
if len(pop_group) > 0:
|
1044
|
-
ax.hist(
|
1045
|
-
pop_group["afferent_section_pos"],
|
1046
|
-
bins=15,
|
1047
|
-
alpha=0.7,
|
1048
|
-
label=pop_name,
|
1049
|
-
color=pop_colors[pop_name],
|
1050
|
-
)
|
1089
|
+
source_idx = source_pop_to_idx[source_pop]
|
1090
|
+
target_idx = target_pop_to_idx[target_pop]
|
1051
1091
|
|
1052
|
-
|
1053
|
-
|
1054
|
-
|
1055
|
-
|
1056
|
-
ax.tick_params(labelsize=7)
|
1057
|
-
ax.grid(True, alpha=0.3)
|
1092
|
+
if len(filtered_edges) == 0:
|
1093
|
+
num_connections[source_idx, target_idx] = 0
|
1094
|
+
text_data[source_idx, target_idx] = "No connections"
|
1095
|
+
continue
|
1058
1096
|
|
1059
|
-
|
1060
|
-
|
1061
|
-
ax.legend(fontsize=8)
|
1097
|
+
total_connections = len(filtered_edges)
|
1098
|
+
target_model_template = filtered_edges["target_model_template"].iloc[0]
|
1062
1099
|
|
1063
|
-
|
1064
|
-
|
1065
|
-
|
1100
|
+
try:
|
1101
|
+
# Get or create section mapping for this model
|
1102
|
+
if target_model_template not in section_mappings:
|
1103
|
+
cell_class_name = (
|
1104
|
+
target_model_template.split(":")[1]
|
1105
|
+
if ":" in target_model_template
|
1106
|
+
else target_model_template
|
1107
|
+
)
|
1108
|
+
cell = getattr(h, cell_class_name)()
|
1109
|
+
|
1110
|
+
# Create section mapping
|
1111
|
+
section_mapping = {}
|
1112
|
+
for idx, sec in enumerate(cell.all):
|
1113
|
+
section_mapping[idx] = sec.name().split(".")[-1] # Clean name
|
1114
|
+
section_mappings[target_model_template] = section_mapping
|
1115
|
+
|
1116
|
+
section_mapping = section_mappings[target_model_template]
|
1117
|
+
|
1118
|
+
# Calculate section distribution
|
1119
|
+
section_counts = filtered_edges["afferent_section_id"].value_counts()
|
1120
|
+
section_percentages = (section_counts / total_connections * 100).round(1)
|
1121
|
+
|
1122
|
+
# Format section distribution text - show all sections
|
1123
|
+
section_display = []
|
1124
|
+
for section_id, percentage in section_percentages.items():
|
1125
|
+
section_name = section_mapping.get(section_id, f"sec_{section_id}")
|
1126
|
+
section_display.append(f"{section_name}:{percentage}%")
|
1127
|
+
|
1128
|
+
num_connections[source_idx, target_idx] = total_connections
|
1129
|
+
text_data[source_idx, target_idx] = "\n".join(section_display)
|
1130
|
+
|
1131
|
+
except Exception as e:
|
1132
|
+
print(f"Warning: Error processing {target_model_template}: {str(e)}")
|
1133
|
+
num_connections[source_idx, target_idx] = total_connections
|
1134
|
+
text_data[source_idx, target_idx] = "Section info N/A"
|
1135
|
+
|
1136
|
+
# Create the plot
|
1137
|
+
title = f"Synaptic Distribution by Section: {source} to {target}"
|
1138
|
+
fig, ax = plot_connection_info(
|
1139
|
+
text=text_data,
|
1140
|
+
num=num_connections,
|
1141
|
+
source_labels=list(source_pops),
|
1142
|
+
target_labels=list(target_pops),
|
1143
|
+
title=title,
|
1144
|
+
syn_info="1",
|
1066
1145
|
)
|
1067
|
-
if is_notebook:
|
1146
|
+
if is_notebook():
|
1068
1147
|
plt.show()
|
1069
1148
|
else:
|
1070
|
-
|
1071
|
-
|
1072
|
-
# Create a summary table
|
1073
|
-
print("Summary of connections by section and source population:")
|
1074
|
-
pivot_table = edges.pivot_table(
|
1075
|
-
values="afferent_section_id",
|
1076
|
-
index="afferent_section_name",
|
1077
|
-
columns="source_pop_name",
|
1078
|
-
aggfunc="count",
|
1079
|
-
fill_value=0,
|
1080
|
-
)
|
1081
|
-
print(pivot_table)
|
1149
|
+
return fig, ax
|
1082
1150
|
|
1083
1151
|
|
1084
1152
|
def plot_connection_info(
|
bmtool/util/util.py
CHANGED
@@ -16,6 +16,9 @@ import numpy as np
|
|
16
16
|
import pandas as pd
|
17
17
|
from neuron import h
|
18
18
|
|
19
|
+
from typing import Dict, List, Optional, Union, Any
|
20
|
+
from pathlib import Path
|
21
|
+
|
19
22
|
# from bmtk.utils.io.cell_vars import CellVarsFile
|
20
23
|
# from bmtk.analyzer.cell_vars import _get_cell_report
|
21
24
|
# from bmtk.analyzer.io_tools import load_config
|
@@ -345,95 +348,366 @@ def load_edges(edges_file, edge_types_file):
|
|
345
348
|
return edges # return (population, edges_df)
|
346
349
|
|
347
350
|
|
348
|
-
def load_edges_from_paths(
|
351
|
+
def load_edges_from_paths(
|
352
|
+
file_paths: str,
|
353
|
+
verbose: bool = False
|
354
|
+
) -> Dict[str, pd.DataFrame]:
|
349
355
|
"""
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
356
|
+
Load multiple SONATA edge files into a dictionary of DataFrames.
|
357
|
+
|
358
|
+
This function reads a configuration file containing multiple edge file pairs
|
359
|
+
(CSV + H5) and loads each pair into a separate DataFrame. Each DataFrame
|
360
|
+
contains merged edge connectivity and metadata information.
|
361
|
+
|
362
|
+
Parameters:
|
363
|
+
-----------
|
364
|
+
file_paths str
|
365
|
+
Expected structure: [{'edge_types_file': ..., 'edges_file': ...}, ...]
|
366
|
+
verbose : bool, default False
|
367
|
+
If True, print detailed information about the loading process
|
368
|
+
|
369
|
+
Returns:
|
370
|
+
--------
|
371
|
+
Dict[str, pd.DataFrame]
|
372
|
+
Dictionary mapping edge dataset names to DataFrames.
|
373
|
+
Keys are derived from CSV filenames (without '_edge_types.csv' suffix).
|
374
|
+
Values are DataFrames containing merged edge data.
|
375
|
+
|
376
|
+
Notes:
|
377
|
+
------
|
378
|
+
- If loading fails for any dataset, an empty DataFrame is stored for that key
|
379
|
+
- Dataset names are extracted from CSV filenames for readability
|
380
|
+
- All edge data follows SONATA format specifications
|
360
381
|
"""
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
with h5py.File(
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
382
|
+
|
383
|
+
# Load configuration and extract edge file information
|
384
|
+
|
385
|
+
edge_files_list = file_paths
|
386
|
+
|
387
|
+
if verbose:
|
388
|
+
print(f"Loading {len(edge_files_list)} edge datasets from config: {config}")
|
389
|
+
|
390
|
+
edges_dict: Dict[str, pd.DataFrame] = {}
|
391
|
+
|
392
|
+
# Process each edge file pair
|
393
|
+
for i, file_info in enumerate(edge_files_list):
|
394
|
+
csv_path = file_info['edge_types_file']
|
395
|
+
h5_path = file_info['edges_file']
|
396
|
+
|
397
|
+
# Generate meaningful key from h5 file
|
398
|
+
with h5py.File(h5_path, "r") as connections_h5:
|
399
|
+
key = list(connections_h5["/edges"])[0]
|
400
|
+
|
401
|
+
if verbose:
|
402
|
+
print(f"\n{'='*60}")
|
403
|
+
print(f"Loading edge set {i+1}/{len(edge_files_list)}: {key}")
|
404
|
+
print(f"{'='*60}")
|
405
|
+
|
406
|
+
try:
|
407
|
+
# Load individual edge dataset
|
408
|
+
df = load_sonata_edges_to_dataframe(csv_path, h5_path, verbose=verbose)
|
409
|
+
edges_dict[key] = df
|
410
|
+
|
411
|
+
if verbose:
|
412
|
+
print(f"\nSuccessfully loaded {key}: {df.shape[0]} edges, {df.shape[1]} columns")
|
413
|
+
|
414
|
+
# Show important columns for verification
|
415
|
+
important_cols = ['edge_type_id', 'source_node_id', 'target_node_id',
|
416
|
+
'model_template', 'afferent_section_id']
|
417
|
+
available_important = [col for col in important_cols if col in df.columns]
|
418
|
+
print(f"Key columns available: {available_important}")
|
419
|
+
|
420
|
+
# Show model template information if available
|
421
|
+
if 'model_template' in df.columns:
|
422
|
+
unique_templates = df['model_template'].dropna().unique()
|
423
|
+
print(f"Model templates: {unique_templates}")
|
424
|
+
|
425
|
+
except Exception as e:
|
426
|
+
if verbose:
|
427
|
+
print(f"Error loading {key}: {str(e)}")
|
428
|
+
edges_dict[key] = pd.DataFrame() # Store empty DataFrame on error
|
429
|
+
|
430
|
+
if verbose:
|
431
|
+
print(f"\n{'='*60}")
|
432
|
+
print("LOADING SUMMARY")
|
433
|
+
print(f"{'='*60}")
|
434
|
+
for key, df in edges_dict.items():
|
435
|
+
if not df.empty:
|
436
|
+
print(f"{key}: {df.shape[0]} edges, {df.shape[1]} columns")
|
437
|
+
if 'model_template' in df.columns:
|
438
|
+
templates = df['model_template'].dropna().unique()
|
439
|
+
print(f" Model templates: {list(templates)}")
|
440
|
+
if 'afferent_section_id' in df.columns:
|
441
|
+
print(f" afferent_section_id available")
|
442
|
+
else:
|
443
|
+
print(f"{key}: No data loaded (error occurred)")
|
444
|
+
|
445
|
+
return edges_dict
|
391
446
|
|
392
|
-
c_df.reset_index(inplace=True)
|
393
|
-
c_df.rename(columns={"index": "edge_id"}, inplace=True)
|
394
|
-
c_df.set_index("edge_type_id", inplace=True)
|
395
447
|
|
396
|
-
|
397
|
-
|
398
|
-
|
448
|
+
def load_sonata_edges_to_dataframe(
|
449
|
+
csv_path: Union[str, Path],
|
450
|
+
h5_path: Union[str, Path],
|
451
|
+
verbose: bool = False
|
452
|
+
) -> pd.DataFrame:
|
453
|
+
"""
|
454
|
+
Load SONATA format edge data into a pandas DataFrame.
|
455
|
+
|
456
|
+
This function combines edge connectivity data from HDF5 files with edge type
|
457
|
+
metadata from CSV files according to the SONATA specification. H5 attributes
|
458
|
+
take precedence over CSV attributes when both exist for the same column.
|
459
|
+
|
460
|
+
Parameters:
|
461
|
+
-----------
|
462
|
+
csv_path : Union[str, Path]
|
463
|
+
Path to the edge types CSV file (space-delimited)
|
464
|
+
h5_path : Union[str, Path]
|
465
|
+
Path to the edges HDF5 file containing connectivity data
|
466
|
+
verbose : bool, default False
|
467
|
+
If True, print detailed information about the loading process
|
468
|
+
|
469
|
+
Returns:
|
470
|
+
--------
|
471
|
+
pd.DataFrame
|
472
|
+
Combined edge data with columns from both H5 and CSV sources.
|
473
|
+
Columns include:
|
474
|
+
- edge_id: Sequential edge identifier
|
475
|
+
- population: Edge population name
|
476
|
+
- edge_type_id: Edge type identifier for merging with CSV
|
477
|
+
- source_node_id, target_node_id: Connected node IDs
|
478
|
+
- source_population, target_population: Node population names
|
479
|
+
- edge_group_id, edge_group_index: Edge grouping information
|
480
|
+
- Additional attributes from H5 edge groups
|
481
|
+
- Edge type metadata from CSV (model_template, afferent_section_id, etc.)
|
482
|
+
|
483
|
+
Notes:
|
484
|
+
------
|
485
|
+
- CSV file must be space-delimited and contain 'edge_type_id' column
|
486
|
+
- H5 file must follow SONATA edge format with required datasets
|
487
|
+
- When both H5 and CSV contain the same attribute, H5 version is kept
|
488
|
+
- Dynamics parameters are flattened with 'dynamics_params/' prefix
|
489
|
+
"""
|
490
|
+
|
491
|
+
# Load edge types CSV (space-delimited)
|
492
|
+
edge_types_df = pd.read_csv(csv_path, sep=' ')
|
493
|
+
|
494
|
+
if verbose:
|
495
|
+
print(f"Loaded edge types from: {csv_path}")
|
496
|
+
print(f" Columns: {edge_types_df.columns.tolist()}")
|
497
|
+
print(f" Shape: {edge_types_df.shape}")
|
498
|
+
print(f" Unique edge_type_ids: {sorted(edge_types_df['edge_type_id'].unique())}")
|
499
|
+
|
500
|
+
# Open HDF5 file and process edge data
|
501
|
+
with h5py.File(h5_path, 'r') as h5f:
|
502
|
+
|
503
|
+
# Navigate to edges group
|
504
|
+
edges_group = h5f['edges']
|
505
|
+
populations = list(edges_group.keys())
|
506
|
+
|
507
|
+
if verbose:
|
508
|
+
print(f"\nProcessing H5 file: {h5_path}")
|
509
|
+
print(f" Edge populations: {populations}")
|
510
|
+
|
511
|
+
# Process each population (typically one per file)
|
512
|
+
all_edges_data: List[pd.DataFrame] = []
|
513
|
+
|
514
|
+
for pop_name in populations:
|
515
|
+
pop_group = edges_group[pop_name]
|
516
|
+
|
517
|
+
if verbose:
|
518
|
+
print(f"\n Processing population: {pop_name}")
|
519
|
+
|
520
|
+
# Read required SONATA edge datasets
|
521
|
+
edge_type_ids = pop_group['edge_type_id'][:]
|
522
|
+
source_node_ids = pop_group['source_node_id'][:]
|
523
|
+
target_node_ids = pop_group['target_node_id'][:]
|
524
|
+
edge_group_ids = pop_group['edge_group_id'][:]
|
525
|
+
edge_group_indices = pop_group['edge_group_index'][:]
|
526
|
+
|
527
|
+
if verbose:
|
528
|
+
print(f" Unique edge_type_ids in H5: {sorted(np.unique(edge_type_ids))}")
|
529
|
+
print(f" Number of edges: {len(edge_type_ids)}")
|
530
|
+
|
531
|
+
# Extract node population attributes with proper string handling
|
532
|
+
source_pop = pop_group['source_node_id'].attrs.get('node_population', '')
|
533
|
+
target_pop = pop_group['target_node_id'].attrs.get('node_population', '')
|
534
|
+
|
535
|
+
# Handle both string and bytes cases for population names
|
536
|
+
if isinstance(source_pop, bytes):
|
537
|
+
source_pop = source_pop.decode()
|
538
|
+
if isinstance(target_pop, bytes):
|
539
|
+
target_pop = target_pop.decode()
|
540
|
+
|
541
|
+
n_edges = len(edge_type_ids)
|
542
|
+
|
543
|
+
# Create base DataFrame with core edge attributes
|
544
|
+
edges_df = pd.DataFrame({
|
545
|
+
'edge_id': np.arange(n_edges), # Sequential edge identifiers
|
546
|
+
'population': pop_name,
|
547
|
+
'edge_type_id': edge_type_ids,
|
548
|
+
'source_node_id': source_node_ids,
|
549
|
+
'target_node_id': target_node_ids,
|
550
|
+
'source_population': source_pop,
|
551
|
+
'target_population': target_pop,
|
552
|
+
'edge_group_id': edge_group_ids,
|
553
|
+
'edge_group_index': edge_group_indices
|
554
|
+
})
|
555
|
+
|
556
|
+
# Process edge groups to extract additional attributes
|
557
|
+
edge_group_names = [
|
558
|
+
key for key in pop_group.keys()
|
559
|
+
if key not in ['edge_type_id', 'source_node_id', 'target_node_id',
|
560
|
+
'edge_group_id', 'edge_group_index', 'indices']
|
561
|
+
]
|
562
|
+
|
563
|
+
# Track which columns come from H5 for precedence handling
|
564
|
+
edge_attributes: Dict[str, np.ndarray] = {}
|
565
|
+
h5_column_names: set = set()
|
566
|
+
|
567
|
+
# Process each edge group to extract group-specific attributes
|
568
|
+
for group_name in edge_group_names:
|
569
|
+
group = pop_group[group_name]
|
570
|
+
group_id = int(group_name)
|
571
|
+
|
572
|
+
# Identify edges belonging to this group
|
573
|
+
group_mask = edge_group_ids == group_id
|
574
|
+
group_indices = edge_group_indices[group_mask]
|
575
|
+
|
576
|
+
# Extract all attributes from this edge group
|
577
|
+
for attr_name in group.keys():
|
578
|
+
if attr_name == 'dynamics_params': # sonata says this exist but i have yet to see it
|
579
|
+
# Handle nested dynamics parameters
|
580
|
+
dynamics_group = group[attr_name]
|
581
|
+
for param_name in dynamics_group.keys():
|
582
|
+
param_data = dynamics_group[param_name][:]
|
583
|
+
full_attr_name = f"dynamics_params/{param_name}"
|
584
|
+
|
585
|
+
# Initialize attribute array if not exists
|
586
|
+
if full_attr_name not in edge_attributes:
|
587
|
+
edge_attributes[full_attr_name] = np.full(n_edges, np.nan, dtype=object)
|
588
|
+
|
589
|
+
# Assign data to appropriate edges
|
590
|
+
edge_attributes[full_attr_name][group_mask] = param_data[group_indices]
|
591
|
+
h5_column_names.add(full_attr_name)
|
592
|
+
else:
|
593
|
+
# Handle regular attributes with proper string decoding
|
594
|
+
attr_data = group[attr_name][:]
|
595
|
+
|
596
|
+
# Handle string attributes properly
|
597
|
+
if attr_data.dtype.kind in ['S', 'U']: # String or Unicode
|
598
|
+
# Decode bytes to strings if necessary
|
599
|
+
if attr_data.dtype.kind == 'S':
|
600
|
+
attr_data = np.array([
|
601
|
+
s.decode() if isinstance(s, bytes) else s
|
602
|
+
for s in attr_data
|
603
|
+
])
|
604
|
+
|
605
|
+
# Initialize attribute array with appropriate default
|
606
|
+
if attr_name not in edge_attributes:
|
607
|
+
if attr_data.dtype.kind in ['S', 'U']:
|
608
|
+
edge_attributes[attr_name] = np.full(n_edges, '', dtype=object)
|
609
|
+
else:
|
610
|
+
edge_attributes[attr_name] = np.full(n_edges, np.nan, dtype=object)
|
611
|
+
|
612
|
+
# Assign data to appropriate edges
|
613
|
+
edge_attributes[attr_name][group_mask] = attr_data[group_indices]
|
614
|
+
h5_column_names.add(attr_name)
|
615
|
+
|
616
|
+
# Add all H5 attributes to the DataFrame
|
617
|
+
for attr_name, attr_values in edge_attributes.items():
|
618
|
+
edges_df[attr_name] = attr_values
|
619
|
+
|
620
|
+
all_edges_data.append(edges_df)
|
621
|
+
|
622
|
+
# Combine all populations into single DataFrame
|
623
|
+
if all_edges_data:
|
624
|
+
combined_edges_df = pd.concat(all_edges_data, ignore_index=True)
|
625
|
+
else:
|
626
|
+
combined_edges_df = pd.DataFrame()
|
627
|
+
|
628
|
+
if verbose:
|
629
|
+
print(f"\n H5 columns: {sorted(h5_column_names)}")
|
630
|
+
print(f" CSV columns: {edge_types_df.columns.tolist()}")
|
631
|
+
print(f" Combined edges before merge: {combined_edges_df.shape}")
|
632
|
+
|
633
|
+
# Merge H5 data with CSV edge type metadata
|
634
|
+
if not combined_edges_df.empty and not edge_types_df.empty:
|
635
|
+
# Determine merge columns (typically just edge_type_id)
|
636
|
+
merge_cols = []
|
637
|
+
if 'edge_type_id' in combined_edges_df.columns and 'edge_type_id' in edge_types_df.columns:
|
638
|
+
merge_cols.append('edge_type_id')
|
639
|
+
|
640
|
+
if 'population' in edge_types_df.columns and 'population' in combined_edges_df.columns:
|
641
|
+
merge_cols.append('population')
|
642
|
+
|
643
|
+
if merge_cols:
|
644
|
+
if verbose:
|
645
|
+
# Debug merge compatibility
|
646
|
+
h5_edge_types = set(combined_edges_df['edge_type_id'].unique())
|
647
|
+
csv_edge_types = set(edge_types_df['edge_type_id'].unique())
|
648
|
+
overlap = h5_edge_types.intersection(csv_edge_types)
|
649
|
+
|
650
|
+
print(f"\n Merging on columns: {merge_cols}")
|
651
|
+
print(f" Edge types in H5: {sorted(h5_edge_types)}")
|
652
|
+
print(f" Edge types in CSV: {sorted(csv_edge_types)}")
|
653
|
+
print(f" Overlap: {sorted(overlap)}")
|
654
|
+
|
655
|
+
# Perform left join to preserve all H5 edges
|
656
|
+
final_df = combined_edges_df.merge(
|
657
|
+
edge_types_df,
|
658
|
+
on=merge_cols,
|
659
|
+
how='left',
|
660
|
+
suffixes=('', '_csv')
|
399
661
|
)
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
662
|
+
|
663
|
+
if verbose:
|
664
|
+
print(f" After merge: {final_df.shape}")
|
665
|
+
|
666
|
+
# Apply H5 precedence rule: H5 attributes override CSV attributes
|
667
|
+
for col in edge_types_df.columns:
|
668
|
+
if col in merge_cols:
|
669
|
+
continue # Skip merge columns
|
670
|
+
|
671
|
+
csv_col_name = f"{col}_csv" if f"{col}_csv" in final_df.columns else col
|
672
|
+
|
673
|
+
# If column exists in H5, handle row-by-row precedence
|
674
|
+
if col in h5_column_names:
|
675
|
+
if csv_col_name in final_df.columns and csv_col_name != col:
|
676
|
+
# Row-by-row logic: use CSV value only where H5 is NaN
|
677
|
+
mask_h5_nan = final_df[col].isna()
|
678
|
+
final_df.loc[mask_h5_nan, col] = final_df.loc[mask_h5_nan, csv_col_name]
|
679
|
+
|
680
|
+
if verbose and mask_h5_nan.any():
|
681
|
+
n_filled = mask_h5_nan.sum()
|
682
|
+
print(f" Column '{col}': filled {n_filled} NaN values from CSV")
|
683
|
+
|
684
|
+
# Remove CSV version after filling NaN values
|
685
|
+
final_df = final_df.drop(columns=[csv_col_name])
|
686
|
+
elif verbose:
|
687
|
+
print(f" Column '{col}' exists in H5 only")
|
688
|
+
# If column only in CSV, rename if needed
|
689
|
+
elif csv_col_name in final_df.columns and csv_col_name != col:
|
690
|
+
final_df = final_df.rename(columns={csv_col_name: col})
|
691
|
+
if verbose:
|
692
|
+
print(f" Added column '{col}' from CSV")
|
693
|
+
else:
|
694
|
+
if verbose:
|
695
|
+
print(" No common merge columns found. Using H5 data only.")
|
696
|
+
final_df = combined_edges_df
|
697
|
+
else:
|
698
|
+
final_df = combined_edges_df
|
699
|
+
|
700
|
+
if verbose:
|
701
|
+
print(f"\nFinal DataFrame: {final_df.shape}")
|
702
|
+
print(f"Final columns: {final_df.columns.tolist()}")
|
703
|
+
|
704
|
+
# Set edge_type_id as the index to match old function
|
705
|
+
if 'edge_type_id' in final_df.columns:
|
706
|
+
final_df = final_df.set_index('edge_type_id')
|
707
|
+
if verbose:
|
708
|
+
print(f"Set edge_type_id as index. Final shape: {final_df.shape}")
|
709
|
+
|
710
|
+
return final_df
|
437
711
|
|
438
712
|
|
439
713
|
def load_mechanisms_from_config(config=None):
|
@@ -604,7 +878,6 @@ def relation_matrix(
|
|
604
878
|
if relation_func:
|
605
879
|
source_nodes = nodes[source].add_prefix("source_")
|
606
880
|
target_nodes = nodes[target].add_prefix("target_")
|
607
|
-
|
608
881
|
c_edges = pd.merge(
|
609
882
|
left=edges[e_name],
|
610
883
|
right=source_nodes,
|
@@ -856,7 +1129,8 @@ def percent_connections(
|
|
856
1129
|
cons = edges[(edges[source_id_type] == source_id) & (edges[target_id_type] == target_id)]
|
857
1130
|
if not include_gap:
|
858
1131
|
try:
|
859
|
-
|
1132
|
+
gaps = cons["is_gap_junction"]==True
|
1133
|
+
cons = cons[~gaps]
|
860
1134
|
except:
|
861
1135
|
raise Exception("no gap junctions found to drop from connections")
|
862
1136
|
|
@@ -1003,7 +1277,7 @@ def gap_junction_connections(
|
|
1003
1277
|
# print(cons)
|
1004
1278
|
|
1005
1279
|
try:
|
1006
|
-
cons = cons[cons["is_gap_junction"]]
|
1280
|
+
cons = cons[cons["is_gap_junction"]==True]
|
1007
1281
|
except:
|
1008
1282
|
raise Exception("no gap junctions found to drop from connections")
|
1009
1283
|
mean = cons["target_node_id"].value_counts().mean()
|
@@ -13,7 +13,7 @@ bmtool/analysis/lfp.py,sha256=S2JvxkjcK3-EH93wCrhqNSFY6cX7fOq74pz64ibHKrc,26556
|
|
13
13
|
bmtool/analysis/netcon_reports.py,sha256=VnPZNKPaQA7oh1q9cIatsqQudm4cOtzNtbGPXoiDCD0,2909
|
14
14
|
bmtool/analysis/spikes.py,sha256=3n-xmyEZ7w6CKEND7-aKOAvdDg0lwDuPI5sMdOuPwa0,24637
|
15
15
|
bmtool/bmplot/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
16
|
-
bmtool/bmplot/connections.py,sha256=
|
16
|
+
bmtool/bmplot/connections.py,sha256=Tdi-17poEuBBgXcKQ--pcDTDwl029nXg9EConRgZo-k,58928
|
17
17
|
bmtool/bmplot/entrainment.py,sha256=BrBMerqyiG2YWAO_OEFv7OJf3yeFz3l9jUt4NamluLc,32837
|
18
18
|
bmtool/bmplot/lfp.py,sha256=SNpbWGOUnYEgnkeBw5S--aPN5mIGD22Gw2Pwus0_lvY,2034
|
19
19
|
bmtool/bmplot/netcon_reports.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -23,12 +23,12 @@ bmtool/debug/commands.py,sha256=VV00f6q5gzZI503vUPeG40ABLLen0bw_k4-EX-H5WZE,580
|
|
23
23
|
bmtool/debug/debug.py,sha256=9yUFvA4_Bl-x9s29quIEG3pY-S8hNJF3RKBfRBHCl28,208
|
24
24
|
bmtool/util/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
25
25
|
bmtool/util/commands.py,sha256=Nn-R-4e9g8ZhSPZvTkr38xeKRPfEMANB9Lugppj82UI,68564
|
26
|
-
bmtool/util/util.py,sha256=
|
26
|
+
bmtool/util/util.py,sha256=TAWdGd0tDuouS-JiusMs8WwP7kQpWHPr1nu0XG01TBQ,75056
|
27
27
|
bmtool/util/neuron/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
28
28
|
bmtool/util/neuron/celltuner.py,sha256=lokRLUM1rsdSYBYrNbLBBo39j14mm8TBNVNRnSlhHCk,94868
|
29
|
-
bmtool-0.7.
|
30
|
-
bmtool-0.7.
|
31
|
-
bmtool-0.7.
|
32
|
-
bmtool-0.7.
|
33
|
-
bmtool-0.7.
|
34
|
-
bmtool-0.7.
|
29
|
+
bmtool-0.7.3.dist-info/licenses/LICENSE,sha256=qrXg2jj6kz5d0EnN11hllcQt2fcWVNumx0xNbV05nyM,1068
|
30
|
+
bmtool-0.7.3.dist-info/METADATA,sha256=uzyKoEDxd3kJQc89-pzf6wSxR7v3BhBHYEvyqCh5aHM,3575
|
31
|
+
bmtool-0.7.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
32
|
+
bmtool-0.7.3.dist-info/entry_points.txt,sha256=0-BHZ6nUnh0twWw9SXNTiRmKjDnb1VO2DfG_-oprhAc,45
|
33
|
+
bmtool-0.7.3.dist-info/top_level.txt,sha256=gpd2Sj-L9tWbuJEd5E8C8S8XkNm5yUE76klUYcM-eWM,7
|
34
|
+
bmtool-0.7.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|