bmtool 0.8.4__tar.gz → 0.8.4.2__tar.gz

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.
Files changed (44) hide show
  1. {bmtool-0.8.4 → bmtool-0.8.4.2}/PKG-INFO +1 -1
  2. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/SLURM.py +240 -119
  3. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/analysis/spikes.py +63 -8
  4. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool.egg-info/PKG-INFO +1 -1
  5. {bmtool-0.8.4 → bmtool-0.8.4.2}/setup.py +1 -1
  6. {bmtool-0.8.4 → bmtool-0.8.4.2}/LICENSE +0 -0
  7. {bmtool-0.8.4 → bmtool-0.8.4.2}/README.md +0 -0
  8. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/__init__.py +0 -0
  9. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/__main__.py +0 -0
  10. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/analysis/__init__.py +0 -0
  11. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/analysis/entrainment.py +0 -0
  12. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/analysis/lfp.py +0 -0
  13. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/analysis/netcon_reports.py +0 -0
  14. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/bmplot/__init__.py +0 -0
  15. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/bmplot/connections.py +0 -0
  16. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/bmplot/entrainment.py +0 -0
  17. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/bmplot/lfp.py +0 -0
  18. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/bmplot/netcon_reports.py +0 -0
  19. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/bmplot/spikes.py +0 -0
  20. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/connectors.py +0 -0
  21. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/debug/__init__.py +0 -0
  22. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/debug/commands.py +0 -0
  23. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/debug/debug.py +0 -0
  24. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/graphs.py +0 -0
  25. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/manage.py +0 -0
  26. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/plot_commands.py +0 -0
  27. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/singlecell.py +0 -0
  28. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/stimulus/__init__.py +0 -0
  29. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/stimulus/assemblies.py +0 -0
  30. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/stimulus/core.py +0 -0
  31. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/stimulus/generators.py +0 -0
  32. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/synapses.py +0 -0
  33. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/util/__init__.py +0 -0
  34. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/util/commands.py +0 -0
  35. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/util/neuron/__init__.py +0 -0
  36. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/util/neuron/celltuner.py +0 -0
  37. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/util/util.py +0 -0
  38. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool.egg-info/SOURCES.txt +0 -0
  39. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool.egg-info/dependency_links.txt +0 -0
  40. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool.egg-info/entry_points.txt +0 -0
  41. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool.egg-info/requires.txt +0 -0
  42. {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool.egg-info/top_level.txt +0 -0
  43. {bmtool-0.8.4 → bmtool-0.8.4.2}/pyproject.toml +0 -0
  44. {bmtool-0.8.4 → bmtool-0.8.4.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bmtool
3
- Version: 0.8.4
3
+ Version: 0.8.4.2
4
4
  Summary: BMTool
5
5
  Home-page: https://github.com/cyneuro/bmtool
6
6
  Download-URL:
@@ -4,9 +4,11 @@ import os
4
4
  import shutil
5
5
  import subprocess
6
6
  import time
7
+ import uuid
7
8
 
8
9
  import numpy as np
9
10
  import requests
11
+ import bmtk.simulator.core.simulation_config as bmtk_config
10
12
 
11
13
 
12
14
  def check_job_status(job_id):
@@ -209,14 +211,16 @@ class SimulationBlock:
209
211
  self.status_list = status_list
210
212
  self.job_ids = []
211
213
  self.component_path = component_path
214
+ self.run_uuid = str(uuid.uuid4())[:8]
212
215
 
213
- def create_batch_script(self, case_name, command):
216
+ def create_batch_script(self, case_name, command, config_file=None):
214
217
  """
215
218
  Creates a SLURM batch script for the given simulation case.
216
219
 
217
220
  Args:
218
221
  case_name (str): Name of the simulation case.
219
222
  command (str): Command to run the simulation.
223
+ config_file (str): Optional path to the config file to use.
220
224
 
221
225
  Returns:
222
226
  str: Path to the batch script file.
@@ -233,9 +237,20 @@ class SimulationBlock:
233
237
  additional_commands_str = "\n".join(self.additional_commands)
234
238
  # Conditional account linegit
235
239
  account_line = f"#SBATCH --account={self.account}\n" if self.account else ""
236
- env_var_component_path = (
237
- f"export COMPONENT_PATH={self.component_path}" if self.component_path else ""
238
- )
240
+
241
+ # If a specific config file is provided, update the command to use it
242
+ if config_file and config_file in command:
243
+ # command already has config_file
244
+ pass
245
+ elif config_file:
246
+ # this is a bit naive, might need adjustment depending on how command is structured
247
+ parts = command.split()
248
+ for i, part in enumerate(parts):
249
+ if part.endswith(".json"):
250
+ parts[i] = config_file
251
+ break
252
+ command = " ".join(parts)
253
+
239
254
  mem_per_cpu = int(
240
255
  np.ceil(int(self.mem) / int(self.ntasks))
241
256
  ) # do ceil cause more mem is always better then less
@@ -257,9 +272,6 @@ class SimulationBlock:
257
272
  # Additional user-defined commands
258
273
  {additional_commands_str}
259
274
 
260
- #enviroment vars
261
- {env_var_component_path}
262
-
263
275
  export OUTPUT_DIR={case_output_dir}
264
276
 
265
277
  {command}
@@ -270,12 +282,16 @@ export OUTPUT_DIR={case_output_dir}
270
282
 
271
283
  return batch_script_path
272
284
 
273
- def submit_block(self):
285
+ def submit_block(self, config_files=None):
274
286
  """
275
287
  Submits all simulation cases in the block as separate SLURM jobs.
288
+
289
+ Args:
290
+ config_files (dict): Map of case_name to config_file path.
276
291
  """
277
292
  for case_name, command in self.simulation_cases.items():
278
- script_path = self.create_batch_script(case_name, command)
293
+ config_file = config_files.get(case_name) if config_files else None
294
+ script_path = self.create_batch_script(case_name, command, config_file=config_file)
279
295
  result = subprocess.run(["sbatch", script_path], capture_output=True, text=True)
280
296
  if result.returncode == 0:
281
297
  job_id = result.stdout.strip().split()[-1]
@@ -379,6 +395,34 @@ def globus_transfer(source_endpoint, dest_endpoint, source_path, dest_path):
379
395
  os.system(command)
380
396
 
381
397
 
398
+ def get_synaptic_params(path_to_syn_folder):
399
+ """Gets values of all json files and puts them into one list"""
400
+ combined_data = []
401
+ if not os.path.exists(path_to_syn_folder):
402
+ return combined_data
403
+ # List all files in the input folder
404
+ for filename in os.listdir(path_to_syn_folder):
405
+ if filename.endswith(".json"):
406
+ file_path = os.path.join(path_to_syn_folder, filename)
407
+
408
+ # Open and read the JSON file
409
+ try:
410
+ with open(file_path, "r") as file:
411
+ data = json.load(file)
412
+ # Append the filename and its data
413
+ combined_data.append({"filename": filename, "data": data})
414
+ except Exception as e:
415
+ print(f"Error reading {file_path}: {e}")
416
+ return combined_data
417
+
418
+
419
+ def save_synaptic_params(data, path_to_output_file):
420
+ """Saves combined json data into one file"""
421
+ os.makedirs(os.path.dirname(path_to_output_file), exist_ok=True)
422
+ with open(path_to_output_file, "w") as output_file:
423
+ json.dump(data, output_file, indent=4)
424
+
425
+
382
426
  class BlockRunner:
383
427
  """
384
428
  Class to handle submitting multiple blocks sequentially.
@@ -420,132 +464,209 @@ class BlockRunner:
420
464
  block.component_path = self.original_component_paths[i]
421
465
  print("Component paths restored to original values.", flush=True)
422
466
 
467
+ def _archive_block(self, block, config_files, circuit_configs, destination_dir):
468
+ """
469
+ Helper to move files after a block completes.
470
+ Moves cloned components, simulation configs, and circuit configs to the block output directory.
471
+ """
472
+ block_output_dir = os.path.join(block.output_base_dir, block.block_name)
473
+ os.makedirs(block_output_dir, exist_ok=True)
474
+
475
+ # Move components directory
476
+ archived_comp_path = os.path.join(block_output_dir, os.path.basename(destination_dir))
477
+ if os.path.exists(destination_dir):
478
+ shutil.move(destination_dir, archived_comp_path)
479
+ print(f"Archived components to {archived_comp_path}", flush=True)
480
+
481
+ # Move simulation configs
482
+ for config_path in config_files.values():
483
+ if os.path.exists(config_path):
484
+ shutil.move(config_path, os.path.join(block_output_dir, os.path.basename(config_path)))
485
+
486
+ # Move circuit configs
487
+ for c_config_path in circuit_configs.values():
488
+ if os.path.exists(c_config_path):
489
+ shutil.move(c_config_path, os.path.join(block_output_dir, os.path.basename(c_config_path)))
490
+
423
491
  def submit_blocks_sequentially(self):
424
492
  """
425
- Submits all blocks sequentially, ensuring each block starts only after the previous block has completed or is running.
426
- Updates the JSON file with new parameters before each block run.
493
+ Submits all blocks sequentially, ensuring each block starts only after the previous block has completed.
427
494
  """
428
495
  for i, block in enumerate(self.blocks):
429
- print(block.output_base_dir)
430
- # Update JSON file with new parameter value
431
- if self.json_file_path is None and self.param_values is None:
432
- source_dir = block.component_path
433
- destination_dir = f"{source_dir}{i+1}"
434
- block.component_path = destination_dir
435
- shutil.copytree(
436
- source_dir, destination_dir, dirs_exist_ok=True
437
- ) # create new components folder
438
- print(f"skipping json editing for block {block.block_name}", flush=True)
439
- else:
440
- if len(self.blocks) != len(self.param_values):
441
- raise Exception("Number of blocks needs to each number of params given")
442
- new_value = self.param_values[i]
443
- # hope this path is correct
444
- source_dir = block.component_path
445
- destination_dir = f"{source_dir}{i+1}"
446
- block.component_path = destination_dir
447
-
448
- shutil.copytree(
449
- source_dir, destination_dir, dirs_exist_ok=True
450
- ) # create new components folder
451
- json_file_path = os.path.join(destination_dir, self.json_file_path)
452
-
496
+ source_dir = block.component_path
497
+ destination_dir = os.path.join(os.getcwd(), f"components_{block.run_uuid}")
498
+ shutil.copytree(source_dir, destination_dir, dirs_exist_ok=True)
499
+
500
+ config_files = {}
501
+ circuit_configs = {}
502
+
503
+ # Keep track of the first circ_data for synaptic report generation
504
+ first_circ_data = None
505
+
506
+ for case_name, case_command in block.simulation_cases.items():
507
+ original_sim_config = next((p for p in case_command.split() if p.endswith(".json")), None)
508
+ if not original_sim_config: continue
509
+
510
+ # Put duplicate config in same directory as original to preserve relative path resolution
511
+ original_config_dir = os.path.dirname(os.path.abspath(original_sim_config))
512
+ new_sim_path = os.path.join(original_config_dir, f"simulation_config_{block.run_uuid}_{case_name}.json")
513
+ config_files[case_name] = new_sim_path
514
+
515
+ with open(original_sim_config, "r") as f:
516
+ sim_data = json.load(f)
517
+
518
+ # Use BMTK's config parser to resolve manifest variables like $BASE_DIR automatically
519
+ resolved_config = bmtk_config.from_json(original_sim_config)
520
+ original_circuit_config = resolved_config.get("network")
521
+
522
+ # Generate a unique circuit config for this case
523
+ new_circuit_path = os.path.join(os.getcwd(), f"circuit_config_{block.run_uuid}_{case_name}.json")
524
+ circuit_configs[case_name] = new_circuit_path
525
+
526
+ # Clone and update the circuit config for this case
527
+ with open(original_circuit_config, "r") as f:
528
+ circ_data = json.load(f)
529
+
530
+ if first_circ_data is None:
531
+ first_circ_data = circ_data
532
+
533
+ circ_data["manifest"]["$COMPONENTS_DIR"] = destination_dir
534
+
535
+ with open(new_circuit_path, "w") as f:
536
+ json.dump(circ_data, f, indent=4)
537
+
538
+ sim_data["network"] = new_circuit_path
539
+
540
+ # Update $OUTPUT_DIR in manifest to the correct block and case directory
541
+ block_case_output = os.path.join(block.output_base_dir, block.block_name, case_name)
542
+ sim_data["manifest"]["$OUTPUT_DIR"] = block_case_output
543
+ os.makedirs(block_case_output, exist_ok=True)
544
+
545
+ with open(new_sim_path, "w") as f:
546
+ json.dump(sim_data, f, indent=4)
547
+
548
+ # Apply parameter sweeps (once per block/destination_dir) BEFORE generating synaptic report
549
+ if self.json_file_path is not None and self.param_values is not None:
550
+ new_val = self.param_values[i]
551
+ target_json = os.path.join(destination_dir, self.json_file_path)
453
552
  if self.syn_dict is None:
454
- json_editor = seedSweep(json_file_path, self.param_name)
455
- json_editor.edit_json(new_value)
553
+ seedSweep(target_json, self.param_name).edit_json(new_val)
456
554
  else:
457
- # need to keep the orignal around
458
- syn_dict_temp = copy.deepcopy(self.syn_dict)
459
- json_to_be_ratioed = syn_dict_temp["json_file_path"]
460
- corrected_ratio_path = os.path.join(destination_dir, json_to_be_ratioed)
461
- syn_dict_temp["json_file_path"] = corrected_ratio_path
462
- json_editor = multiSeedSweep(
463
- json_file_path, self.param_name, syn_dict=syn_dict_temp, base_ratio=1
464
- )
465
- json_editor.edit_all_jsons(new_value)
466
-
467
- # Submit the block
555
+ sd_copy = copy.deepcopy(self.syn_dict)
556
+ sd_copy["json_file_path"] = os.path.join(destination_dir, sd_copy["json_file_path"])
557
+ multiSeedSweep(target_json, self.param_name, syn_dict=sd_copy, base_ratio=1).edit_all_jsons(new_val)
558
+
559
+ # Generate synaptic report (once per block) AFTER parameter sweeps for accuracy
560
+ syn_models_rel = first_circ_data["components"]["synaptic_models_dir"].replace("$COMPONENTS_DIR/", "")
561
+ syn_data = get_synaptic_params(os.path.join(destination_dir, syn_models_rel))
562
+ block_output_dir = os.path.join(block.output_base_dir, block.block_name)
563
+ os.makedirs(block_output_dir, exist_ok=True)
564
+ save_synaptic_params(syn_data, os.path.join(block_output_dir, f"synaptic_report_{block.run_uuid}.json"))
565
+
566
+ print(f"Submitting block: {block.block_name}", flush=True)
567
+
468
568
  print(f"Submitting block: {block.block_name}", flush=True)
469
- block.submit_block()
569
+ block.submit_block(config_files=config_files)
570
+
470
571
  if self.webhook:
471
- message = f"SIMULATION UPDATE: Block {i} has been submitted! There are {(len(self.blocks)-1)-i} left to be submitted"
472
- send_teams_message(self.webhook, message)
473
-
474
- # Wait for the block to complete
475
- if i == len(self.blocks) - 1:
476
- while not block.check_block_status():
477
- print(f"Waiting for the last block {i} to complete...")
478
- time.sleep(self.check_interval)
479
- else: # Not the last block so if job is running lets start a new one (checks status list)
480
- while not block.check_block_status():
481
- print(f"Waiting for block {i} to complete...")
482
- time.sleep(self.check_interval)
483
-
484
- print(f"Block {block.block_name} completed.", flush=True)
485
- print("All blocks are done!", flush=True)
486
- # Restore component paths to their original values
572
+ send_teams_message(self.webhook, f"Submitted {block.block_name}. {len(self.blocks)-1-i} remaining.")
573
+
574
+ while not block.check_block_status():
575
+ time.sleep(self.check_interval)
576
+
577
+ self._archive_block(block, config_files, circuit_configs, destination_dir)
578
+
487
579
  self.restore_component_paths()
488
580
  if self.webhook:
489
- message = "SIMULATION UPDATE: Simulation are Done!"
490
- send_teams_message(self.webhook, message)
581
+ send_teams_message(self.webhook, "Simulations complete.")
491
582
 
492
583
  def submit_blocks_parallel(self):
493
584
  """
494
- submits all the blocks at once onto the queue. To do this the components dir will be cloned and each block will have its own.
495
- Also the json_file_path should be the path after the components dir
585
+ Submits all blocks to the queue simultaneously and archives them as they finish.
496
586
  """
497
- for i, block in enumerate(self.blocks):
498
- if self.param_values is None:
499
- source_dir = block.component_path
500
- destination_dir = f"{source_dir}{i+1}"
501
- block.component_path = destination_dir
502
- shutil.copytree(
503
- source_dir, destination_dir, dirs_exist_ok=True
504
- ) # create new components folder
505
- print(f"skipping json editing for block {block.block_name}", flush=True)
506
- else:
507
- if block.component_path is None:
508
- raise Exception(
509
- "Unable to use parallel submitter without defining the component path"
510
- )
511
- new_value = self.param_values[i]
512
-
513
- source_dir = block.component_path
514
- destination_dir = f"{source_dir}{i+1}"
515
- block.component_path = destination_dir
516
-
517
- shutil.copytree(
518
- source_dir, destination_dir, dirs_exist_ok=True
519
- ) # create new components folder
520
- json_file_path = os.path.join(destination_dir, self.json_file_path)
587
+ active_trackers = [] # List of (block, config_files, circuit_configs, destination_dir)
521
588
 
589
+ for i, block in enumerate(self.blocks):
590
+ source_dir = block.component_path
591
+ destination_dir = os.path.join(os.getcwd(), f"components_{block.run_uuid}")
592
+ shutil.copytree(source_dir, destination_dir, dirs_exist_ok=True)
593
+
594
+ config_files = {}
595
+ circuit_configs = {}
596
+
597
+ # Keep track of the first circ_data for synaptic report generation
598
+ first_circ_data = None
599
+
600
+ for case_name, case_command in block.simulation_cases.items():
601
+ original_sim_config = next((p for p in case_command.split() if p.endswith(".json")), None)
602
+ if not original_sim_config: continue
603
+
604
+ # Put duplicate config in same directory as original to preserve relative path resolution
605
+ original_config_dir = os.path.dirname(os.path.abspath(original_sim_config))
606
+ new_sim_path = os.path.join(original_config_dir, f"simulation_config_{block.run_uuid}_{case_name}.json")
607
+ config_files[case_name] = new_sim_path
608
+
609
+ with open(original_sim_config, "r") as f:
610
+ sim_data = json.load(f)
611
+
612
+ # Use BMTK's config parser to resolve manifest variables like $BASE_DIR automatically
613
+ resolved_config = bmtk_config.from_json(original_sim_config)
614
+ original_circuit_config = resolved_config.get("network")
615
+
616
+ # Generate a unique circuit config for this case
617
+ new_circuit_path = os.path.join(os.getcwd(), f"circuit_config_{block.run_uuid}_{case_name}.json")
618
+ circuit_configs[case_name] = new_circuit_path
619
+
620
+ # Clone and update the circuit config for this case
621
+ with open(original_circuit_config, "r") as f:
622
+ circ_data = json.load(f)
623
+
624
+ if first_circ_data is None:
625
+ first_circ_data = circ_data
626
+
627
+ circ_data["manifest"]["$COMPONENTS_DIR"] = destination_dir
628
+
629
+ with open(new_circuit_path, "w") as f:
630
+ json.dump(circ_data, f, indent=4)
631
+
632
+ sim_data["network"] = new_circuit_path
633
+
634
+ # Update $OUTPUT_DIR in manifest to the correct block and case directory
635
+ block_case_output = os.path.join(block.output_base_dir, block.block_name, case_name)
636
+ sim_data["manifest"]["$OUTPUT_DIR"] = block_case_output
637
+ os.makedirs(block_case_output, exist_ok=True)
638
+
639
+ with open(new_sim_path, "w") as f:
640
+ json.dump(sim_data, f, indent=4)
641
+
642
+ # Apply parameter sweeps (once per block/destination_dir) BEFORE generating synaptic report
643
+ if self.json_file_path is not None and self.param_values is not None:
644
+ target_json = os.path.join(destination_dir, self.json_file_path)
522
645
  if self.syn_dict is None:
523
- json_editor = seedSweep(json_file_path, self.param_name)
524
- json_editor.edit_json(new_value)
646
+ seedSweep(target_json, self.param_name).edit_json(self.param_values[i])
525
647
  else:
526
- # need to keep the orignal around
527
- syn_dict_temp = copy.deepcopy(self.syn_dict)
528
- json_to_be_ratioed = syn_dict_temp["json_file_path"]
529
- corrected_ratio_path = os.path.join(destination_dir, json_to_be_ratioed)
530
- syn_dict_temp["json_file_path"] = corrected_ratio_path
531
- json_editor = multiSeedSweep(
532
- json_file_path, self.param_name, syn_dict_temp, base_ratio=1
533
- )
534
- json_editor.edit_all_jsons(new_value)
535
- # submit block with new component path
536
- print(f"Submitting block: {block.block_name}", flush=True)
537
- block.submit_block()
538
- if i == len(self.blocks) - 1:
539
- print(
540
- "\nEverything has been submitted. You can close out of this or keep this script running to get a message when everything is finished\n"
541
- )
542
- while not block.check_block_status():
543
- print(f"Waiting for the last block {i} to complete...")
544
- time.sleep(self.check_interval)
545
-
546
- print("All blocks are done!", flush=True)
547
- # Restore component paths to their original values
648
+ sd_copy = copy.deepcopy(self.syn_dict)
649
+ sd_copy["json_file_path"] = os.path.join(destination_dir, sd_copy["json_file_path"])
650
+ multiSeedSweep(target_json, self.param_name, syn_dict=sd_copy, base_ratio=1).edit_all_jsons(self.param_values[i])
651
+
652
+ # Generate synaptic report (once per block) AFTER parameter sweeps for accuracy
653
+ syn_models_rel = first_circ_data["components"]["synaptic_models_dir"].replace("$COMPONENTS_DIR/", "")
654
+ syn_data = get_synaptic_params(os.path.join(destination_dir, syn_models_rel))
655
+ block_output_dir = os.path.join(block.output_base_dir, block.block_name)
656
+ os.makedirs(block_output_dir, exist_ok=True)
657
+ save_synaptic_params(syn_data, os.path.join(block_output_dir, f"synaptic_report_{block.run_uuid}.json"))
658
+
659
+ block.submit_block(config_files=config_files)
660
+ active_trackers.append((block, config_files, circuit_configs, destination_dir))
661
+
662
+ while active_trackers:
663
+ for tracker in active_trackers[:]:
664
+ block, cfgs, c_cfgs, d_dir = tracker
665
+ if block.check_block_status():
666
+ self._archive_block(block, cfgs, c_cfgs, d_dir)
667
+ active_trackers.remove(tracker)
668
+ time.sleep(self.check_interval)
669
+
548
670
  self.restore_component_paths()
549
671
  if self.webhook:
550
- message = "SIMULATION UPDATE: Simulations are Done!"
551
- send_teams_message(self.webhook, message)
672
+ send_teams_message(self.webhook, "All parallel blocks complete.")
@@ -553,7 +553,10 @@ def compare_firing_over_times(
553
553
 
554
554
 
555
555
  def find_bursting_cells(
556
- df: pd.DataFrame, isi_threshold: float = 10, burst_count_threshold: int = 1
556
+ df: pd.DataFrame,
557
+ isi_threshold: float = 10,
558
+ burst_count_threshold: int = 1,
559
+ groupby: str = "pop_name",
557
560
  ) -> pd.DataFrame:
558
561
  """
559
562
  Finds bursting cells in a population based on a time difference threshold.
@@ -561,16 +564,18 @@ def find_bursting_cells(
561
564
  Parameters
562
565
  ----------
563
566
  df : pd.DataFrame
564
- DataFrame containing spike data with columns for timestamps, node_ids, and pop_name
567
+ DataFrame containing spike data with columns for timestamps, node_ids, and specified groupby
565
568
  isi_threshold : float, optional
566
569
  Time difference threshold in milliseconds to identify bursts
567
570
  burst_count_threshold : int, optional
568
571
  Number of bursts required to identify a bursting cell
572
+ groupby : str, optional
573
+ The column to group by and append the burster suffix to
569
574
 
570
575
  Returns
571
576
  -------
572
577
  pd.DataFrame
573
- DataFrame with bursting cells renamed in their pop_name column
578
+ DataFrame with bursting cells renamed in their specified groupby column
574
579
  """
575
580
  # Create a new DataFrame with the time differences
576
581
  diff_df = df.copy()
@@ -581,7 +586,9 @@ def find_bursting_cells(
581
586
 
582
587
  # Group by node_ids and check if any row has a burst instance
583
588
  # check if there are enough bursts
584
- burst_summary = diff_df.groupby("node_ids")["is_burst_instance"].sum() >= burst_count_threshold
589
+ burst_summary = (
590
+ diff_df.groupby("node_ids")["is_burst_instance"].sum() >= burst_count_threshold
591
+ )
585
592
 
586
593
  # Convert to a DataFrame with reset index
587
594
  burst_cells = burst_summary.reset_index(name="is_burst")
@@ -590,21 +597,69 @@ def find_bursting_cells(
590
597
  burst_cells = pd.merge(burst_cells, df, on="node_ids")
591
598
 
592
599
  # Create a mask for burst cells that don't already have "_bursters" in their name
593
- burst_mask = burst_cells["is_burst"] & ~burst_cells["pop_name"].str.contains(
600
+ burst_mask = burst_cells["is_burst"] & ~burst_cells[groupby].str.contains(
594
601
  "_bursters", na=False
595
602
  )
596
603
 
597
604
  # Add "_bursters" suffix only to those cells
598
- burst_cells.loc[burst_mask, "pop_name"] = burst_cells.loc[burst_mask, "pop_name"] + "_bursters"
605
+ burst_cells.loc[burst_mask, groupby] = burst_cells.loc[burst_mask, groupby] + "_bursters"
599
606
 
600
- for pop in sorted(burst_cells["pop_name"].unique()):
607
+ for pop in sorted(burst_cells[groupby].unique()):
601
608
  print(
602
- f"Number of cells in {pop}: {burst_cells[burst_cells['pop_name'] == pop]['node_ids'].nunique()}"
609
+ f"Number of cells in {pop}: {burst_cells[burst_cells[groupby] == pop]['node_ids'].nunique()}"
603
610
  )
604
611
 
605
612
  return burst_cells
606
613
 
607
614
 
615
+ def find_burst_times(timestamps: pd.Series, isi_threshold: float = 10):
616
+ """
617
+ Finds the start times of bursts from a series of timestamps.
618
+ A burst start is defined as the first spike in a sequence where the ISI is below the threshold.
619
+
620
+ Parameters
621
+ ----------
622
+ timestamps : pd.Series
623
+ Series of timestamps for a single node
624
+ isi_threshold : float, optional
625
+ Time difference threshold in milliseconds to identify bursts (default unit in bmtk)
626
+
627
+ Returns
628
+ -------
629
+ list, int
630
+ A list of burst start times and the total number of bursts found
631
+
632
+ Examples
633
+ --------
634
+ >>> test_cell = spikes_df[spikes_df['node_ids'] == 1]
635
+ >>> burst_starts, count = find_burst_times(test_cell['timestamps'])
636
+ """
637
+ if len(timestamps) < 2:
638
+ return [], 0
639
+
640
+ # Calculate ISIs
641
+ isi = timestamps.diff()
642
+ is_burst_instance = isi < isi_threshold
643
+
644
+ # Find where sequences of is_burst_instance start
645
+ # A burst starts if current is a burst instance but previous was NOT
646
+ # OR it's the very first burst instance in the series
647
+ burst_starts = []
648
+ in_burst = False
649
+
650
+ for i in range(1, len(timestamps)):
651
+ if is_burst_instance.iloc[i]:
652
+ if not in_burst:
653
+ # This is the start of a new burst sequence
654
+ # The burst actually starts at the PREVIOUS spike
655
+ burst_starts.append(timestamps.iloc[i - 1])
656
+ in_burst = True
657
+ else:
658
+ in_burst = False
659
+
660
+ return burst_starts, len(burst_starts)
661
+
662
+
608
663
  def find_highest_firing_cells(
609
664
  df: pd.DataFrame, upper_quantile: float, groupby: str = "pop_name"
610
665
  ) -> pd.DataFrame:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bmtool
3
- Version: 0.8.4
3
+ Version: 0.8.4.2
4
4
  Summary: BMTool
5
5
  Home-page: https://github.com/cyneuro/bmtool
6
6
  Download-URL:
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setup(
7
7
  name="bmtool",
8
- version="0.8.4",
8
+ version="0.8.4.2",
9
9
  author="Neural Engineering Laboratory at the University of Missouri",
10
10
  author_email="gregglickert@mail.missouri.edu",
11
11
  description="BMTool",
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes