bluecellulab 2.6.61__tar.gz → 2.6.77__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 (118) hide show
  1. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/.github/workflows/test.yml +1 -1
  2. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/.gitignore +4 -1
  3. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/PKG-INFO +10 -5
  4. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/README.rst +2 -2
  5. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/analysis/analysis.py +118 -65
  6. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/cell/core.py +259 -11
  7. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/cell/injector.py +25 -12
  8. bluecellulab-2.6.77/bluecellulab/cell/section_tools.py +96 -0
  9. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/circuit/circuit_access/bluepy_circuit_access.py +2 -1
  10. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/circuit/circuit_access/sonata_circuit_access.py +2 -2
  11. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/circuit/config/bluepy_simulation_config.py +15 -1
  12. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/circuit/config/definition.py +12 -0
  13. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/circuit/config/sonata_simulation_config.py +69 -3
  14. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/circuit/node_id.py +1 -1
  15. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/circuit/simulation_access.py +29 -7
  16. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/circuit_simulation.py +268 -139
  17. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/connection.py +13 -5
  18. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/plotwindow.py +1 -1
  19. bluecellulab-2.6.77/bluecellulab/reports/manager.py +91 -0
  20. bluecellulab-2.6.77/bluecellulab/reports/utils.py +227 -0
  21. bluecellulab-2.6.77/bluecellulab/reports/writers/__init__.py +25 -0
  22. bluecellulab-2.6.77/bluecellulab/reports/writers/base_writer.py +30 -0
  23. bluecellulab-2.6.77/bluecellulab/reports/writers/compartment.py +221 -0
  24. bluecellulab-2.6.77/bluecellulab/reports/writers/spikes.py +86 -0
  25. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/simulation/neuron_globals.py +12 -0
  26. bluecellulab-2.6.77/bluecellulab/simulation/report.py +0 -0
  27. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/simulation/simulation.py +5 -10
  28. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/stimulus/circuit_stimulus_definitions.py +72 -28
  29. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/tools.py +22 -85
  30. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/utils.py +1 -1
  31. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/validation/validation.py +154 -69
  32. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab.egg-info/PKG-INFO +10 -5
  33. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab.egg-info/SOURCES.txt +8 -0
  34. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab.egg-info/requires.txt +7 -1
  35. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/docs/requirements_docs.txt +1 -1
  36. bluecellulab-2.6.77/docs/source/_static/.gitkeep +0 -0
  37. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/docs/source/conf.py +4 -1
  38. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/pyproject.toml +10 -2
  39. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/tox.ini +5 -3
  40. bluecellulab-2.6.61/bluecellulab/simulation/report.py +0 -264
  41. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/.compile_mod.sh +0 -0
  42. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/.gitattributes +0 -0
  43. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/.github/dependabot.yml +0 -0
  44. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/.github/workflows/release.yml +0 -0
  45. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/.gitlab-ci.yml +0 -0
  46. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/.readthedocs.yml +0 -0
  47. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/.zenodo.json +0 -0
  48. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/AUTHORS.txt +0 -0
  49. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/CHANGELOG.rst +0 -0
  50. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/CITATION.cff +0 -0
  51. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/CONTRIBUTING.rst +0 -0
  52. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/LICENSE +0 -0
  53. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/MANIFEST.in +0 -0
  54. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/Makefile +0 -0
  55. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/__init__.py +0 -0
  56. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/analysis/__init__.py +0 -0
  57. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/analysis/inject_sequence.py +0 -0
  58. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/analysis/plotting.py +0 -0
  59. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/analysis/utils.py +0 -0
  60. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/cell/__init__.py +0 -0
  61. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/cell/ballstick/__init__.py +0 -0
  62. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/cell/ballstick/emodel.hoc +0 -0
  63. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/cell/ballstick/morphology.asc +0 -0
  64. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/cell/cell_dict.py +0 -0
  65. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/cell/plotting.py +0 -0
  66. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/cell/random.py +0 -0
  67. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/cell/recording.py +0 -0
  68. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/cell/section_distance.py +0 -0
  69. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/cell/serialized_sections.py +0 -0
  70. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/cell/sonata_proxy.py +0 -0
  71. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/cell/stimuli_generator.py +0 -0
  72. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/cell/template.py +0 -0
  73. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/circuit/__init__.py +0 -0
  74. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/circuit/circuit_access/__init__.py +0 -0
  75. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/circuit/circuit_access/definition.py +0 -0
  76. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/circuit/config/__init__.py +0 -0
  77. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/circuit/config/sections.py +0 -0
  78. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/circuit/format.py +0 -0
  79. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/circuit/iotools.py +0 -0
  80. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/circuit/synapse_properties.py +0 -0
  81. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/circuit/validate.py +0 -0
  82. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/dendrogram.py +0 -0
  83. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/exceptions.py +0 -0
  84. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/graph.py +0 -0
  85. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/hoc/Cell.hoc +0 -0
  86. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/hoc/RNGSettings.hoc +0 -0
  87. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/hoc/TDistFunc.hoc +0 -0
  88. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/hoc/TStim.hoc +0 -0
  89. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/hoc/fileUtils.hoc +0 -0
  90. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/importer.py +0 -0
  91. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/neuron_interpreter.py +0 -0
  92. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/psection.py +0 -0
  93. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/psegment.py +0 -0
  94. /bluecellulab-2.6.61/docs/source/_static/.gitkeep → /bluecellulab-2.6.77/bluecellulab/reports/__init__.py +0 -0
  95. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/rngsettings.py +0 -0
  96. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/simulation/__init__.py +0 -0
  97. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/simulation/parallel.py +0 -0
  98. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/stimulus/__init__.py +0 -0
  99. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/stimulus/factory.py +0 -0
  100. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/stimulus/stimulus.py +0 -0
  101. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/synapse/__init__.py +0 -0
  102. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/synapse/synapse_factory.py +0 -0
  103. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/synapse/synapse_types.py +0 -0
  104. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/type_aliases.py +0 -0
  105. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab/verbosity.py +0 -0
  106. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab.egg-info/dependency_links.txt +0 -0
  107. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/bluecellulab.egg-info/top_level.txt +0 -0
  108. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/docs/Makefile +0 -0
  109. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/docs/images/voltage-readme.png +0 -0
  110. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/docs/make.bat +0 -0
  111. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/docs/source/api.rst +0 -0
  112. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/docs/source/changelog.rst +0 -0
  113. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/docs/source/compiling-mechanisms.rst +0 -0
  114. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/docs/source/contributing.rst +0 -0
  115. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/docs/source/index.rst +0 -0
  116. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/docs/source/list_of_stim.rst +0 -0
  117. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/docs/source/logo/BlueCelluLabBanner.jpg +0 -0
  118. {bluecellulab-2.6.61 → bluecellulab-2.6.77}/setup.cfg +0 -0
