bmtool 0.7.4.1__py3-none-any.whl → 0.7.5.1__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/singlecell.py CHANGED
@@ -10,6 +10,8 @@ import pandas as pd
10
10
  from neuron import h
11
11
  from scipy.optimize import curve_fit
12
12
 
13
+ from bmtool.util.util import load_templates_from_config, load_config
14
+
13
15
 
14
16
  def load_biophys1():
15
17
  """
@@ -1025,19 +1027,62 @@ class ZAP(CurrentClamp):
1025
1027
 
1026
1028
 
1027
1029
  class Profiler:
1028
- """All in one single cell profiler"""
1030
+ """All in one single cell profiler
1031
+
1032
+ This Profiler now supports being initialized with either explicit
1033
+ `template_dir` and `mechanism_dir` paths or with a BMTK `config` file
1034
+ (which should contain `components.templates_dir` and
1035
+ `components.mechanisms_dir`). When `config` is provided it will be used
1036
+ to load mechanisms and templates via the utility helpers.
1037
+ """
1029
1038
 
1030
- def __init__(self, template_dir: str = None, mechanism_dir: str = None, dt=None):
1039
+ def __init__(self, template_dir: str = None, mechanism_dir: str = None, dt=None, config: str = None):
1040
+ # initialize to None and then prefer config-derived paths if provided
1031
1041
  self.template_dir = None
1032
1042
  self.mechanism_dir = None
1033
1043
 
1034
- if not self.template_dir:
1035
- self.template_dir = template_dir
1036
- if not self.mechanism_dir:
1037
- self.mechanism_dir = mechanism_dir
1038
- self.templates = None
1044
+ # If a BMTK config is provided, load mechanisms/templates from it
1045
+ if config is not None:
1046
+ try:
1047
+ # load and apply the config values for directories
1048
+ conf = load_config(config)
1049
+ # conf behaves like a dict returned by bmtk Config.from_json
1050
+ try:
1051
+ comps = conf["components"]
1052
+ except Exception:
1053
+ comps = getattr(conf, "components", None)
1054
+
1055
+ if comps is not None:
1056
+ # support dict-like and object-like components
1057
+ try:
1058
+ self.template_dir = comps.get("templates_dir")
1059
+ except Exception:
1060
+ self.template_dir = getattr(comps, "templates_dir", None)
1061
+ try:
1062
+ self.mechanism_dir = comps.get("mechanisms_dir")
1063
+ except Exception:
1064
+ self.mechanism_dir = getattr(comps, "mechanisms_dir", None)
1065
+
1066
+ # actually load mechanisms and templates using the helper
1067
+ load_templates_from_config(config)
1068
+ except Exception:
1069
+ # fall back to explicit dirs if config parsing/loading fails
1070
+ pass
1071
+
1072
+ else:
1073
+ # fall back to explicit args if not set by config
1074
+ if not self.template_dir:
1075
+ self.template_dir = template_dir
1076
+ if not self.mechanism_dir:
1077
+ self.mechanism_dir = mechanism_dir
1078
+
1079
+ # template_dir is required for loading templates later
1080
+ if self.template_dir is None:
1081
+ raise ValueError("Profiler requires either 'template_dir' or a 'config' containing components.templates_dir")
1082
+
1083
+ self.templates = None
1039
1084
 
1040
- self.load_templates()
1085
+ self.load_templates()
1041
1086
 
1042
1087
  h.load_file("stdrun.hoc")
1043
1088
  if dt is not None:
bmtool/synapses.py CHANGED
@@ -8,6 +8,7 @@ import ipywidgets as widgets
8
8
  import matplotlib.pyplot as plt
9
9
  import neuron
10
10
  import numpy as np
11
+ import pandas as pd
11
12
  from IPython.display import clear_output, display
12
13
  from ipywidgets import HBox, VBox
13
14
  from neuron import h
@@ -18,10 +19,10 @@ from scipy.optimize import curve_fit, minimize, minimize_scalar
18
19
  from scipy.signal import find_peaks
19
20
  from tqdm.notebook import tqdm
20
21
 
21
- from bmtool.util.util import load_templates_from_config
22
+ from bmtool.util.util import load_templates_from_config, load_nodes_from_config, load_edges_from_config, load_config
22
23
 
23
24
  DEFAULT_GENERAL_SETTINGS = {
24
- "vclamp": True,
25
+ "vclamp": False,
25
26
  "rise_interval": (0.1, 0.9),
26
27
  "tstart": 500.0,
27
28
  "tdur": 100.0,
@@ -43,17 +44,18 @@ DEFAULT_GAP_JUNCTION_GENERAL_SETTINGS = {
43
44
  class SynapseTuner:
44
45
  def __init__(
45
46
  self,
47
+ conn_type_settings: Optional[Dict[str, dict]] = None,
48
+ connection: Optional[str] = None,
49
+ current_name: str = "i",
46
50
  mechanisms_dir: Optional[str] = None,
47
51
  templates_dir: Optional[str] = None,
48
52
  config: Optional[str] = None,
49
- conn_type_settings: Optional[dict] = None,
50
- connection: Optional[str] = None,
51
53
  general_settings: Optional[dict] = None,
52
54
  json_folder_path: Optional[str] = None,
53
- current_name: str = "i",
54
- other_vars_to_record: Optional[list] = None,
55
- slider_vars: Optional[list] = None,
55
+ other_vars_to_record: Optional[List[str]] = None,
56
+ slider_vars: Optional[List[str]] = None,
56
57
  hoc_cell: Optional[object] = None,
58
+ network: Optional[str] = None,
57
59
  ) -> None:
58
60
  """
59
61
  Initialize the SynapseTuner class with connection type settings, mechanisms, and template directories.
@@ -80,8 +82,28 @@ class SynapseTuner:
80
82
  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.
81
83
  hoc_cell : Optional[object]
82
84
  An already loaded NEURON cell object. If provided, template loading and cell setup will be skipped.
