bmtool 0.7.2.1__py3-none-any.whl → 0.7.4__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.
@@ -13,6 +13,8 @@ import numpy as np
13
13
  import pandas as pd
14
14
  from IPython import get_ipython
15
15
 
16
+ from neuron import h
17
+
16
18
  from ..util import util
17
19
 
18
20
  use_description = """
@@ -132,6 +134,7 @@ def total_connection_matrix(
132
134
  title = "All Synapse .mod Files Used"
133
135
  if synaptic_info == "3":
134
136
  title = "All Synapse .json Files Used"
137
+
135
138
  plot_connection_info(
136
139
  text, num, source_labels, target_labels, title, syn_info=synaptic_info, save_file=save_file
137
140
  )
@@ -980,9 +983,10 @@ def distance_delay_plot(
980
983
  plt.show()
981
984
 
982
985
 
983
- def plot_synapse_location(config: str, source: str, target: str, sids: str, tids: str) -> tuple:
986
+ def plot_synapse_location(config: str, source: str, target: str, sids: str, tids: str, syn_feature: str = 'afferent_section_id') -> tuple:
984
987
  """
985
988
  Generates a connectivity matrix showing synaptic distribution across different cell sections.
989
+ Note does exclude gap junctions since they dont have an afferent id stored in the h5 file!
986
990
 
987
991
  Parameters
988
992
  ----------