@@ -14,7 +14,7 @@ jobs:
14
14
  runs-on: ubuntu-latest
15
15
  strategy:
16
16
  matrix:
17
- python-version: ["3.9", "3.10", "3.11", "3.12"]
17
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
18
18
 
19
19
  steps:
20
20
  - uses: actions/checkout@v4
@@ -27,4 +27,7 @@ nrnivmodl.log
27
27
  *.btr
28
28
  *.whl
29
29
  .ipynb_checkpoints
30
- cADpyr_L2TPC_dendrogram.png
30
+ cADpyr_L2TPC_dendrogram.png
31
+ ex2_sonatagraph.pdf
32
+ .venv
33
+ bad_dendritic_fit.png
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bluecellulab
3
- Version: 2.6.61
3
+ Version: 2.6.77
4
4
  Summary: Biologically detailed neural network simulations and analysis.
5
5
  Author: Blue Brain Project, EPFL
6
6
  License: Apache2.0
@@ -14,12 +14,12 @@ Classifier: Operating System :: POSIX
14
14
  Classifier: Topic :: Scientific/Engineering
15
15
  Classifier: Programming Language :: Python :: 3
16
16
  Classifier: Topic :: Utilities
17
- Requires-Python: >=3.9
17
+ Requires-Python: >=3.10
18
18
  Description-Content-Type: text/x-rst
19
19
  License-File: LICENSE
20
20
  License-File: AUTHORS.txt
21
21
  Requires-Dist: NEURON<9.0.0,>=8.0.2
22
- Requires-Dist: numpy<2.0.0,>=1.8.0
22
+ Requires-Dist: numpy<2.4,>=2.0.0
23
23
  Requires-Dist: matplotlib<4.0.0,>=3.0.0
24
24
  Requires-Dist: pandas<3.0.0,>=1.0.0
25
25
  Requires-Dist: bluepysnap<4.0.0,>=3.0.0
@@ -28,6 +28,11 @@ Requires-Dist: typing-extensions>=4.8.0
28
28
  Requires-Dist: networkx>=3.1
29
29
  Requires-Dist: h5py>=3.8.0
30
30
  Requires-Dist: seaborn