85
+ network : Optional[str]
86
+ Name of the specific network dataset to access from the loaded edges data (e.g., 'network_to_network').
87
+ If not provided, will use all available networks. When a config file is provided, this enables
88
+ the network dropdown feature in InteractiveTuner for switching between different networks.
89
+
90
+ Network Dropdown Feature:
91
+ -------------------------
92
+ When initialized with a BMTK config file, the tuner automatically:
93
+ 1. Loads all available network datasets from the config
94
+ 2. Creates a network dropdown in InteractiveTuner (if multiple networks exist)
95
+ 3. Allows dynamic switching between networks, which rebuilds connection types
96
+ 4. Updates connection dropdown options when network is changed
97
+ 5. Preserves current connection if it exists in the new network, otherwise selects the first available
83
98
  """
84
99
  self.hoc_cell = hoc_cell
100
+ # Store config and network information for network dropdown functionality
101
+ self.config = config # Store config path for network dropdown functionality
102
+ self.available_networks = [] # Store available networks from config file
103
+ self.current_network = network # Store current network selection
104
+ # Cache for loaded dynamics params JSON by filename to avoid repeated disk reads
105
+ self._syn_params_cache = {}
106
+ h.load_file('stdrun.hoc')
85
107
 
86
108
  if hoc_cell is None:
87
109
  if config is None and (mechanisms_dir is None or templates_dir is None):
@@ -95,11 +117,37 @@ class SynapseTuner:
95
117
  else:
96
118
  # loads both mech and templates
97
119
  load_templates_from_config(config)
120
+ # Load available networks from config for network dropdown feature
121
+ self._load_available_networks()
122
+ # Prebuild connection type settings for each available network to
123
+ # make network switching in the UI fast. This will make __init__ slower
124
+ # but dramatically speed up response when changing the network dropdown.
125
+ self._prebuilt_conn_type_settings = {}
126
+ try:
127
+ for net in self.available_networks:
128
+ self._prebuilt_conn_type_settings[net] = self._build_conn_type_settings_from_config(config, network=net)
129
+ except Exception as e:
130
+ print(f"Warning: error prebuilding conn_type_settings for networks: {e}")
98
131
 
99
132
  if conn_type_settings is None:
100
- raise ValueError("conn_type_settings must be provided.")
133
+ if config is not None:
134
+ print("Building conn_type_settings from BMTK config files...")
135
+ # If we prebuilt per-network settings, use the one for the requested network
136
+ if hasattr(self, '_prebuilt_conn_type_settings') and network in getattr(self, '_prebuilt_conn_type_settings', {}):
137
+ conn_type_settings = self._prebuilt_conn_type_settings[network]
138
+ else:
139
+ conn_type_settings = self._build_conn_type_settings_from_config(config, network=network)
140
+ print(f"Found {len(conn_type_settings)} connection types: {list(conn_type_settings.keys())}")
141
+
142
+ # If connection is not specified, use the first available connection
143
+ if connection is None and conn_type_settings:
144
+ connection = list(conn_type_settings.keys())[0]
145
+ print(f"No connection specified, using first available: {connection}")
146
+ else:
147
+ raise ValueError("conn_type_settings must be provided if config is not specified.")
148
+
101
149
  if connection is None:
102
- raise ValueError("connection must be provided.")
150
+ raise ValueError("connection must be provided or inferable from conn_type_settings.")
103
151
  if connection not in conn_type_settings:
104
152
  raise ValueError(f"connection '{connection}' not found in conn_type_settings.")
105
153
 
@@ -113,12 +161,20 @@ class SynapseTuner:
113
161
  else:
114
162
  # Merge defaults with user-provided
115
163
  self.general_settings = {**DEFAULT_GENERAL_SETTINGS, **general_settings}
164
+
165
+ # Store the initial connection name and set up connection
166
+ self.current_connection = connection
116
167
  self.conn = self.conn_type_settings[connection]
168
+ self._current_cell_type = self.conn["spec_settings"]["post_cell"]
117
169
  self.synaptic_props = self.conn["spec_syn_param"]
118
170
  self.vclamp = self.general_settings["vclamp"]
119
171
  self.current_name = current_name
120
172
  self.other_vars_to_record = other_vars_to_record or []
121
173
  self.ispk = None
174
+ self.input_mode = False # Add input_mode attribute
175
+
176
+ # Store original slider_vars for connection switching
177
+ self.original_slider_vars = slider_vars or list(self.synaptic_props.keys())
122
178
 
123
179
  if slider_vars:
124
180
  # Start by filtering based on keys in slider_vars
@@ -174,6 +230,497 @@ class SynapseTuner:
174
230
 
175
231
  self._set_up_recorders()
176
232
 
233
+ def _build_conn_type_settings_from_config(self, config_path: str, node_set: Optional[str] = None, network: Optional[str] = None) -> Dict[str, dict]:
234
+ """
235
+ Build conn_type_settings from BMTK simulation and circuit config files using the method used by relation matrix function in util.
236
+
237
+ Parameters:
238
+ -----------
239
+ config_path : str
240
+ Path to the simulation config JSON file.
241
+ node_set : Optional[str]
242
+ Specific node set to filter connections for. If None, processes all connections.
243
+ network : Optional[str]
244
+ Name of the specific network dataset to access (e.g., 'network_to_network').
245
+ If None, processes all available networks.
246
+
247
+ Returns:
248
+ --------
249
+ Dict[str, dict]
250
+ Dictionary with connection names as keys and connection settings as values.
251
+
252
+ NOTE: a lot of this code could probs be made a bit more simple or just removed i kinda tried a bunch of things and it works now
253
+ but is kinda complex and some code is probs note needed
254
+
255
+ """
256
+ # Load configuration and get nodes and edges using util.py methods
257
+ config = load_config(config_path)
258
+ # Ensure the config dict knows its source path so path substitutions can be resolved
259
+ try:
260
+ # load_config may return a dict; store path used so callers can resolve $COMPONENTS_DIR
261
+ config['config_path'] = config_path
262
+ except Exception:
263
+ pass
264
+ nodes = load_nodes_from_config(config_path)
265
+ edges = load_edges_from_config(config_path)
266
+
267
+ conn_type_settings = {}
268
+
269
+ # If a specific network is requested, only process that one
270
+ if network:
271
+ if network not in edges:
272
+ print(f"Warning: Network '{network}' not found in edges. Available networks: {list(edges.keys())}")
273
+ return conn_type_settings
274
+ edge_datasets = {network: edges[network]}
275
+ else:
276
+ edge_datasets = edges
277
+
278
+ # Process each edge dataset using the util.py approach
279
+ for edge_dataset_name, edge_df in edge_datasets.items():
280
+ if edge_df.empty:
281
+ continue
282
+
283
+ # Create merged DataFrames with source and target node information like util.py does
284
+ source_node_df = None
285
+ target_node_df = None
286
+
287
+ # First, try to deterministically parse the edge_dataset_name for patterns like '<src>_to_<tgt>'
288
+ # e.g., 'network_to_network', 'extnet_to_network'
289
+ if '_to_' in edge_dataset_name:
290
+ parts = edge_dataset_name.split('_to_')
291
+ if len(parts) == 2:
292
+ src_name, tgt_name = parts
293
+ if src_name in nodes:
294
+ source_node_df = nodes[src_name].add_prefix('source_')
295
+ if tgt_name in nodes:
296
+ target_node_df = nodes[tgt_name].add_prefix('target_')
297
+
298
+ # If not found by parsing name, fall back to inspecting a sample edge row which contains
299
+ # explicit 'source_population' and 'target_population' fields (this avoids reversing source/target)
300
+ if source_node_df is None or target_node_df is None:
301
+ sample_edge = edge_df.iloc[0] if len(edge_df) > 0 else None
302
+ if sample_edge is not None:
303
+ # Use explicit population names from the edge entry
304
+ source_pop_name = sample_edge.get('source_population', '')
305
+ target_pop_name = sample_edge.get('target_population', '')
306
+ if source_pop_name in nodes:
307
+ source_node_df = nodes[source_pop_name].add_prefix('source_')
308
+ if target_pop_name in nodes:
309
+ target_node_df = nodes[target_pop_name].add_prefix('target_')
310
+
311
+ # As a last resort, attempt to heuristically match by prefix/suffix of the dataset name
312
+ if source_node_df is None or target_node_df is None:
313
+ for pop_name, node_df in nodes.items():
314
+ if source_node_df is None and (edge_dataset_name.startswith(pop_name) or edge_dataset_name.endswith(pop_name)):
315
+ source_node_df = node_df.add_prefix('source_')
316
+ elif target_node_df is None and (edge_dataset_name.startswith(pop_name) or edge_dataset_name.endswith(pop_name)):
317
+ target_node_df = node_df.add_prefix('target_')
318
+
319
+ # If we still don't have the node data, skip this edge dataset
320
+ if source_node_df is None or target_node_df is None:
321
+ print(f"Warning: Could not find node data for edge dataset {edge_dataset_name}")
322
+ continue
323
+
324
+ # Merge edge data with source node info
325
+ edges_with_source = pd.merge(
326
+ edge_df.reset_index(),
327
+ source_node_df,
328
+ how='left',
329
+ left_on='source_node_id',
330
+ right_index=True
331
+ )
332
+
333
+ # Merge with target node info
334
+ edges_with_nodes = pd.merge(
335
+ edges_with_source,
336
+ target_node_df,
337
+ how='left',
338
+ left_on='target_node_id',
339
+ right_index=True
340
+ )
341
+
342
+ # Get unique edge types from the merged dataset
343
+ if 'edge_type_id' in edges_with_nodes.columns:
344
+ edge_types = edges_with_nodes['edge_type_id'].unique()
345
+ else:
346
+ edge_types = [0] # Single edge type
347
+
348
+ # Process each edge type
349
+ for edge_type_id in edge_types:
350
+ # Filter edges for this type
351
+ if 'edge_type_id' in edges_with_nodes.columns:
352
+ edge_type_data = edges_with_nodes[edges_with_nodes['edge_type_id'] == edge_type_id]
353
+ else:
354
+ edge_type_data = edges_with_nodes
355
+
356
+ if len(edge_type_data) == 0:
357
+ continue
358
+
359
+ # Get representative edge for this type
360
+ edge_info = edge_type_data.iloc[0]
361
+
362
+ # Skip gap junctions
363
+ if 'is_gap_junction' in edge_info and pd.notna(edge_info['is_gap_junction']) and edge_info['is_gap_junction']:
364
+ continue
365
+
366
+ # Get population names from the merged data (this is the key improvement!)
367
+ source_pop = edge_info.get('source_pop_name', '')
368
+ target_pop = edge_info.get('target_pop_name', '')
369
+
370
+ # Get target cell template from the merged data
371
+ target_model_template = edge_info.get('target_model_template', '')
372
+ if target_model_template.startswith('hoc:'):
373
+ target_cell_type = target_model_template.replace('hoc:', '')
374
+ else:
375
+ target_cell_type = target_model_template
376
+
377
+ # Create connection name using the actual population names
378
+ if source_pop and target_pop:
379
+ conn_name = f"{source_pop}2{target_pop}"
380
+ else:
381
+ conn_name = f"{edge_dataset_name}_type_{edge_type_id}"
382
+
383
+ # Get synaptic model template
384
+ model_template = edge_info.get('model_template', 'exp2syn')
385
+
386
+ # Build connection settings early so we can attach metadata like dynamics file name
387
+ conn_settings = {
388
+ 'spec_settings': {
389
+ 'post_cell': target_cell_type,
390
+ 'vclamp_amp': -70.0, # Default voltage clamp amplitude
391
+ 'sec_x': 0.5, # Default location on section
392
+ 'sec_id': 0, # Default to soma
393
+ # level_of_detail may be overridden by dynamics params below
394
+ 'level_of_detail': model_template,
395
+ },
396
+ 'spec_syn_param': {}
397
+ }
398
+
399
+ # Load synaptic parameters from dynamics_params file if available.
400
+ # NOTE: the edge DataFrame produced by load_edges_from_config/load_sonata_edges_to_dataframe
401
+ # already contains the 'dynamics_params' column (from the CSV) or the
402
+ # flattened H5 dynamics_params attributes (prefixed with 'dynamics_params/').
403
+ # Prefer the direct 'dynamics_params' column value from the merged DataFrame
404
+ # rather than performing ad-hoc string parsing here.
405
+ syn_params = {}
406
+ dynamics_file_name = None
407
+ # Prefer a top-level 'dynamics_params' column if present
408
+ if 'dynamics_params' in edge_info and pd.notna(edge_info.get('dynamics_params')):
409
+ val = edge_info.get('dynamics_params')
410
+ # Some CSV loaders can produce bytes or numpy types; coerce to str
411
+ try:
412
+ dynamics_file_name = str(val).strip()
413
+ except Exception:
414
+ dynamics_file_name = None
415
+
416
+ # If we found a dynamics file name, use it directly (skip token parsing)
417
+ if dynamics_file_name and dynamics_file_name.upper() != 'NULL':
418
+ try:
419
+ conn_settings['spec_settings']['dynamics_params_file'] = dynamics_file_name
420
+ # use a cache to avoid re-reading the same JSON multiple times
421
+ if dynamics_file_name in self._syn_params_cache:
422
+ syn_params = self._syn_params_cache[dynamics_file_name]
423
+ else:
424
+ syn_params = self._load_synaptic_params_from_config(config, dynamics_file_name)
425
+ # cache result (even if empty dict) to avoid repeated lookups
426
+ self._syn_params_cache[dynamics_file_name] = syn_params
427
+ except Exception as e:
428
+ print(f"Warning: could not load dynamics_params file '{dynamics_file_name}' for edge {edge_dataset_name}: {e}")
429
+
430
+ # If a dynamics params JSON filename was provided, prefer using its basename
431
+ # as the connection name so that the UI matches the JSON definitions.
432
+ if dynamics_file_name:
433
+ try:
434
+ json_base = os.path.splitext(os.path.basename(dynamics_file_name))[0]
435
+ # Ensure uniqueness in conn_type_settings
436
+ if json_base in conn_type_settings:
437
+ # Append edge_type_id to disambiguate
438
+ json_base = f"{json_base}_type_{edge_type_id}"
439
+ conn_name = json_base
440
+ except Exception:
441
+ pass
442
+
443
+ # If the dynamics params defined a level_of_detail, override the default
444
+ if isinstance(syn_params, dict) and 'level_of_detail' in syn_params:
445
+ conn_settings['spec_settings']['level_of_detail'] = syn_params.get('level_of_detail', model_template)
446
+
447
+ # Add synaptic parameters, excluding level_of_detail
448
+ for key, value in syn_params.items():
449
+ if key != 'level_of_detail':
450
+ conn_settings['spec_syn_param'][key] = value
451
+ else:
452
+ # Fallback: some SONATA/H5 edge files expose dynamics params as flattened
453
+ # columns named like 'dynamics_params/<param>'. If no filename was given,
454
+ # gather any such columns from edge_info and use them as spec_syn_param.
455
+ for col in edge_info.index:
456
+ if isinstance(col, str) and col.startswith('dynamics_params/'):
457
+ param_key = col.split('/', 1)[1]
458
+ try:
459
+ val = edge_info[col]
460
+ if pd.notna(val):
461
+ conn_settings['spec_syn_param'][param_key] = val
462
+ except Exception:
463
+ # Ignore malformed entries
464
+ pass
465
+
466
+ # Add weight from edge info if available
467
+ if 'syn_weight' in edge_info and pd.notna(edge_info['syn_weight']):
468
+ conn_settings['spec_syn_param']['initW'] = float(edge_info['syn_weight'])
469
+
470
+ # Handle afferent section information
471
+ if 'afferent_section_id' in edge_info and pd.notna(edge_info['afferent_section_id']):
472
+ conn_settings['spec_settings']['sec_id'] = int(edge_info['afferent_section_id'])
473
+
474
+ if 'afferent_section_pos' in edge_info and pd.notna(edge_info['afferent_section_pos']):
475
+ conn_settings['spec_settings']['sec_x'] = float(edge_info['afferent_section_pos'])
476
+
477
+ # Store in connection settings
478
+ conn_type_settings[conn_name] = conn_settings
479
+
480
+ return conn_type_settings
481
+
482
+ def _load_available_networks(self) -> None:
483
+ """
484
+ Load available network names from the config file for the network dropdown feature.
485
+
486
+ This method is automatically called during initialization when a config file is provided.
487
+ It populates the available_networks list which enables the network dropdown in
488
+ InteractiveTuner when multiple networks are available.
489
+
490
+ Network Dropdown Behavior:
491
+ -------------------------
492
+ - If only one network exists: No network dropdown is shown
493
+ - If multiple networks exist: Network dropdown appears next to connection dropdown
494
+ - Networks are loaded from the edges data in the config file
495
+ - Current network defaults to the first available if not specified during init
496
+ """
497
+ if self.config is None:
498
+ self.available_networks = []
499
+ return
500
+
501
+ try:
502
+ edges = load_edges_from_config(self.config)
503
+ self.available_networks = list(edges.keys())
504
+
505
+ # Set current network to first available if not specified
506
+ if self.current_network is None and self.available_networks:
507
+ self.current_network = self.available_networks[0]
508
+ except Exception as e:
509
+ print(f"Warning: Could not load networks from config: {e}")
510
+ self.available_networks = []
511
+
512
+ def _load_synaptic_params_from_config(self, config: dict, dynamics_params: str) -> dict:
513
+ """
514
+ Load synaptic parameters from dynamics params file using config information.
515
+
516
+ Parameters:
517
+ -----------
518
+ config : dict
519
+ BMTK configuration dictionary
520
+ dynamics_params : str
521
+ Dynamics parameters filename
522
+
523
+ Returns:
524
+ --------
525
+ dict
526
+ Synaptic parameters dictionary
527
+ """
528
+ try:
529
+ # Get the synaptic models directory from config
530
+ synaptic_models_dir = config.get('components', {}).get('synaptic_models_dir', '')
531
+ if synaptic_models_dir:
532
+ # Handle path variables
533
+ if synaptic_models_dir.startswith('$'):
534
+ # This is a placeholder, try to resolve it
535
+ config_dir = os.path.dirname(config.get('config_path', ''))
536
+ synaptic_models_dir = synaptic_models_dir.replace('$COMPONENTS_DIR',
537
+ os.path.join(config_dir, 'components'))
538
+ synaptic_models_dir = synaptic_models_dir.replace('$BASE_DIR', config_dir)
539
+
540
+ dynamics_file = os.path.join(synaptic_models_dir, dynamics_params)
541
+
542
+ if os.path.exists(dynamics_file):
543
+ with open(dynamics_file, 'r') as f:
544
+ return json.load(f)
545
+ else:
546
+ print(f"Warning: Dynamics params file not found: {dynamics_file}")
547
+ except Exception as e:
548
+ print(f"Warning: Error loading synaptic parameters: {e}")
549
+
550
+ return {}
551
+
552
+ @classmethod
553
+ def list_connections_from_config(cls, config_path: str, network: Optional[str] = None) -> Dict[str, dict]:
554
+ """
555
+ Class method to list all available connections from a BMTK config file without creating a tuner.
556
+
557
+ Parameters:
558
+ -----------
559
+ config_path : str
560
+ Path to the simulation config JSON file.
561
+ network : Optional[str]
562
+ Name of the specific network dataset to access (e.g., 'network_to_network').
563
+ If None, processes all available networks.
564
+
565
+ Returns:
566
+ --------
567
+ Dict[str, dict]
568
+ Dictionary with connection names as keys and connection info as values.
569
+ """
570
+ # Create a temporary instance just to use the parsing methods
571
+ temp_tuner = cls.__new__(cls) # Create without calling __init__
572
+ conn_type_settings = temp_tuner._build_conn_type_settings_from_config(config_path, network=network)
573
+
574
+ # Create a summary of connections with key info
575
+ connections_summary = {}
576
+ for conn_name, settings in conn_type_settings.items():
577
+ connections_summary[conn_name] = {
578
+ 'post_cell': settings['spec_settings']['post_cell'],
579
+ 'synapse_type': settings['spec_settings']['level_of_detail'],
580
+ 'parameters': list(settings['spec_syn_param'].keys())
581
+ }
582
+
583
+ return connections_summary
584
+
585
+ def _switch_connection(self, new_connection: str) -> None:
586
+ """
587
+ Switch to a different connection type and update all related properties.
588
+
589
+ Parameters:
590
+ -----------
591
+ new_connection : str
592
+ Name of the new connection type to switch to.
593
+ """
594
+ if new_connection not in self.conn_type_settings:
595
+ raise ValueError(f"Connection '{new_connection}' not found in conn_type_settings.")
596
+
597
+ # Update current connection
598
+ self.current_connection = new_connection
599
+ self.conn = self.conn_type_settings[new_connection]
600
+ self.synaptic_props = self.conn["spec_syn_param"]
601
+
602
+ # Update slider vars for new connection
603
+ if hasattr(self, 'original_slider_vars'):
604
+ # Filter slider vars based on new connection's parameters
605
+ self.slider_vars = {
606
+ key: value for key, value in self.synaptic_props.items()
607
+ if key in self.original_slider_vars
608
+ }
609
+
610
+ # Check for missing keys and try to get them from the synapse
611
+ for key in self.original_slider_vars:
612
+ if key not in self.synaptic_props:
613
+ try:
614
+ # We'll get this after recreating the synapse
615
+ pass
616
+ except AttributeError as e:
617
+ print(f"Warning: Could not access '{key}' for connection '{new_connection}': {e}")
618
+ else:
619
+ self.slider_vars = self.synaptic_props
620
+
621
+ # Need to recreate the cell if it's different
622
+ if self.hoc_cell is None:
623
+ # Check if we need a different cell type
624
+ new_cell_type = self.conn["spec_settings"]["post_cell"]
625
+ if not hasattr(self, '_current_cell_type') or self._current_cell_type != new_cell_type:
626
+ self._current_cell_type = new_cell_type
627
+ self._set_up_cell()
628
+
629
+ # Recreate synapse for new connection
630
+ self._set_up_synapse()
631
+
632
+ # Update any missing slider vars from the new synapse
633
+ if hasattr(self, 'original_slider_vars'):
634
+ for key in self.original_slider_vars:
635
+ if key not in self.synaptic_props:
636
+ try:
637
+ value = getattr(self.syn, key)
638
+ self.slider_vars[key] = value
639
+ except AttributeError as e:
640
+ print(f"Warning: Could not access '{key}' for connection '{new_connection}': {e}")
641
+
642
+ # Recreate NetCon connections with new synapse
643
+ self.nc = h.NetCon(
644
+ self.nstim,
645
+ self.syn,
646
+ self.general_settings["threshold"],
647
+ self.general_settings["delay"],
648
+ self.general_settings["weight"],
649
+ )
650
+ self.nc2 = h.NetCon(
651
+ self.nstim2,
652
+ self.syn,
653
+ self.general_settings["threshold"],
654
+ self.general_settings["delay"],
655
+ self.general_settings["weight"],
656
+ )
657
+
658
+ # Recreate voltage clamp with potentially new cell
659
+ self.vcl = h.VClamp(self.cell.soma[0](0.5))
660
+
661
+ # Recreate recorders for new synapse
662
+ self._set_up_recorders()
663
+
664
+ # Reset NEURON state
665
+ h.finitialize()
666
+
667
+ print(f"Successfully switched to connection: {new_connection}")
668
+
669
+ def _switch_network(self, new_network: str) -> None:
670
+ """
671
+ Switch to a different network and rebuild conn_type_settings for the new network.
672
+
673
+ This method is called when the user selects a different network from the network
674
+ dropdown in InteractiveTuner. It performs a complete rebuild of the connection
675
+ types available for the new network.
676
+
677
+ Parameters:
678
+ -----------
679
+ new_network : str
680
+ Name of the new network to switch to.
681
+
682
+ Network Switching Process:
683
+ -------------------------
684
+ 1. Validates the new network exists in available_networks
685
+ 2. Rebuilds conn_type_settings using the new network's edge data
686
+ 3. Updates the connection dropdown with new network's available connections
687
+ 4. Preserves current connection if it exists in new network
688
+ 5. Falls back to first available connection if current doesn't exist
689
+ 6. Recreates synapses and NEURON objects for the new connection
690
+ 7. Updates UI components to reflect the changes
691
+ """
692
+ if new_network not in self.available_networks:
693
+ print(f"Warning: Network '{new_network}' not found in available networks: {self.available_networks}")
694
+ return
695
+
696
+ if new_network == self.current_network:
697
+ return # No change needed
698
+
699
+ # Update current network
700
+ self.current_network = new_network
701
+
702
+ # Switch conn_type_settings using prebuilt data if available, otherwise build on-demand
703
+ if self.config:
704
+ print(f"Switching connections for network: {new_network}")
705
+ if hasattr(self, '_prebuilt_conn_type_settings') and new_network in self._prebuilt_conn_type_settings:
706
+ self.conn_type_settings = self._prebuilt_conn_type_settings[new_network]
707
+ else:
708
+ # Fallback: build on-demand (slower)
709
+ self.conn_type_settings = self._build_conn_type_settings_from_config(self.config, network=new_network)
710
+
711
+ # Update available connections and select first one if current doesn't exist
712
+ available_connections = list(self.conn_type_settings.keys())
713
+ if self.current_connection not in available_connections and available_connections:
714
+ self.current_connection = available_connections[0]
715
+ print(f"Connection '{self.current_connection}' not available in new network. Switched to: {available_connections[0]}")
716
+
717
+ # Switch to the (potentially new) connection
718
+ if self.current_connection in self.conn_type_settings:
719
+ self._switch_connection(self.current_connection)
720
+
721
+ print(f"Successfully switched to network: {new_network}")
722
+ print(f"Available connections: {available_connections}")
723
+
177
724
  def _update_spec_syn_param(self, json_folder_path: str) -> None:
178
725
  """
