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.
- {bmtool-0.8.4 → bmtool-0.8.4.2}/PKG-INFO +1 -1
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/SLURM.py +240 -119
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/analysis/spikes.py +63 -8
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool.egg-info/PKG-INFO +1 -1
- {bmtool-0.8.4 → bmtool-0.8.4.2}/setup.py +1 -1
- {bmtool-0.8.4 → bmtool-0.8.4.2}/LICENSE +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/README.md +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/__init__.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/__main__.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/analysis/__init__.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/analysis/entrainment.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/analysis/lfp.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/analysis/netcon_reports.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/bmplot/__init__.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/bmplot/connections.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/bmplot/entrainment.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/bmplot/lfp.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/bmplot/netcon_reports.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/bmplot/spikes.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/connectors.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/debug/__init__.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/debug/commands.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/debug/debug.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/graphs.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/manage.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/plot_commands.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/singlecell.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/stimulus/__init__.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/stimulus/assemblies.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/stimulus/core.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/stimulus/generators.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/synapses.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/util/__init__.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/util/commands.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/util/neuron/__init__.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/util/neuron/celltuner.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool/util/util.py +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool.egg-info/SOURCES.txt +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool.egg-info/dependency_links.txt +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool.egg-info/entry_points.txt +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool.egg-info/requires.txt +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/bmtool.egg-info/top_level.txt +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/pyproject.toml +0 -0
- {bmtool-0.8.4 → bmtool-0.8.4.2}/setup.cfg +0 -0
|
@@ -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
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
#
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
455
|
-
json_editor.edit_json(new_value)
|
|
553
|
+
seedSweep(target_json, self.param_name).edit_json(new_val)
|
|
456
554
|
else:
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
524
|
-
json_editor.edit_json(new_value)
|
|
646
|
+
seedSweep(target_json, self.param_name).edit_json(self.param_values[i])
|
|
525
647
|
else:
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
block.submit_block()
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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 =
|
|
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[
|
|
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,
|
|
605
|
+
burst_cells.loc[burst_mask, groupby] = burst_cells.loc[burst_mask, groupby] + "_bursters"
|
|
599
606
|
|
|
600
|
-
for pop in sorted(burst_cells[
|
|
607
|
+
for pop in sorted(burst_cells[groupby].unique()):
|
|
601
608
|
print(
|
|
602
|
-
f"Number of cells in {pop}: {burst_cells[burst_cells[
|
|
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:
|
|
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
|
|
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
|