@@ -996,6 +1000,8 @@ def plot_synapse_location(config: str, source: str, target: str, sids: str, tids
996
1000
  Column name in nodes file containing source population identifiers
997
1001
  tids : str
998
1002
  Column name in nodes file containing target population identifiers
1003
+ syn_feature : str, default 'afferent_section_id'
1004
+ Synaptic feature to analyze ('afferent_section_id' or 'afferent_section_pos')
999
1005
 
1000
1006
  Returns
1001
1007
  -------
@@ -1009,37 +1015,50 @@ def plot_synapse_location(config: str, source: str, target: str, sids: str, tids
1009
1015
  RuntimeError
1010
1016
  If template loading or cell instantiation fails
1011
1017
  """
1012
- import matplotlib.pyplot as plt
1013
- import numpy as np
1014
- from neuron import h
1015
-
1016
1018
  # Validate inputs
1017
1019
  if not all([config, source, target, sids, tids]):
1018
1020
  raise ValueError(
1019
1021
  "Missing required parameters: config, source, target, sids, and tids must be provided"
1020
1022
  )
1021
1023
 
1024
+ # Fix the validation logic - it was using 'or' instead of 'and'
1025
+ if syn_feature not in ["afferent_section_id", "afferent_section_pos"]:
1026
+ raise ValueError("Currently only syn features supported are afferent_section_id or afferent_section_pos")
1027
+
1022
1028
  try:
1023
1029
  # Load mechanisms and template
1024
1030
  util.load_templates_from_config(config)
1025
1031
  except Exception as e:
1026
1032
  raise RuntimeError(f"Failed to load templates from config: {str(e)}")
1027
-
1033
+
1028
1034
  try:
1029
1035
  # Load node and edge data
1030
1036
  nodes, edges = util.load_nodes_edges_from_config(config)
1031
1037
  if source not in nodes or f"{source}_to_{target}" not in edges:
1032
1038
  raise ValueError(f"Source '{source}' or target '{target}' networks not found in data")
1033
1039
 
1034
- nodes = nodes[source]
1040
+ target_nodes = nodes[target]
1041
+ source_nodes = nodes[source]
1035
1042
  edges = edges[f"{source}_to_{target}"]
1043
+
1044
+ # Find edges with NaN values in the specified feature
1045
+ nan_edges = edges[edges[syn_feature].isna()]
1046
+ # Print information about removed edges
1047
+ if not nan_edges.empty:
1048
+ unique_indices = sorted(list(set(nan_edges.index.tolist())))
1049
+ print(f"Removing {len(nan_edges)} edges with missing {syn_feature}")
1050
+ print(f"Unique indices removed: {unique_indices}")
1051
+
1052
+ # Filter out edges with NaN values in the specified feature
1053
+ edges = edges[edges[syn_feature].notna()]
1054
+
1036
1055
  except Exception as e:
1037
1056
  raise RuntimeError(f"Failed to load nodes and edges: {str(e)}")
1038
1057
 
1039
1058
  # Map identifiers while checking for missing values
1040
- edges["target_model_template"] = edges["target_node_id"].map(nodes["model_template"])
1041
- edges["target_pop_name"] = edges["target_node_id"].map(nodes[tids])
1042
- edges["source_pop_name"] = edges["source_node_id"].map(nodes[sids])
1059
+ edges["target_model_template"] = edges["target_node_id"].map(target_nodes["model_template"])
1060
+ edges["target_pop_name"] = edges["target_node_id"].map(target_nodes[tids])
1061
+ edges["source_pop_name"] = edges["source_node_id"].map(source_nodes[sids])
1043
1062
 
1044
1063
  if edges["target_model_template"].isnull().any():
1045
1064
  print("Warning: Some target nodes missing model template")
@@ -1101,7 +1120,7 @@ def plot_synapse_location(config: str, source: str, target: str, sids: str, tids
1101
1120
  section_mapping = section_mappings[target_model_template]
1102
1121
 
1103
1122
  # Calculate section distribution
1104
- section_counts = filtered_edges["afferent_section_id"].value_counts()
1123
+ section_counts = filtered_edges[syn_feature].value_counts()
1105
1124
  section_percentages = (section_counts / total_connections * 100).round(1)
1106
1125
 
1107
1126
  # Format section distribution text - show all sections
@@ -1110,16 +1129,17 @@ def plot_synapse_location(config: str, source: str, target: str, sids: str, tids
1110
1129
  section_name = section_mapping.get(section_id, f"sec_{section_id}")
1111
1130
  section_display.append(f"{section_name}:{percentage}%")
1112
1131
 
1132
+
1113
1133
  num_connections[source_idx, target_idx] = total_connections
1114
1134
  text_data[source_idx, target_idx] = "\n".join(section_display)
1115
1135
 
1116
1136
  except Exception as e:
1117
1137
  print(f"Warning: Error processing {target_model_template}: {str(e)}")
1118
1138
  num_connections[source_idx, target_idx] = total_connections
1119
- text_data[source_idx, target_idx] = "Section info N/A"
1139
+ text_data[source_idx, target_idx] = "Feature info N/A"
1120
1140
 
1121
1141
  # Create the plot
1122
- title = f"Synaptic Distribution by Section: {source} to {target}"
1142
+ title = f"Synaptic Distribution by {syn_feature.replace('_', ' ').title()}: {source} to {target}"
1123
1143
  fig, ax = plot_connection_info(
1124
1144
  text=text_data,
1125
1145
  num=num_connections,
bmtool/bmplot/lfp.py CHANGED
@@ -1,7 +1,6 @@
1
1
  import matplotlib.pyplot as plt
2
2
  import numpy as np
3
-
4
- from bmtool.analysis.lfp import gen_aperiodic
3
+ from fooof.sim.gen import gen_aperiodic
5
4
 
6
5
 
7
6
  def plot_spectrogram(
bmtool/synapses.py CHANGED
@@ -20,68 +20,104 @@ from tqdm.notebook import tqdm
20
20
 
21
21
  from bmtool.util.util import load_templates_from_config
22
22
 
23
+ DEFAULT_GENERAL_SETTINGS = {
24
+ "vclamp": True,
25
+ "rise_interval": (0.1, 0.9),
26
+ "tstart": 500.0,
27
+ "tdur": 100.0,
28
+ "threshold": -15.0,
29
+ "delay": 1.3,
30
+ "weight": 1.0,
31
+ "dt": 0.025,
32
+ "celsius": 20,
33
+ }
34
+
35
+ DEFAULT_GAP_JUNCTION_GENERAL_SETTINGS = {
36
+ "tstart": 500.0,
37
+ "tdur": 500.0,
38
+ "dt": 0.025,
39
+ "celsius": 20,
40
+ }
41
+
23
42
 
24
43
  class SynapseTuner:
25
44
  def __init__(
26
45
  self,
27
- mechanisms_dir: str = None,
28
- templates_dir: str = None,
29
- config: str = None,
30
- conn_type_settings: dict = None,
31
- connection: str = None,
32
- general_settings: dict = None,
33
- json_folder_path: str = None,
46
+ mechanisms_dir: Optional[str] = None,
47
+ templates_dir: Optional[str] = None,
48
+ config: Optional[str] = None,
49
+ conn_type_settings: Optional[dict] = None,
50
+ connection: Optional[str] = None,
51
+ general_settings: Optional[dict] = None,
52
+ json_folder_path: Optional[str] = None,
34
53
  current_name: str = "i",
35
- other_vars_to_record: list = None,
36
- slider_vars: list = None,
54
+ other_vars_to_record: Optional[list] = None,
55
+ slider_vars: Optional[list] = None,
56
+ hoc_cell: Optional[object] = None,
37
57
  ) -> None:
38
58
  """
39
- Initialize the SynapseModule class with connection type settings, mechanisms, and template directories.
59
+ Initialize the SynapseTuner class with connection type settings, mechanisms, and template directories.
40
60
 
41
61
  Parameters:
42
62
  -----------
43
- mechanisms_dir : str
63
+ mechanisms_dir : Optional[str]
44
64
  Directory path containing the compiled mod files needed for NEURON mechanisms.
45
- templates_dir : str
65
+ templates_dir : Optional[str]
46
66
  Directory path containing cell template files (.hoc or .py) loaded into NEURON.
47
- conn_type_settings : dict
67
+ conn_type_settings : Optional[dict]
48
68
  A dictionary containing connection-specific settings, such as synaptic properties and details.
49
- connection : str
69
+ connection : Optional[str]
50
70
  Name of the connection type to be used from the conn_type_settings dictionary.
51
- general_settings : dict
71
+ general_settings : Optional[dict]
52
72
  General settings dictionary including parameters like simulation time step, duration, and temperature.
53
- json_folder_path : str, optional
73
+ json_folder_path : Optional[str]
54
74
  Path to folder containing JSON files with additional synaptic properties to update settings.
55
75
  current_name : str, optional
56
76
  Name of the synaptic current variable to be recorded (default is 'i').
57
- other_vars_to_record : list, optional
77
+ other_vars_to_record : Optional[list]
58
78
  List of additional synaptic variables to record during the simulation (e.g., 'Pr', 'Use').
59
- slider_vars : list, optional
79
+ slider_vars : Optional[list]
60
80
  List of synaptic variables you would like sliders set up for the STP sliders method by default will use all parameters in spec_syn_param.
61
-
81
+ hoc_cell : Optional[object]
82
+ An already loaded NEURON cell object. If provided, template loading and cell setup will be skipped.
62
83
  """
63
- if config is None and (mechanisms_dir is None or templates_dir is None):
64
- raise ValueError(
65
- "Either a config file or both mechanisms_dir and templates_dir must be provided."
66
- )
84
+ self.hoc_cell = hoc_cell
67
85
 
68
- if config is None:
69
- neuron.load_mechanisms(mechanisms_dir)
70
- h.load_file(templates_dir)
71
- else:
72
- # loads both mech and templates
73
- load_templates_from_config(config)
86
+ if hoc_cell is None:
87
+ if config is None and (mechanisms_dir is None or templates_dir is None):
88
+ raise ValueError(
89
+ "Either a config file, both mechanisms_dir and templates_dir, or a hoc_cell must be provided."
90
+ )
74
91
 
75
- self.conn_type_settings = conn_type_settings
92
+ if config is None:
93
+ neuron.load_mechanisms(mechanisms_dir)
94
+ h.load_file(templates_dir)
95
+ else:
96
+ # loads both mech and templates
97
+ load_templates_from_config(config)
98
+
99
+ if conn_type_settings is None:
100
+ raise ValueError("conn_type_settings must be provided.")
101
+ if connection is None:
102
+ raise ValueError("connection must be provided.")
103
+ if connection not in conn_type_settings:
104
+ raise ValueError(f"connection '{connection}' not found in conn_type_settings.")
105
+
106
+ self.conn_type_settings: dict = conn_type_settings
76
107
  if json_folder_path:
77
108
  print(f"updating settings from json path {json_folder_path}")
78
109
  self._update_spec_syn_param(json_folder_path)
79
- self.general_settings = general_settings
110
+ # Use default general settings if not provided
111
+ if general_settings is None:
112
+ self.general_settings: dict = DEFAULT_GENERAL_SETTINGS.copy()
113
+ else:
114
+ # Merge defaults with user-provided
115
+ self.general_settings = {**DEFAULT_GENERAL_SETTINGS, **general_settings}
80
116
  self.conn = self.conn_type_settings[connection]
81
117
  self.synaptic_props = self.conn["spec_syn_param"]
82
- self.vclamp = general_settings["vclamp"]
118
+ self.vclamp = self.general_settings["vclamp"]
83
119
  self.current_name = current_name
84
- self.other_vars_to_record = other_vars_to_record
120
+ self.other_vars_to_record = other_vars_to_record or []
85
121
  self.ispk = None
86
122
 
87
123
  if slider_vars:
@@ -94,25 +130,26 @@ class SynapseTuner:
94
130
  # If the key is missing from synaptic_props, get the value using getattr
95
131
  if key not in self.synaptic_props:
96
132
  try:
97
- # Get the alternative value from getattr dynamically
98
133
  self._set_up_cell()
99
134
  self._set_up_synapse()
100
135
  value = getattr(self.syn, key)
101
- # print(value)
102
136
  self.slider_vars[key] = value
103
137
  except AttributeError as e:
104
138
  print(f"Error accessing '{key}' in syn {self.syn}: {e}")
105
-
106
139
  else:
107
140
  self.slider_vars = self.synaptic_props
108
141
 
109
- h.tstop = general_settings["tstart"] + general_settings["tdur"]
110
- h.dt = general_settings["dt"] # Time step (resolution) of the simulation in ms
142
+ h.tstop = self.general_settings["tstart"] + self.general_settings["tdur"]
143
+ h.dt = self.general_settings["dt"] # Time step (resolution) of the simulation in ms
111
144
  h.steps_per_ms = 1 / h.dt
112
- h.celsius = general_settings["celsius"]
145
+ h.celsius = self.general_settings["celsius"]
113
146
 
114
147
  # get some stuff set up we need for both SingleEvent and Interactive Tuner
115
- self._set_up_cell()
148
+ # Only set up cell if hoc_cell was not provided
149
+ if self.hoc_cell is None:
150
+ self._set_up_cell()
151
+ else:
152
+ self.cell = self.hoc_cell
116
153
  self._set_up_synapse()
117
154
 
118
155
  self.nstim = h.NetStim()
@@ -137,7 +174,7 @@ class SynapseTuner:
137
174
 
138
175
  self._set_up_recorders()
139
176
 
140
- def _update_spec_syn_param(self, json_folder_path):
177
+ def _update_spec_syn_param(self, json_folder_path: str) -> None:
141
178
  """
142
179
  Update specific synaptic parameters using JSON files located in the specified folder.
143
180
 
@@ -146,6 +183,8 @@ class SynapseTuner:
146
183
  json_folder_path : str
147
184
  Path to folder containing JSON files, where each JSON file corresponds to a connection type.
148
185
  """
186
+ if not self.conn_type_settings:
187
+ return
149
188
  for conn_type, settings in self.conn_type_settings.items():
150
189
  json_file_path = os.path.join(json_folder_path, f"{conn_type}.json")
151
190
  if os.path.exists(json_file_path):
@@ -155,13 +194,17 @@ class SynapseTuner:
155
194
  else:
156
195
  print(f"JSON file for {conn_type} not found.")
157
196
 
158
- def _set_up_cell(self):
197
+ def _set_up_cell(self) -> None:
159
198
  """
160
199
  Set up the neuron cell based on the specified connection settings.
200
+ This method is only called when hoc_cell is not provided.
161
201
  """
162
- self.cell = getattr(h, self.conn["spec_settings"]["post_cell"])()
202
+ if self.hoc_cell is None:
203
+ self.cell = getattr(h, self.conn["spec_settings"]["post_cell"])()
204
+ else:
205
+ self.cell = self.hoc_cell
163
206
 
164
- def _set_up_synapse(self):
207
+ def _set_up_synapse(self) -> None:
165
208
  """
166
209
  Set up the synapse on the target cell according to the synaptic parameters in `conn_type_settings`.
167
210
 
@@ -176,7 +219,7 @@ class SynapseTuner:
176
219
  )
177
220
  )