179
726
  Update specific synaptic parameters using JSON files located in the specified folder.
@@ -213,11 +760,14 @@ class SynapseTuner:
213
760
  - `_set_up_cell()` should be called before setting up the synapse.
214
761
  - Synapse location, type, and properties are specified within `spec_syn_param` and `spec_settings`.
215
762
  """
216
- self.syn = getattr(h, self.conn["spec_settings"]["level_of_detail"])(
217
- list(self.cell.all)[self.conn["spec_settings"]["sec_id"]](
218
- self.conn["spec_settings"]["sec_x"]
763
+ try:
764
+ self.syn = getattr(h, self.conn["spec_settings"]["level_of_detail"])(
765
+ list(self.cell.all)[self.conn["spec_settings"]["sec_id"]](
766
+ self.conn["spec_settings"]["sec_x"]
767
+ )
219
768
  )
220
- )
769
+ except:
770
+ raise Exception("Make sure the mod file exist you are trying to load check spelling!")
221
771
  for key, value in self.conn["spec_syn_param"].items():
222
772
  if isinstance(value, (int, float)):
223
773
  if hasattr(self.syn, key):
@@ -741,6 +1291,8 @@ class SynapseTuner:
741
1291
  Sets up interactive sliders for tuning short-term plasticity (STP) parameters in a Jupyter Notebook.
742
1292
 
743
1293
  This method creates an interactive UI with sliders for:
1294
+ - Network selection dropdown (if multiple networks available and config provided)
1295
+ - Connection type selection dropdown
744
1296
  - Input frequency
745
1297
  - Delay between pulse trains
746
1298
  - Duration of stimulation (for continuous input mode)
@@ -752,10 +1304,21 @@ class SynapseTuner:
752
1304
  - Toggling voltage clamp mode
753
1305
  - Switching between standard and continuous input modes
754
1306
 
1307
+ Network Dropdown Feature:
1308
+ ------------------------
1309
+ When the SynapseTuner is initialized with a BMTK config file containing multiple networks:
1310
+ - A network dropdown appears next to the connection dropdown
1311
+ - Users can dynamically switch between networks (e.g., 'network_to_network', 'external_to_network')
1312
+ - Switching networks rebuilds available connections and updates the connection dropdown
1313
+ - The current connection is preserved if it exists in the new network
1314
+ - If multiple networks exist but only one is specified during init, that network is used as default
1315
+
755
1316
  Notes:
756
1317
  ------
757
1318
  Ideal for exploratory parameter tuning and interactive visualization of
758
1319
  synapse behavior with different parameter values and stimulation protocols.
1320
+ The network dropdown feature enables comprehensive exploration of multi-network
1321
+ BMTK simulations without needing to reinitialize the tuner.
759
1322
  """
