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.
@@ -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 plot_synapse_location_histograms(config, target_model, source=None, target=None):
984
+ def plot_synapse_location(config: str, source: str, target: str, sids: str, tids: str) -> tuple:
985
985
  """
986
- generates a histogram of the positions of the synapses on a cell broken down by section
987
- config: a BMTK config
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
- # Color map for source populations
1031
- color_map = plt.cm.tab10(np.linspace(0, 1, len(unique_pops)))
1032
- pop_colors = {pop: color for pop, color in zip(unique_pops, color_map)}
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
- # Create a histogram for each section
1035
- for i, section in enumerate(sections_with_data):
1036
- ax = plt.subplot(len(sections_with_data), 1, i + 1)
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
- # Get data for this section
1039
- section_data = edges[edges["afferent_section_name"] == section]
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
- # Group by source population
1042
- for pop_name, pop_group in section_data.groupby("source_pop_name"):
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
- # Set title and labels
1053
- ax.set_title(f"{section}", fontsize=10)
1054
- ax.set_xlabel("Section Position", fontsize=8)
1055
- ax.set_ylabel("Frequency", fontsize=8)
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
- # Only add legend to the first plot
1060
- if i == 0:
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
- plt.tight_layout()
1064
- plt.suptitle(
1065
- "Connection Distribution by Cell Section and Source Population", fontsize=16, y=1.02
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
- pass
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(edge_paths): # network_dir='network'):
351
+ def load_edges_from_paths(
352
+ file_paths: str,
353
+ verbose: bool = False
354
+ ) -> Dict[str, pd.DataFrame]:
349
355
  """
350
- Returns: A dictionary of connections with filenames (minus _edges.h5) as keys
351
-
352
- edge_paths must be in the format in a circuit config file:
353
- [
354
- {
355
- "edges_file":"filepath", (csv)
356
- "edge_types_file":"filepath" (h5)
357
- },...
358
- ]
359
- util.load_edges_from_paths([{"edges_file":"network/hippocampus_hippocampus_edges.h5","edge_types_file":"network/hippocampus_hippocampus_edge_types.csv"}])
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
- import h5py
362
- import pandas as pd
363
- # edges_regex = "_edges.h5"
364
- # edge_types_regex = "_edge_types.csv"
365
-
366
- # edges_h5_fpaths = glob.glob(os.path.join(network_dir,'*'+edges_regex))
367
- # edge_types_fpaths = glob.glob(os.path.join(network_dir,'*'+edge_types_regex))
368
-
369
- # connections = [re.findall('^[A-Za-z0-9]+_[A-Za-z0-9][^_]+', os.path.basename(n))[0] for n in edges_h5_fpaths]
370
- edges_dict = {}
371
-
372
- def get_edge_table(edges_file, edge_types_file, population=None):
373
- # dataframe where each row is an edge type
374
- cm_df = pd.read_csv(edge_types_file, sep=" ")
375
- cm_df.set_index("edge_type_id", inplace=True)
376
-
377
- with h5py.File(edges_file, "r") as connections_h5:
378
- if population is None:
379
- if len(connections_h5["/edges"]) > 1:
380
- raise Exception(
381
- "Multiple populations in edges file. Not currently implemented, should not be hard to do, contact Tyler"
382
- )
383
- else:
384
- population = list(connections_h5["/edges"])[0]
385
- conn_grp = connections_h5["/edges"][population]
386
-
387
- # dataframe where each row is an edge
388
- c_df = pd.DataFrame(
389
- {key: conn_grp[key] for key in ("edge_type_id", "source_node_id", "target_node_id")}
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
- # add edge type properties to df of edges
397
- edges_df = pd.merge(
398
- left=c_df, right=cm_df, how="left", left_index=True, right_index=True
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
- # extra properties of individual edges (see SONATA Data format)
402
- if conn_grp.get("0"):
403
- edge_group_id = conn_grp["edge_group_id"][()]
404
- edge_group_index = conn_grp["edge_group_index"][()]
405
- n_group = edge_group_id.max() + 1
406
- prop_dtype = {}
407
- for group_id in range(n_group):
408
- group = conn_grp[str(group_id)]
409
- idx = edge_group_id == group_id
410
- for prop in group:
411
- # create new column with NaN if property does not exist
412
- if prop not in edges_df:
413
- edges_df[prop] = np.nan
414
- edges_df.loc[idx, prop] = tuple(group[prop][edge_group_index[idx]])
415
- prop_dtype[prop] = group[prop].dtype
416
- # convert to original data type if possible
417
- for prop, dtype in prop_dtype.items():
418
- edges_df[prop] = edges_df[prop].astype(dtype, errors="ignore")
419
-
420
- return population, edges_df
421
-
422
- # for edges_dict, conn_models_file, conns_file in zip(connections, edge_types_fpaths, edges_h5_fpaths):
423
- # connections_dict[connection] = get_connection_table(conn_models_file,conns_file)
424
- try:
425
- for nodes in edge_paths:
426
- edges_file = nodes["edges_file"]
427
- edge_types_file = nodes["edge_types_file"]
428
- region_name, region = get_edge_table(edges_file, edge_types_file)
429
- edges_dict[region_name] = region
430
- except Exception as e:
431
- print(repr(e))
432
- print("Hint: Are you loading the correct simulation config file?")
433
- print("Command Line: bmtool plot --config yourconfig.json <rest of command>")
434
- print("Python: bmplot.connection_matrix(config='yourconfig.json')")
435
-
436
- return edges_dict
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
- cons = cons[~cons["is_gap_junction"]]
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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bmtool
3
- Version: 0.7.2
3
+ Version: 0.7.3
4
4
  Summary: BMTool
5
5
  Home-page: https://github.com/cyneuro/bmtool
6
6
  Download-URL:
@@ -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=KSORZ43v1B5xfiBN6AnAD7tJySVTkLIY3j_zb2r-YPA,55696
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=S8sAXwDiISGAqnSXRIgFqxqCRzL5YcxAqP1UGxGA5Z4,62906
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.2.dist-info/licenses/LICENSE,sha256=qrXg2jj6kz5d0EnN11hllcQt2fcWVNumx0xNbV05nyM,1068
30
- bmtool-0.7.2.dist-info/METADATA,sha256=JacxbP2RvvbSuuveedTUJjl8KeqOCKf4FlW-UrRmfCk,3575
31
- bmtool-0.7.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
32
- bmtool-0.7.2.dist-info/entry_points.txt,sha256=0-BHZ6nUnh0twWw9SXNTiRmKjDnb1VO2DfG_-oprhAc,45
33
- bmtool-0.7.2.dist-info/top_level.txt,sha256=gpd2Sj-L9tWbuJEd5E8C8S8XkNm5yUE76klUYcM-eWM,7
34
- bmtool-0.7.2.dist-info/RECORD,,
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