178
221
  for key, value in self.conn["spec_syn_param"].items():
179
- if isinstance(value, (int, float)): # Only create sliders for numeric values
222
+ if isinstance(value, (int, float)):
180
223
  if hasattr(self.syn, key):
181
224
  setattr(self.syn, key, value)
182
225
  else:
@@ -184,7 +227,7 @@ class SynapseTuner:
184
227
  f"Warning: {key} cannot be assigned as it does not exist in the synapse. Check your mod file or spec_syn_param."
185
228
  )
186
229
 
187
- def _set_up_recorders(self):
230
+ def _set_up_recorders(self) -> None:
188
231
  """
189
232
  Set up recording vectors to capture simulation data.
190
233
 
@@ -952,11 +995,12 @@ class SynapseTuner:
952
995
  class GapJunctionTuner:
953
996
  def __init__(
954
997
  self,
955
- mechanisms_dir: str = None,
956
- templates_dir: str = None,
957
- config: str = None,
958
- general_settings: dict = None,
959
- conn_type_settings: dict = None,
998
+ mechanisms_dir: Optional[str] = None,
999
+ templates_dir: Optional[str] = None,
1000
+ config: Optional[str] = None,
1001
+ general_settings: Optional[dict] = None,
1002
+ conn_type_settings: Optional[dict] = None,
1003
+ hoc_cell: Optional[object] = None,
960
1004
  ):
961
1005
  """