760
1323
  # Widgets setup (Sliders)
761
1324
  freqs = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 35, 50, 100, 200]
@@ -766,6 +1329,26 @@ class SynapseTuner:
766
1329
  duration0 = 300
767
1330
  vlamp_status = self.vclamp
768
1331
 
1332
+ # Connection dropdown
1333
+ connection_options = list(self.conn_type_settings.keys())
1334
+ w_connection = widgets.Dropdown(
1335
+ options=connection_options,
1336
+ value=self.current_connection,
1337
+ description="Connection:",
1338
+ style={'description_width': 'initial'}
1339
+ )
1340
+
1341
+ # Network dropdown - only shown if config was provided and multiple networks are available
1342
+ # This enables users to switch between different network datasets dynamically
1343
+ w_network = None
1344
+ if self.config is not None and len(self.available_networks) > 1:
1345
+ w_network = widgets.Dropdown(
1346
+ options=self.available_networks,
1347
+ value=self.current_network,
1348
+ description="Network:",
1349
+ style={'description_width': 'initial'}
1350
+ )
1351
+
769
1352
  w_run = widgets.Button(description="Run Train", icon="history", button_style="primary")
770
1353
  w_single = widgets.Button(description="Single Event", icon="check", button_style="success")
771
1354
  w_vclamp = widgets.ToggleButton(
@@ -774,6 +1357,17 @@ class SynapseTuner:
774
1357
  icon="fast-backward",
775
1358
  button_style="warning",
776
1359
  )