31
+ Requires-Dist: efel>=5.7.19
32
+ Requires-Dist: neo>=0.14.0
33
+ Requires-Dist: quantities>=0.16.2
34
+ Provides-Extra: examples
35
+ Requires-Dist: ipywidgets; extra == "examples"
31
36
  Dynamic: license-file
32
37
 
33
38
  |banner|
@@ -98,8 +103,8 @@ We are providing support on `Gitter <https://gitter.im/openbraininstitute/BlueCe
98
103
  Main dependencies
99
104
  =================
100
105
 
101
- * `Python 3.9+ <https://www.python.org/downloads/release/python-390/>`_
102
- * `Neuron 8.0.2+ <https://pypi.org/project/NEURON/>`_
106
+ * `Python 3.10+ <https://www.python.org/downloads/release/python-390/>`_
107
+ * `NEURON <=8.2.7 <https://pypi.org/project/NEURON/>`__
103
108
 
104
109
  Installation
105
110
  ============
@@ -66,8 +66,8 @@ We are providing support on `Gitter <https://gitter.im/openbraininstitute/BlueCe
66
66
  Main dependencies
67
67
  =================
68
68
 
69
- * `Python 3.9+ <https://www.python.org/downloads/release/python-390/>`_
70
- * `Neuron 8.0.2+ <https://pypi.org/project/NEURON/>`_
69
+ * `Python 3.10+ <https://www.python.org/downloads/release/python-390/>`_
70
+ * `NEURON <=8.2.7 <https://pypi.org/project/NEURON/>`__
71
71
 
72
72
  Installation
73
73
  ============
@@ -4,9 +4,11 @@ try:
4
4
  except ImportError:
5
5
  efel = None
6
6
  from itertools import islice
7
+ from itertools import repeat
7
8
  import logging
8
9
  from matplotlib.collections import LineCollection
9
10
  import matplotlib.pyplot as plt
11
+ from multiprocessing import Pool
10
12
  import neuron
11
13
  import numpy as np
12
14
  import pathlib
@@ -18,6 +20,7 @@ from bluecellulab.analysis.inject_sequence import run_stimulus
18
20
  from bluecellulab.analysis.plotting import plot_iv_curve, plot_fi_curve
19
21
  from bluecellulab.analysis.utils import exp_decay
20
22
  from bluecellulab.simulation import Simulation
23
+ from bluecellulab.simulation.neuron_globals import set_neuron_globals
21
24
  from bluecellulab.stimulus import StimulusFactory
22
25
  from bluecellulab.stimulus.circuit_stimulus_definitions import Hyperpolarizing
23
26
  from bluecellulab.tools import calculate_rheobase
@@ -40,7 +43,10 @@ def compute_plot_iv_curve(cell,
40
43
  show_figure=True,
41
44
  save_figure=False,
42
45
  output_dir="./",
43
- output_fname="iv_curve.pdf"):
46
+ output_fname="iv_curve.pdf",
47
+ n_processes=None,
48
+ celsius=None,
49
+ v_init=None):
44
50
  """Compute and plot the Current-Voltage (I-V) curve for a given cell by
45
51
  injecting a range of currents.
46
52
 
@@ -72,6 +78,11 @@ def compute_plot_iv_curve(cell,
72
78
  save_figure (bool): Whether to save the figure. Default is False.
73
79
  output_dir (str): The directory to save the figure if save_figure is True. Default is "./".
74
80
  output_fname (str): The filename to save the figure as if save_figure is True. Default is "iv_curve.png".
81
+ n_processes (int, optional): The number of processes to use for parallel execution.
82
+ If None or if it is higher than the number of steps,
83
+ it will use the number of steps as the number of processes.
84
+ celsius (float, optional): Temperature in Celsius.
85
+ v_init (float, optional): Initial membrane potential.
75
86
 
76
87
  Returns:
77
88
  tuple: A tuple containing:
@@ -95,15 +106,22 @@ def compute_plot_iv_curve(cell,
95
106
  for amp in list_amp
96
107
  ]
97
108
 
98
- recordings = []
99
- for step in steps:
100
- recording = run_stimulus(cell.template_params,
101
- step,
102
- section=injecting_section,
103
- segment=injecting_segment,
104
- recording_section=recording_section,
105
- recording_segment=recording_segment)
106
- recordings.append(recording)
109
+ if n_processes is None or n_processes > len(steps):
110
+ n_processes = len(steps)
111
+ with Pool(n_processes, initializer=set_neuron_globals, initargs=(celsius, v_init)) as p:
112
+ recordings = p.starmap(
113
+ run_stimulus,
114
+ zip(
115
+ repeat(cell.template_params),
116
+ steps,
117
+ repeat(injecting_section),
118
+ repeat(injecting_segment),
119
+ repeat(True), # cvode
120
+ repeat(True), # add_hypamp
121
+ repeat(recording_section),
122
+ repeat(recording_segment),
123
+ )
124
+ )
107
125
 
108
126
  steady_states = []
109
127
  # compute steady state response
@@ -148,7 +166,10 @@ def compute_plot_fi_curve(cell,
148
166
  show_figure=True,
149
167
  save_figure=False,
150
168
  output_dir="./",
151
- output_fname="fi_curve.pdf"):
169
+ output_fname="fi_curve.pdf",
170
+ n_processes=None,
171
+ celsius=None,
172
+ v_init=None):
152
173
  """Compute and plot the Frequency-Current (F-I) curve for a given cell by