962
1006
  Initialize the GapJunctionTuner class.
@@ -973,34 +1017,49 @@ class GapJunctionTuner:
973
1017
  General settings dictionary including parameters like simulation time step, duration, and temperature.
974
1018
  conn_type_settings : dict
975
1019
  A dictionary containing connection-specific settings for gap junctions.
1020
+ hoc_cell : object, optional
1021
+ An already loaded NEURON cell object. If provided, template loading and cell creation will be skipped.
976
1022
  """
977
- if config is None and (mechanisms_dir is None or templates_dir is None):
978
- raise ValueError(
979
- "Either a config file or both mechanisms_dir and templates_dir must be provided."
980
- )
1023
+ self.hoc_cell = hoc_cell
981
1024
 
982
- if config is None:
983
- neuron.load_mechanisms(mechanisms_dir)
984
- h.load_file(templates_dir)
985
- else:
986
- # this will load both mechs and templates
987
- load_templates_from_config(config)
1025
+ if hoc_cell is None:
1026
+ if config is None and (mechanisms_dir is None or templates_dir is None):
1027
+ raise ValueError(
1028
+ "Either a config file, both mechanisms_dir and templates_dir, or a hoc_cell must be provided."
1029
+ )
1030
+
1031
+ if config is None:
1032
+ neuron.load_mechanisms(mechanisms_dir)
1033
+ h.load_file(templates_dir)
1034
+ else:
1035
+ # this will load both mechs and templates
1036
+ load_templates_from_config(config)
988
1037
 
989
- self.general_settings = general_settings
1038
+ # Use default general settings if not provided, merge with user-provided
1039
+ if general_settings is None:
1040
+ self.general_settings: dict = DEFAULT_GAP_JUNCTION_GENERAL_SETTINGS.copy()
1041
+ else:
1042
+ self.general_settings = {**DEFAULT_GAP_JUNCTION_GENERAL_SETTINGS, **general_settings}
990
1043
  self.conn_type_settings = conn_type_settings
991
1044
 
992
- h.tstop = general_settings["tstart"] + general_settings["tdur"] + 100.0
993
- h.dt = general_settings["dt"] # Time step (resolution) of the simulation in ms
1045
+ h.tstop = self.general_settings["tstart"] + self.general_settings["tdur"] + 100.0
1046
+ h.dt = self.general_settings["dt"] # Time step (resolution) of the simulation in ms
994
1047
  h.steps_per_ms = 1 / h.dt
995
- h.celsius = general_settings["celsius"]
996
-
997
- self.cell_name = conn_type_settings["cell"]
1048
+ h.celsius = self.general_settings["celsius"]
998
1049
 
999
1050
  # set up gap junctions
1000
1051
  pc = h.ParallelContext()
1001
1052
 
1002
- self.cell1 = getattr(h, self.cell_name)()
1003
- self.cell2 = getattr(h, self.cell_name)()
1053
+ # Use provided hoc_cell or create new cells
1054
+ if self.hoc_cell is not None:
1055
+ self.cell1 = self.hoc_cell
1056
+ # For gap junctions, we need two cells, so create a second one if using hoc_cell
1057
+ self.cell_name = conn_type_settings["cell"]
1058
+ self.cell2 = getattr(h, self.cell_name)()
1059
+ else:
1060
+ self.cell_name = conn_type_settings["cell"]
1061
+ self.cell1 = getattr(h, self.cell_name)()
1062
+ self.cell2 = getattr(h, self.cell_name)()
1004
1063
 
1005
1064
  self.icl = h.IClamp(self.cell1.soma[0](0.5))
1006
1065
  self.icl.delay = self.general_settings["tstart"]
@@ -1247,6 +1306,10 @@ class SynapseOptimizer:
1247
1306
  - max_amplitude: maximum synaptic response amplitude
1248
1307
  - rise_time: time for synaptic response to rise from 20% to 80% of peak
1249
1308
  - decay_time: time constant of synaptic response decay
1309
+ - latency: synaptic response latency
1310
+ - half_width: synaptic response half-width
1311
+ - baseline: baseline current
1312
+ - amp: peak amplitude from syn_props
1250
1313
  """