1360
+
1361
+ # Voltage clamp amplitude input
1362
+ default_vclamp_amp = getattr(self.conn['spec_settings'], 'vclamp_amp', -70.0)
1363
+ w_vclamp_amp = widgets.FloatText(
1364
+ value=default_vclamp_amp,
1365
+ description="V_clamp (mV):",
1366
+ step=5.0,
1367
+ style={'description_width': 'initial'},
1368
+ layout=widgets.Layout(width='150px')
1369
+ )
1370
+
777
1371
  w_input_mode = widgets.ToggleButton(
778
1372
  value=False, description="Continuous input", icon="eject", button_style="info"
779
1373
  )
@@ -785,54 +1379,178 @@ class SynapseTuner:
785
1379
  options=durations, value=duration0, description="Duration"
786
1380
  )
787
1381
 
1382
+ def create_dynamic_sliders():
1383
+ """Create sliders based on current connection's parameters"""
1384
+ sliders = {}
1385
+ for key, value in self.slider_vars.items():
1386
+ if isinstance(value, (int, float)): # Only create sliders for numeric values
1387
+ if hasattr(self.syn, key):
1388
+ if value == 0:
1389
+ print(
1390
+ f"{key} was set to zero, going to try to set a range of values, try settings the {key} to a nonzero value if you dont like the range!"
1391
+ )
1392
+ slider = widgets.FloatSlider(
1393
+ value=value, min=0, max=1000, step=1, description=key
1394
+ )
1395
+ else:
1396
+ slider = widgets.FloatSlider(
1397
+ value=value, min=0, max=value * 20, step=value / 5, description=key
1398
+ )
1399
+ sliders[key] = slider
1400
+ else:
1401
+ print(f"skipping slider for {key} due to not being a synaptic variable")
1402
+ return sliders
1403
+
788
1404
  # Generate sliders dynamically based on valid numeric entries in self.slider_vars