153
174
  injecting a range of currents.
154
175
 
@@ -182,6 +203,11 @@ def compute_plot_fi_curve(cell,
182
203
  save_figure (bool): Whether to save the figure. Default is False.
183
204
  output_dir (str): The directory to save the figure if save_figure is True. Default is "./".
184
205
  output_fname (str): The filename to save the figure as if save_figure is True. Default is "iv_curve.png".
206
+ n_processes (int, optional): The number of processes to use for parallel execution.
207
+ If None or if it is higher than the number of steps,
208
+ it will use the number of steps as the number of processes.
209
+ celsius (float, optional): Temperature in Celsius.
210
+ v_init (float, optional): Initial membrane potential.
185
211
 
186
212
  Returns:
187
213
  tuple: A tuple containing:
@@ -201,19 +227,26 @@ def compute_plot_fi_curve(cell,
201
227
  for amp in list_amp
202
228
  ]
203
229
 
204
- spikes = []
205
- for step in steps:
206
- recording = run_stimulus(cell.template_params,
207
- step,
208
- section=injecting_section,
209
- segment=injecting_segment,
210
- recording_section=recording_section,
211
- recording_segment=recording_segment,
212
- enable_spike_detection=True,
213
- threshold_spike_detection=threshold_voltage)
214
- spikes.append(recording.spike)
230
+ if n_processes is None or n_processes > len(steps):
231
+ n_processes = len(steps)
232
+ with Pool(n_processes, initializer=set_neuron_globals, initargs=(celsius, v_init)) as p:
233
+ recordings = p.starmap(
234
+ run_stimulus,
235
+ zip(
236
+ repeat(cell.template_params),
237
+ steps,
238
+ repeat(injecting_section),
239
+ repeat(injecting_segment),
240
+ repeat(True), # cvode
241
+ repeat(True), # add_hypamp
242
+ repeat(recording_section),
243
+ repeat(recording_segment),
244
+ repeat(True), # enable_spike_detection
245
+ repeat(threshold_voltage), # threshold_spike_detection
246
+ )
247
+ )
215
248
 
216
- spike_count = [len(spike) for spike in spikes]
249
+ spike_count = [len(recording.spike) for recording in recordings]
217
250
 
218
251
  plot_fi_curve(list_amp,
219
252
  spike_count,
@@ -327,54 +360,69 @@ class BPAP:
327
360
  return soma_amp, dend_amps, dend_dist, apic_amps, apic_dist
328
361
 
329
362
  @staticmethod
330
- def fit(soma_amp, dend_amps, dend_dist, apic_amps, apic_dist):
363
+ def fit(soma_amp, branch_amps, branch_dist):
331
364
  """Fit the amplitudes vs distances to an exponential decay function."""
332
365
  from scipy.optimize import curve_fit
333
366
 
334
- popt_dend = None
335
- if dend_amps and dend_dist:
336
- dist = [0] + dend_dist # add soma distance
337
- amps = soma_amp + dend_amps # add soma amplitude
338
- popt_dend, _ = curve_fit(exp_decay, dist, amps)
339
-
340
- popt_apic = None
341
- if apic_amps and apic_dist:
342
- dist = [0] + apic_dist # add soma distance
343
- amps = soma_amp + apic_amps # add soma amplitude
344
- popt_apic, _ = curve_fit(exp_decay, dist, amps)
345
-
346
- return popt_dend, popt_apic
347
-
348
- def validate(self, soma_amp, dend_amps, dend_dist, apic_amps, apic_dist):
367
+ if not branch_amps or not branch_dist or len(branch_amps) != len(branch_dist):
368
+ return None, False
369
+ try:
370
+ dist = [0] + branch_dist
371
+ amps = soma_amp + branch_amps
372
+ popt, _ = curve_fit(exp_decay, dist, amps)
373
+ return popt, False
374
+ except RuntimeError:
375
+ return None, True
376
+
377
+ def validate(self, soma_amp, dend_amps, dend_dist, apic_amps, apic_dist, validate_with_fit=True):
349
378
  """Check that the exponential fit is decaying."""
350
379
  validated = True
351
380
  notes = ""
352
- popt_dend, popt_apic = self.fit(soma_amp, dend_amps, dend_dist, apic_amps, apic_dist)
353
- if dend_amps is not None:
354
- plt.cla()
355
- plt.plot([0], soma_amp, '.')
356
- plt.plot(dend_dist, dend_amps, '.')
357
- x = np.linspace(0, max(dend_dist), 100)
358
- plt.plot(x, exp_decay(x, *popt_dend), color='darkgreen', linestyle='--', label='Basal Dendritic Fit')
359
- plt.savefig("bad_dendritic_fit.png")
360
- if popt_dend is None:
361
- logger.debug("No dendritic recordings found.")
362
- notes += "No dendritic recordings found.\n"
363
- elif popt_dend[1] <= 0 or popt_dend[0] <= 0:
364
- logger.debug("Dendritic fit is not decaying.")
365
- validated = False
366
- notes += "Dendritic fit is not decaying.\n"
381
+ if validate_with_fit:
382
+ popt_dend, dend_fit_error = self.fit(soma_amp, dend_amps, dend_dist)
383
+ popt_apic, apic_fit_error = self.fit(soma_amp, apic_amps, apic_dist)
384
+ if dend_fit_error or apic_fit_error:
385
+ logger.debug("Fitting error occurred.")
386
+ validated = False
387
+ notes += "Validation failed: Fitting error occurred.\n"
388
+ return validated, notes
389
+ if popt_dend is None:
390
+ logger.debug("No dendritic recordings found.")
391
+ notes += "No dendritic recordings found.\n"
392
+ elif popt_dend[1] <= 0 or popt_dend[0] <= 0:
393
+ logger.debug("Dendritic fit is not decaying.")
394
+ validated = False
395
+ notes += "Dendritic fit is not decaying.\n"
396
+ else:
397
+ notes += "Dendritic validation passed: dendritic amplitude is decaying with distance relative to soma.\n"
398
+ if popt_apic is None:
399
+ logger.debug("No apical recordings found.")
400
+ notes += "No apical recordings found.\n"
401
+ elif popt_apic[1] <= 0 or popt_apic[0] <= 0:
402
+ logger.debug("Apical fit is not decaying.")
403
+ validated = False
404
+ notes += "Apical fit is not decaying.\n"
405
+ else:
406
+ notes += "Apical validation passed: apical amplitude is decaying with distance relative to soma.\n"
367
407
  else:
368
- notes += "Dendritic validation passed: dendritic amplitude is decaying with distance relative to soma.\n"
369
- if popt_apic is None:
370
- logger.debug("No apical recordings found.")
371
- notes += "No apical recordings found.\n"
372
- elif popt_apic[1] <= 0 or popt_apic[0] <= 0:
373
- logger.debug("Apical fit is not decaying.")
374
- validated = False
375
- notes += "Apical fit is not decaying.\n"
376
- else:
377
- notes += "Apical validation passed: apical amplitude is decaying with distance relative to soma.\n"
408
+ if dend_amps and dend_dist:
409
+ furthest_dend_idx = np.argmax(dend_dist)
410
+ if dend_amps[furthest_dend_idx] < soma_amp[0]:
411
+ notes += "Dendritic validation passed: dendritic amplitude is decaying with distance relative to soma.\n"
412
+ else:
413
+ validated = False
414
+ notes += "Dendritic validation failed: dendritic amplitude is not decaying with distance relative to soma.\n"
415
+ else:
416
+ notes += "No dendritic recordings found.\n"
417
+ if apic_amps and apic_dist:
418
+ furthest_apic_idx = np.argmax(apic_dist)
419
+ if apic_amps[furthest_apic_idx] < soma_amp[0]:
420
+ notes += "Apical validation passed: apical amplitude is decaying with distance relative to soma.\n"
421
+ else:
422
+ validated = False
423
+ notes += "Apical validation failed: apical amplitude is not decaying with distance relative to soma.\n"
424
+ else:
425
+ notes += "No apical recordings found.\n"
378
426
 
379
427
  return validated, notes
380
428
 
@@ -389,9 +437,14 @@ class BPAP:
389
437
  save_figure=False,
390
438
  output_dir="./",
391
439
  output_fname="bpap.pdf",
440
+ do_fit=True,
392
441
  ):
393
442
  """Plot the results of the BPAP analysis."""
394
- popt_dend, popt_apic = self.fit(soma_amp, dend_amps, dend_dist, apic_amps, apic_dist)
443
+ popt_dend = None
444
+ popt_apic = None
445
+ if do_fit:
446
+ popt_dend, _ = self.fit(soma_amp, dend_amps, dend_dist)
447
+ popt_apic, _ = self.fit(soma_amp, apic_amps, apic_dist)
395
448
 
396
449
  outpath = pathlib.Path(output_dir) / output_fname
397
450
  fig, ax1 = plt.subplots(figsize=(10, 6))
@@ -19,7 +19,7 @@ import logging
19
19
 
20
20
  from pathlib import Path
21
21
  import queue
22
- from typing import Optional
22
+ from typing import List, Optional, Tuple
23
23
  from typing_extensions import deprecated
24
24
 
25
25
  import neuron
@@ -47,6 +47,7 @@ from bluecellulab.stimulus.circuit_stimulus_definitions import SynapseReplay
47
47
  from bluecellulab.synapse import SynapseFactory, Synapse
48
48
  from bluecellulab.synapse.synapse_types import SynapseID
49
49
  from bluecellulab.type_aliases import HocObjectType, NeuronSection, SectionMapping
50
+ from bluecellulab.cell.section_tools import currents_vars, section_to_variable_recording_str
50
51
 
51
52
  logger = logging.getLogger(__name__)
52
53
 
@@ -250,6 +251,10 @@ class Cell(InjectableMixin, PlottableMixin):
250
251
  else:
251
252
  self._default_disable_ttx()
252
253
 
254
+ @property
255
+ def ttx_enabled(self):
256
+ return getattr(self, "_ttx_enabled", False)
257
+
253
258
  def _default_enable_ttx(self) -> None:
254
259
  """Default enable_ttx implementation."""
255
260
  for section in self.sections.values():
@@ -760,17 +765,66 @@ class Cell(InjectableMixin, PlottableMixin):
760
765
  """Get a vector of AIS voltage."""
761
766
  return self.get_recording('self.axonal[1](0.5)._ref_v')
762
767
 
763
- def add_variable_recording(self, variable: str, section, segx):
764
- if variable == "v":
765
- self.add_voltage_recording(section, segx)
766
- else:
767
- raise ValueError(f"Unsupported variable for recording: {variable}")
768
+ def add_variable_recording(
769
+ self,
770
+ variable: str,
771
+ section: Optional[NeuronSection] = None,
772
+ segx: float = 0.5,
773
+ dt: Optional[float] = None
774
+ ) -> None:
775
+ """Add a recording of any NEURON RANGE variable (e.g., gna, gk, ina)
776
+ from a given section and segment.
768
777
 
769
- def get_variable_recording(self, variable: str, section, segx) -> np.ndarray:
770
- if variable == "v":
771
- return self.get_voltage_recording(section=section, segx=segx)
778
+ Args:
779
+ variable: The NEURON variable name to record (e.g., "gna").
780
+ section: The section to record from (defaults to soma).
781
+ segx: Segment position between 0 and 1.
782
+ dt: Optional recording time step.
783
+ """
784
+
785
+ if section is None:
786
+ section = self.soma
787
+
788
+ # validate before constructing the string
789
+ seg = section(segx)
790
+ if "." in variable:
791
+ mech, var = variable.split(".", 1)
792
+ mobj = getattr(seg, mech, None)
793
+ if mobj is None or not hasattr(mobj, f"_ref_{var}"):
794
+ raise AttributeError(
795
+ f"'{variable}' not recordable at {section.name()}({segx}). "
796
+ f"Mechanisms here: {list(section.psection()['density_mechs'].keys())}"
797
+ )
772
798
  else:
773
- raise ValueError(f"Unsupported variable '{variable}'")
799
+ if not hasattr(seg, f"_ref_{variable}"):
800
+ raise AttributeError(
801
+ f"'{variable}' not recordable at {section.name()}({segx}). "
802
+ f"(Top-level vars are typically v/ina/ik/ica)"
803
+ )
804
+
805
+ var_name = section_to_variable_recording_str(section, segx, variable)
806
+ self.add_recording(var_name, dt)
807
+
808
+ def get_variable_recording(
809
+ self, variable: str, section: Optional[NeuronSection], segx: float
810
+ ) -> np.ndarray:
811
+ """Get a recording of any variable recorded from a section and segment.
812
+
813
+ Args:
814
+ variable: The name of the recorded variable (e.g., 'v', 'gna').
815
+ section: The NEURON section object.
816
+ segx: Segment location from 0 to 1.
817
+
818
+ Returns:
819
+ NumPy array of recorded values.
820
+
821
+ Raises:
822
+ ValueError: If the recording is not found.
823
+ """
824
+ if section is None:
825
+ section = self.soma
826
+ recording_name = section_to_variable_recording_str(section, segx, variable)
827
+ return self.get_recording(recording_name)
774
828
 
775
829
  @property
776
830
  def n_segments(self) -> int:
@@ -784,7 +838,18 @@ class Cell(InjectableMixin, PlottableMixin):
784
838
  connected to that cell."""
785
839
  if self.sonata_proxy is None:
786
840
  raise BluecellulabError("Cell: add_synapse_replay requires a sonata proxy.")
787
- synapse_spikes: dict = get_synapse_replay_spikes(stimulus.spike_file)
841
+
842
+ file_path = Path(stimulus.spike_file).expanduser()
843
+ if not file_path.is_absolute():
844
+ config_dir = stimulus.config_dir
845
+ if config_dir is not None:
846
+ file_path = Path(config_dir) / file_path
847
+
848
+ file_path = file_path.resolve()
849
+
850
+ if not file_path.exists():
851
+ raise FileNotFoundError(f"Spike file not found: {str(file_path)}")
852
+ synapse_spikes: dict = get_synapse_replay_spikes(str(file_path))
788
853
  for synapse_id, synapse in self.synapses.items():
789
854
  source_population = synapse.syn_description["source_population_name"]
790
855
  pre_gid = CellId(
@@ -843,3 +908,186 @@ class Cell(InjectableMixin, PlottableMixin):
843
908
 
844
909
  def __del__(self):
845
910
  self.delete()
911
+
912
+ def get_section(self, section_name: str) -> NeuronSection:
913
+ """Return a single, fully specified NEURON section (e.g., 'soma[0]',
914
+ 'dend[3]').
915
+
916
+ Raises:
917
+ ValueError or TypeError if the section is not found or invalid.
918
+ """
919
+ if section_name in self.sections:
920
+ section = self.sections[section_name]
921
+ if hasattr(section, "nseg"):
922
+ return section
923
+ raise TypeError(f"'{section_name}' exists but is not a NEURON section.")
924
+
925
+ available = ", ".join(self.sections.keys())
926
+ raise ValueError(f"Section '{section_name}' not found. Available: [{available}]")
927
+
928
+ def get_sections(self, section_name: str) -> List[NeuronSection]:
929
+ """Return a list of NEURON sections.
930
+
931
+ If the section name is a fully specified one (e.g., 'dend[3]'), return it as a list of one.
932
+ If the section name is a base name (e.g., 'dend'), return all matching sections like 'dend[0]', 'dend[1]', etc.
933
+
934
+ Raises:
935
+ ValueError if no valid sections are found.
936
+ """
937
+ # Try to interpret as fully qualified section name
938
+ try:
939
+ return [self.get_section(section_name)]
940
+ except ValueError:
941
+ pass # Not a precise match; try prefix match
942
+
943
+ # Fallback to prefix-based match (e.g., 'dend' → 'dend[0]', 'dend[1]', ...)
944
+ matched = [
945
+ section for name, section in self.sections.items()
946
+ if name.startswith(f"{section_name}[")
947
+ ]
948
+ if matched:
949
+ return matched
950
+
951
+ available = ", ".join(self.sections.keys())
952
+ raise ValueError(f"Section '{section_name}' not found. Available: [{available}]")
953
+
954
+ def get_section_by_id(self, section_id: int) -> NeuronSection:
955
+ """Return NEURON section by global section index (LibSONATA
956
+ ordering)."""
957
+ if not self.psections:
958
+ self._init_psections()
959
+
960
+ try:
961
+ return self.psections[int(section_id)].hsection
962
+ except KeyError:
963
+ raise IndexError(f"Section ID {section_id} is out of range for cell {self.cell_id.id}")
964
+
965
+ def resolve_segments_from_compartment_set(self, node_id, compartment_nodes) -> List[Tuple[NeuronSection, str, float]]:
966
+ """Resolve segments for a cell using a predefined compartment node
967
+ list.
968
+
969
+ Supports both LibSONATA format ([node_id, section_id, seg]) and
970
+ name-based format ([node_id, section_name, seg]).
971
+ """
972
+ result = []
973
+ for n_id, sec_ref, seg in compartment_nodes:
974
+ if n_id != node_id:
975
+ continue
976
+
977
+ if isinstance(sec_ref, str):
978
+ # Name-based: e.g., "dend[5]"
979
+ section = self.get_section(sec_ref)
980
+ sec_name = section.name().split(".")[-1]
981
+ elif isinstance(sec_ref, int):
982
+ # ID-based: resolve by section index
983
+ try:
984
+ section = self.get_section_by_id(sec_ref)
985
+ sec_name = section.name().split(".")[-1]
986
+ except AttributeError:
987
+ raise ValueError(f"Cell object does not support section lookup by index: {sec_ref}")
988
+ else:
989
+ raise TypeError(f"Unsupported section reference type: {type(sec_ref)}")
990
+
991
+ result.append((section, sec_name, seg))
992
+ return result
993
+
994
+ def resolve_segments_from_config(self, report_cfg) -> List[Tuple[NeuronSection, str, float]]:
995
+ """Resolve segments from NEURON sections based on config."""
996
+ compartment = report_cfg.get("compartments", "center")
997
+ if compartment not in {"center", "all"}:
998
+ raise ValueError(
999
+ f"Unsupported 'compartments' value '{compartment}' — must be 'center' or 'all'."
1000
+ )
1001
+
1002
+ section_name = report_cfg.get("sections", "soma")
1003
+ sections = self.get_sections(section_name)
1004
+
1005
+ targets = []
1006
+ for sec in sections:
1007
+ sec_name = sec.name().split(".")[-1]
1008
+ if compartment == "center":
1009
+ targets.append((sec, sec_name, 0.5))
1010
+ elif compartment == "all":
1011
+ for seg in sec:
1012
+ targets.append((sec, sec_name, seg.x))
1013
+ return targets
1014
+
1015
+ def configure_recording(self, recording_sites, variable_name, report_name):
1016
+ """Configure recording of a variable on a single cell.
1017
+
1018
+ This function sets up the recording of the specified variable (e.g., membrane voltage)
1019
+ in the target cell, for each resolved segment.
1020
+
1021
+ Parameters
1022
+ ----------
1023
+ cell : Any
1024
+ The cell object on which to configure recordings.
1025
+
1026
+ recording_sites : list of tuples
1027
+ List of tuples (section, section_name, segment) where:
1028
+ - section is the section object in the cell.
1029
+ - section_name is the name of the section.
1030
+ - segment is the Neuron segment index (0-1).
1031
+
1032
+ variable_name : str
1033
+ The name of the variable to record (e.g., "v" for membrane voltage).
1034
+
1035
+ report_name : str
1036
+ The name of the report (used in logging).
1037
+ """
1038
+ node_id = self.cell_id.id
1039
+
1040
+ for sec, sec_name, seg in recording_sites:
1041
+ try:
1042
+ self.add_variable_recording(variable=variable_name, section=sec, segx=seg)
1043
+ logger.info(
1044
+ f"Recording '{variable_name}' at {sec_name}({seg}) on GID {node_id} for report '{report_name}'"
1045
+ )
1046
+ except AttributeError:
1047
+ logger.warning(
1048
+ f"Recording for variable '{variable_name}' is not implemented in Cell."
1049
+ )
1050
+ return
1051
+ except Exception as e:
1052
+ logger.warning(
1053
+ f"Failed to record '{variable_name}' at {sec_name}({seg}) on GID {node_id} for report '{report_name}': {e}"
1054
+ )
1055
+
1056
+ def add_currents_recordings(
1057
+ self,
1058
+ section,
1059
+ segx: float = 0.5,
1060
+ *,
1061
+ include_nonspecific: bool = True,
1062
+ include_point_processes: bool = False,
1063
+ dt: float | None = None,
1064
+ ) -> list[str]:
1065
+ """Record all available currents (ionic + optionally nonspecific) at
1066
+ (section, segx)."""
1067
+
1068
+ # discover what’s available at this site
1069
+ available = currents_vars(section)
1070
+ chosen: list[str] = []
1071
+
1072
+ for name, meta in available.items():
1073
+ kind = meta.get("kind")
1074
+
1075
+ if kind == "ionic_current":
1076
+ self.add_variable_recording(name, section=section, segx=segx, dt=dt)
1077
+ chosen.append(name)
1078
+
1079
+ elif kind == "nonspecific_current":
1080
+ if not include_nonspecific:
1081
+ continue
1082
+ # density-mech nonspecific currents
1083
+ self.add_variable_recording(name, section=section, segx=segx, dt=dt)
1084
+ chosen.append(name)
1085
+
1086
+ elif kind == "point_process_current":
1087
+ if not include_point_processes:
1088
+ continue
1089
+ # point process nonspecific currents
1090
+ self.add_variable_recording(name, section=section, segx=segx, dt=dt)
1091
+ chosen.append(name)
1092
+
1093
+ return chosen