1251
1314
  # Set these to 0 for when we return the dict
1252
1315
  induction = 0
@@ -1255,11 +1318,22 @@ class SynapseOptimizer:
1255
1318
  amp = 0
1256
1319
  rise_time = 0
1257
1320
  decay_time = 0
1321
+ latency = 0
1322
+ half_width = 0
1323
+ baseline = 0
1324
+ syn_amp = 0
1258
1325
 
1259
1326
  if self.run_single_event:
1260
1327
  self.tuner.SingleEvent(plot_and_print=False)
1261
- rise_time = self.tuner.rise_time
1262
- decay_time = self.tuner.decay_time
1328
+ # Use the attributes set by SingleEvent method
1329
+ rise_time = getattr(self.tuner, "rise_time", 0)
1330
+ decay_time = getattr(self.tuner, "decay_time", 0)
1331
+ # Get additional syn_props directly
1332
+ syn_props = self.tuner._get_syn_prop()
1333
+ latency = syn_props.get("latency", 0)
1334
+ half_width = syn_props.get("half_width", 0)
1335
+ baseline = syn_props.get("baseline", 0)
1336
+ syn_amp = syn_props.get("amp", 0)
1263
1337
 
1264
1338
  if self.run_train_input:
1265
1339
  self.tuner._simulate_model(self.train_frequency, self.train_delay)