789
- self.dynamic_sliders = {}
1405
+ self.dynamic_sliders = create_dynamic_sliders()
790
1406
  print(
791
1407
  "Setting up slider! The sliders ranges are set by their init value so try changing that if you dont like the slider range!"
792
1408
  )
793
- for key, value in self.slider_vars.items():
794
- if isinstance(value, (int, float)): # Only create sliders for numeric values
795
- if hasattr(self.syn, key):
796
- if value == 0:
797
- print(
798
- f"{key} was set to zero, going to try to set a range of values, try settings the {key} to a nonzero value if you dont like the range!"
799
- )
800
- slider = widgets.FloatSlider(
801
- value=value, min=0, max=1000, step=1, description=key
802
- )
803
- else:
804
- slider = widgets.FloatSlider(
805
- value=value, min=0, max=value * 20, step=value / 5, description=key
806
- )
807
- self.dynamic_sliders[key] = slider
808
- else:
809
- print(f"skipping slider for {key} due to not being a synaptic variable")
810
1409
 
1410
+ # Create output widget for displaying results
1411
+ output_widget = widgets.Output()
1412
+
811
1413
  def run_single_event(*args):
812
1414
  clear_output()
813
1415
  display(ui)
1416
+ display(output_widget)
1417
+
814
1418
  self.vclamp = w_vclamp.value