@@ -1276,6 +1350,10 @@ class SynapseOptimizer:
1276
1350
  "max_amplitude": float(amp),
1277
1351
  "rise_time": float(rise_time),
1278
1352
  "decay_time": float(decay_time),
1353
+ "latency": float(latency),
1354
+ "half_width": float(half_width),
1355
+ "baseline": float(baseline),
1356
+ "amp": float(syn_amp),
1279
1357
  }
1280
1358
 
1281
1359
  def _default_cost_function(
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.1
3
+ Version: 0.7.4
4
4
  Summary: BMTool
5
5
  Home-page: https://github.com/cyneuro/bmtool
6
6
  Download-URL:
@@ -36,6 +36,7 @@ Requires-Dist: requests
36
36
  Requires-Dist: pyyaml
37
37
  Requires-Dist: PyWavelets
38
38
  Requires-Dist: numba
39
+ Requires-Dist: tqdm
39
40
  Provides-Extra: dev
40
41
  Requires-Dist: ruff>=0.1.0; extra == "dev"
41
42
  Requires-Dist: pytest>=7.0.0; extra == "dev"
@@ -6,16 +6,16 @@ bmtool/graphs.py,sha256=gBTzI6c2BBK49dWGcfWh9c56TAooyn-KaiEy0Im1HcI,6717
6
6
  bmtool/manage.py,sha256=lsgRejp02P-x6QpA7SXcyXdalPhRmypoviIA2uAitQs,608
7
7
  bmtool/plot_commands.py,sha256=Dxm_RaT4CtHnfsltTtUopJ4KVbfhxtktEB_b7bFEXII,12716
8
8
  bmtool/singlecell.py,sha256=I2yolbAnNC8qpnRkNdnDCLidNW7CktmBuRrcowMZJ3A,45041
9
- bmtool/synapses.py,sha256=y8UJAqO1jpZY-mY9gVVMN8Dj1r9jD2fI1nAaNQeQfz4,66148
9
+ bmtool/synapses.py,sha256=-kg_TJoqXStIgE5iHpJWpXU6VRKT0YyIVpTw8frNbxA,69653
10
10
  bmtool/analysis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  bmtool/analysis/entrainment.py,sha256=NQloQtVpEWjDzmkZwMWVcm3hSjErHBZfQl1mrBVoIE8,25321
12
12
  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=L0UGOOsD4Iqxv492uSChpsFHIC_Ntz8X51ZfE_AJcP8,58216
16
+ bmtool/bmplot/connections.py,sha256=DwnnMp5cUwZC3OqsJqvgTPr4lBzc0sNWYRPmwij0WRk,59337
17
17
  bmtool/bmplot/entrainment.py,sha256=BrBMerqyiG2YWAO_OEFv7OJf3yeFz3l9jUt4NamluLc,32837
18
- bmtool/bmplot/lfp.py,sha256=SNpbWGOUnYEgnkeBw5S--aPN5mIGD22Gw2Pwus0_lvY,2034
18
+ bmtool/bmplot/lfp.py,sha256=7JLozQQJ19ty0ZNyfhkuJAr_K8_pVP9C0flVJd_YXaY,2027
19
19
  bmtool/bmplot/netcon_reports.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  bmtool/bmplot/spikes.py,sha256=odzCSMbFRHp9qthSGQ0WzMWUwNQ7R1Z6gLT6VPF_o5Q,15326
21
21
  bmtool/debug/__init__.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.1.dist-info/licenses/LICENSE,sha256=qrXg2jj6kz5d0EnN11hllcQt2fcWVNumx0xNbV05nyM,1068
30
- bmtool-0.7.2.1.dist-info/METADATA,sha256=rEXYmlS4RZxoXEK_2fV57B1c0CSNA38Eh4JmzQ1YGQ8,3577
31
- bmtool-0.7.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
32
- bmtool-0.7.2.1.dist-info/entry_points.txt,sha256=0-BHZ6nUnh0twWw9SXNTiRmKjDnb1VO2DfG_-oprhAc,45
33
- bmtool-0.7.2.1.dist-info/top_level.txt,sha256=gpd2Sj-L9tWbuJEd5E8C8S8XkNm5yUE76klUYcM-eWM,7
34
- bmtool-0.7.2.1.dist-info/RECORD,,
29
+ bmtool-0.7.4.dist-info/licenses/LICENSE,sha256=qrXg2jj6kz5d0EnN11hllcQt2fcWVNumx0xNbV05nyM,1068
30
+ bmtool-0.7.4.dist-info/METADATA,sha256=126Cn17YBt6twoBgMXYXK10kOWUXRhnLnGmPr7z7k_4,3595
31
+ bmtool-0.7.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
32
+ bmtool-0.7.4.dist-info/entry_points.txt,sha256=0-BHZ6nUnh0twWw9SXNTiRmKjDnb1VO2DfG_-oprhAc,45
33
+ bmtool-0.7.4.dist-info/top_level.txt,sha256=gpd2Sj-L9tWbuJEd5E8C8S8XkNm5yUE76klUYcM-eWM,7
34
+ bmtool-0.7.4.dist-info/RECORD,,