1419
+ # Update voltage clamp amplitude if voltage clamp is enabled
1420
+ if self.vclamp:
1421
+ # Update the voltage clamp amplitude settings
1422
+ self.conn['spec_settings']['vclamp_amp'] = w_vclamp_amp.value
1423
+ # Update general settings if they exist
1424
+ if hasattr(self, 'general_settings'):
1425
+ self.general_settings['vclamp_amp'] = w_vclamp_amp.value
815
1426
  # Update synaptic properties based on slider values
816
1427
  self.ispk = None
817
- self.SingleEvent()
1428
+
1429
+ # Clear previous results and run simulation
1430
+ output_widget.clear_output()
1431
+ with output_widget:
1432
+ self.SingleEvent()
1433
+
1434
+ def on_connection_change(*args):
1435
+ """Handle connection dropdown change"""
1436
+ try:
1437
+ new_connection = w_connection.value
1438
+ if new_connection != self.current_connection:
1439
+ # Switch to new connection
1440
+ self._switch_connection(new_connection)
1441
+
1442
+ # Recreate dynamic sliders for new connection
1443
+ self.dynamic_sliders = create_dynamic_sliders()
1444
+
1445
+ # Update UI
1446
+ update_ui_layout()
1447
+ update_ui()
1448
+
1449
+ except Exception as e:
1450
+ print(f"Error switching connection: {e}")
1451
+
1452
+ def on_network_change(*args):
1453
+ """
1454
+ Handle network dropdown change events.
1455
+
1456
+ This callback is triggered when the user selects a different network from
1457
+ the network dropdown. It coordinates the complete switching process:
1458
+ 1. Calls _switch_network() to rebuild connections for the new network
1459
+ 2. Updates the connection dropdown options with new network's connections
1460
+ 3. Recreates dynamic sliders for the new connection parameters
1461
+ 4. Refreshes the entire UI to reflect all changes
1462
+ """
1463
+ if w_network is None:
1464
+ return
1465
+ try:
1466
+ new_network = w_network.value
1467
+ if new_network != self.current_network:
1468
+ # Switch to new network
1469
+ self._switch_network(new_network)
1470
+
1471
+ # Update connection dropdown options with new network's connections
1472
+ connection_options = list(self.conn_type_settings.keys())
1473
+ w_connection.options = connection_options
1474
+ if connection_options:
1475
+ w_connection.value = self.current_connection
1476
+
1477
+ # Recreate dynamic sliders for new connection
1478
+ self.dynamic_sliders = create_dynamic_sliders()
1479
+
1480
+ # Update UI
1481
+ update_ui_layout()
1482
+ update_ui()
1483
+
1484
+ except Exception as e:
1485
+ print(f"Error switching network: {e}")
1486
+
1487
+ def update_ui_layout():
1488
+ """
1489
+ Update the UI layout with new sliders and network dropdown.
1490
+
1491
+ This function reconstructs the entire UI layout including:
1492
+ - Network dropdown (if available) and connection dropdown in the top row
1493
+ - Button controls and input mode toggles
1494
+ - Parameter sliders arranged in columns
1495
+ """
1496
+ nonlocal ui, slider_columns
1497
+
1498
+ # Add the dynamic sliders to the UI
1499
+ slider_widgets = [slider for slider in self.dynamic_sliders.values()]
1500
+
1501
+ if slider_widgets:
1502
+ half = len(slider_widgets) // 2
1503
+ col1 = VBox(slider_widgets[:half])
1504
+ col2 = VBox(slider_widgets[half:])
1505
+ slider_columns = HBox([col1, col2])
1506
+ else:
1507
+ slider_columns = VBox([])
1508
+
1509
+ # Create button row with voltage clamp controls
1510
+ if w_vclamp.value: # Show voltage clamp amplitude input when toggle is on
1511
+ button_row = HBox([w_run, w_single, w_vclamp, w_vclamp_amp, w_input_mode])
1512
+ else: # Hide voltage clamp amplitude input when toggle is off
1513
+ button_row = HBox([w_run, w_single, w_vclamp, w_input_mode])
1514
+
1515
+ # Construct the top row - include network dropdown if available
1516
+ # This creates a horizontal layout with network dropdown (if present) and connection dropdown
1517
+ if w_network is not None:
1518
+ connection_row = HBox([w_network, w_connection])
1519
+ else:
1520
+ connection_row = HBox([w_connection])
1521
+ slider_row = HBox([w_input_freq, self.w_delay, self.w_duration])
1522
+
1523
+ ui = VBox([connection_row, button_row, slider_row, slider_columns])
818
1524
 
819
1525
  # Function to update UI based on input mode
820
1526
  def update_ui(*args):
821
1527
  clear_output()
822
1528
  display(ui)
1529
+ display(output_widget)
1530
+
823
1531
  self.vclamp = w_vclamp.value
1532
+ # Update voltage clamp amplitude if voltage clamp is enabled
1533
+ if self.vclamp:
1534
+ self.conn['spec_settings']['vclamp_amp'] = w_vclamp_amp.value
1535
+ if hasattr(self, 'general_settings'):
1536
+ self.general_settings['vclamp_amp'] = w_vclamp_amp.value
1537
+
824
1538
  self.input_mode = w_input_mode.value
825
1539
  syn_props = {var: slider.value for var, slider in self.dynamic_sliders.items()}
826
1540
  self._set_syn_prop(**syn_props)
827
- if not self.input_mode:
828
- self._simulate_model(w_input_freq.value, self.w_delay.value, w_vclamp.value)
829
- else:
830
- self._simulate_model(w_input_freq.value, self.w_duration.value, w_vclamp.value)
831
- amp = self._response_amplitude()
832
- self._plot_model(
833
- [self.general_settings["tstart"] - self.nstim.interval / 3, self.tstop]
834
- )
835
- _ = self._calc_ppr_induction_recovery(amp)
1541
+
1542
+ # Clear previous results and run simulation
1543
+ output_widget.clear_output()
1544
+ with output_widget:
1545
+ if not self.input_mode:
1546
+ self._simulate_model(w_input_freq.value, self.w_delay.value, w_vclamp.value)
1547
+ else:
1548
+ self._simulate_model(w_input_freq.value, self.w_duration.value, w_vclamp.value)
1549
+ amp = self._response_amplitude()
1550
+ self._plot_model(
1551
+ [self.general_settings["tstart"] - self.nstim.interval / 3, self.tstop]
1552
+ )
1553
+ _ = self._calc_ppr_induction_recovery(amp)
836
1554
 
837
1555
  # Function to switch between delay and duration sliders
838
1556
  def switch_slider(*args):
@@ -843,8 +1561,21 @@ class SynapseTuner:
843
1561
  self.w_delay.layout.display = "" # Show delay slider
844
1562
  self.w_duration.layout.display = "none" # Hide duration slider
845
1563
 
846
- # Link input mode to slider switch
1564
+ # Function to handle voltage clamp toggle
1565
+ def on_vclamp_toggle(*args):
1566
+ """Handle voltage clamp toggle changes to show/hide amplitude input"""
1567
+ update_ui_layout()
1568
+ clear_output()
1569
+ display(ui)
1570
+ display(output_widget)
1571
+
1572
+ # Link widgets to their callback functions
1573
+ w_connection.observe(on_connection_change, names="value")
1574
+ # Link network dropdown callback only if network dropdown was created
1575
+ if w_network is not None:
1576
+ w_network.observe(on_network_change, names="value")
847
1577
  w_input_mode.observe(switch_slider, names="value")
1578
+ w_vclamp.observe(on_vclamp_toggle, names="value")
848
1579
 
849
1580
  # Hide the duration slider initially until the user selects it
850
1581
  self.w_duration.layout.display = "none" # Hide duration slider
@@ -852,18 +1583,10 @@ class SynapseTuner:
852
1583
  w_single.on_click(run_single_event)
853
1584
  w_run.on_click(update_ui)
854
1585
 
855
- # Add the dynamic sliders to the UI
856
- slider_widgets = [slider for slider in self.dynamic_sliders.values()]
857
-
858
- button_row = HBox([w_run, w_single, w_vclamp, w_input_mode])
859
- slider_row = HBox([w_input_freq, self.w_delay, self.w_duration])
860
-
861
- half = len(slider_widgets) // 2
862
- col1 = VBox(slider_widgets[:half])
863
- col2 = VBox(slider_widgets[half:])
864
- slider_columns = HBox([col1, col2])
865
-
866
- ui = VBox([button_row, slider_row, slider_columns])
1586
+ # Initial UI setup
1587
+ slider_columns = VBox([])
1588
+ ui = VBox([])
1589
+ update_ui_layout()
867
1590
 
868
1591
  display(ui)
869
1592
  update_ui()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bmtool
3
- Version: 0.7.4.1
3
+ Version: 0.7.5.1
4
4
  Summary: BMTool
5
5
  Home-page: https://github.com/cyneuro/bmtool
6
6
  Download-URL:
@@ -5,8 +5,8 @@ bmtool/connectors.py,sha256=4SJRqBXM145_-nFycGrnlfjSyaoarOr0QkHl00jWq4I,74384
5
5
  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
- bmtool/singlecell.py,sha256=I2yolbAnNC8qpnRkNdnDCLidNW7CktmBuRrcowMZJ3A,45041
9
- bmtool/synapses.py,sha256=Ayz4swAx0gNG-KtsCDJn3XJ7vw-qDuvSHy6xeIzd9ok,69697
8
+ bmtool/singlecell.py,sha256=xqdLM2TjjnL8YyTy-c3WR6mElTv3E4zkZgkfUh4S5X0,47161
9
+ bmtool/synapses.py,sha256=k-xyZjElz2CHM2oGXqBnFOCd0NusoA6JASzPXHPCj5c,106068
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
@@ -26,9 +26,9 @@ bmtool/util/commands.py,sha256=Nn-R-4e9g8ZhSPZvTkr38xeKRPfEMANB9Lugppj82UI,68564
26
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.4.1.dist-info/licenses/LICENSE,sha256=qrXg2jj6kz5d0EnN11hllcQt2fcWVNumx0xNbV05nyM,1068
30
- bmtool-0.7.4.1.dist-info/METADATA,sha256=TKDtODjH25ZRw2vYbHZWER_RuoAkIOEA3G2Hhpz6PHc,3597
31
- bmtool-0.7.4.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
32
- bmtool-0.7.4.1.dist-info/entry_points.txt,sha256=0-BHZ6nUnh0twWw9SXNTiRmKjDnb1VO2DfG_-oprhAc,45
33
- bmtool-0.7.4.1.dist-info/top_level.txt,sha256=gpd2Sj-L9tWbuJEd5E8C8S8XkNm5yUE76klUYcM-eWM,7
34
- bmtool-0.7.4.1.dist-info/RECORD,,
29
+ bmtool-0.7.5.1.dist-info/licenses/LICENSE,sha256=qrXg2jj6kz5d0EnN11hllcQt2fcWVNumx0xNbV05nyM,1068
30
+ bmtool-0.7.5.1.dist-info/METADATA,sha256=H1XxiaHJuU-Bq_HTck5_z1fm08rTmSNxZYBA7hVcmNk,3597
31
+ bmtool-0.7.5.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
32
+ bmtool-0.7.5.1.dist-info/entry_points.txt,sha256=0-BHZ6nUnh0twWw9SXNTiRmKjDnb1VO2DfG_-oprhAc,45
33
+ bmtool-0.7.5.1.dist-info/top_level.txt,sha256=gpd2Sj-L9tWbuJEd5E8C8S8XkNm5yUE76klUYcM-eWM,7
34
+ bmtool-0.7.5.1.dist-info/RECORD,,