bluecellulab 2.6.71__tar.gz → 2.6.73__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.

Potentially problematic release.


This version of bluecellulab might be problematic. Click here for more details.

Files changed (119) hide show
  1. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/.github/workflows/test.yml +1 -1
  2. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/PKG-INFO +3 -3
  3. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/README.rst +1 -1
  4. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/cell/core.py +147 -3
  5. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/circuit/circuit_access/bluepy_circuit_access.py +2 -1
  6. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/circuit/circuit_access/sonata_circuit_access.py +2 -2
  7. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/circuit/config/bluepy_simulation_config.py +10 -0
  8. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/circuit/config/sonata_simulation_config.py +2 -0
  9. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/circuit/node_id.py +1 -1
  10. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/circuit_simulation.py +2 -2
  11. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/plotwindow.py +1 -1
  12. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/reports/manager.py +23 -10
  13. bluecellulab-2.6.73/bluecellulab/reports/utils.py +227 -0
  14. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/reports/writers/compartment.py +45 -28
  15. bluecellulab-2.6.73/bluecellulab/reports/writers/spikes.py +86 -0
  16. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/tools.py +9 -79
  17. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/utils.py +1 -1
  18. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab.egg-info/PKG-INFO +3 -3
  19. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab.egg-info/requires.txt +1 -1
  20. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/pyproject.toml +1 -1
  21. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/tox.ini +2 -1
  22. bluecellulab-2.6.71/bluecellulab/reports/utils.py +0 -156
  23. bluecellulab-2.6.71/bluecellulab/reports/writers/spikes.py +0 -61
  24. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/.compile_mod.sh +0 -0
  25. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/.gitattributes +0 -0
  26. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/.github/dependabot.yml +0 -0
  27. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/.github/workflows/release.yml +0 -0
  28. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/.gitignore +0 -0
  29. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/.gitlab-ci.yml +0 -0
  30. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/.readthedocs.yml +0 -0
  31. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/.zenodo.json +0 -0
  32. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/AUTHORS.txt +0 -0
  33. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/CHANGELOG.rst +0 -0
  34. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/CITATION.cff +0 -0
  35. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/CONTRIBUTING.rst +0 -0
  36. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/LICENSE +0 -0
  37. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/MANIFEST.in +0 -0
  38. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/Makefile +0 -0
  39. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/__init__.py +0 -0
  40. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/analysis/__init__.py +0 -0
  41. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/analysis/analysis.py +0 -0
  42. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/analysis/inject_sequence.py +0 -0
  43. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/analysis/plotting.py +0 -0
  44. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/analysis/utils.py +0 -0
  45. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/cell/__init__.py +0 -0
  46. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/cell/ballstick/__init__.py +0 -0
  47. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/cell/ballstick/emodel.hoc +0 -0
  48. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/cell/ballstick/morphology.asc +0 -0
  49. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/cell/cell_dict.py +0 -0
  50. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/cell/injector.py +0 -0
  51. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/cell/plotting.py +0 -0
  52. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/cell/random.py +0 -0
  53. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/cell/recording.py +0 -0
  54. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/cell/section_distance.py +0 -0
  55. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/cell/section_tools.py +0 -0
  56. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/cell/serialized_sections.py +0 -0
  57. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/cell/sonata_proxy.py +0 -0
  58. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/cell/stimuli_generator.py +0 -0
  59. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/cell/template.py +0 -0
  60. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/circuit/__init__.py +0 -0
  61. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/circuit/circuit_access/__init__.py +0 -0
  62. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/circuit/circuit_access/definition.py +0 -0
  63. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/circuit/config/__init__.py +0 -0
  64. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/circuit/config/definition.py +0 -0
  65. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/circuit/config/sections.py +0 -0
  66. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/circuit/format.py +0 -0
  67. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/circuit/iotools.py +0 -0
  68. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/circuit/simulation_access.py +0 -0
  69. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/circuit/synapse_properties.py +0 -0
  70. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/circuit/validate.py +0 -0
  71. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/connection.py +0 -0
  72. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/dendrogram.py +0 -0
  73. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/exceptions.py +0 -0
  74. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/graph.py +0 -0
  75. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/hoc/Cell.hoc +0 -0
  76. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/hoc/RNGSettings.hoc +0 -0
  77. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/hoc/TDistFunc.hoc +0 -0
  78. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/hoc/TStim.hoc +0 -0
  79. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/hoc/fileUtils.hoc +0 -0
  80. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/importer.py +0 -0
  81. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/neuron_interpreter.py +0 -0
  82. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/psection.py +0 -0
  83. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/psegment.py +0 -0
  84. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/reports/__init__.py +0 -0
  85. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/reports/writers/__init__.py +0 -0
  86. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/reports/writers/base_writer.py +0 -0
  87. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/rngsettings.py +0 -0
  88. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/simulation/__init__.py +0 -0
  89. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/simulation/neuron_globals.py +0 -0
  90. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/simulation/parallel.py +0 -0
  91. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/simulation/report.py +0 -0
  92. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/simulation/simulation.py +0 -0
  93. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/stimulus/__init__.py +0 -0
  94. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/stimulus/circuit_stimulus_definitions.py +0 -0
  95. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/stimulus/factory.py +0 -0
  96. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/stimulus/stimulus.py +0 -0
  97. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/synapse/__init__.py +0 -0
  98. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/synapse/synapse_factory.py +0 -0
  99. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/synapse/synapse_types.py +0 -0
  100. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/type_aliases.py +0 -0
  101. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/validation/validation.py +0 -0
  102. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab/verbosity.py +0 -0
  103. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab.egg-info/SOURCES.txt +0 -0
  104. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab.egg-info/dependency_links.txt +0 -0
  105. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/bluecellulab.egg-info/top_level.txt +0 -0
  106. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/docs/Makefile +0 -0
  107. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/docs/images/voltage-readme.png +0 -0
  108. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/docs/make.bat +0 -0
  109. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/docs/requirements_docs.txt +0 -0
  110. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/docs/source/_static/.gitkeep +0 -0
  111. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/docs/source/api.rst +0 -0
  112. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/docs/source/changelog.rst +0 -0
  113. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/docs/source/compiling-mechanisms.rst +0 -0
  114. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/docs/source/conf.py +0 -0
  115. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/docs/source/contributing.rst +0 -0
  116. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/docs/source/index.rst +0 -0
  117. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/docs/source/list_of_stim.rst +0 -0
  118. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/docs/source/logo/BlueCelluLabBanner.jpg +0 -0
  119. {bluecellulab-2.6.71 → bluecellulab-2.6.73}/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.9", "3.10", "3.11", "3.12", "3.13"]
18
18
 
19
19
  steps:
20
20
  - uses: actions/checkout@v4
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bluecellulab
3
- Version: 2.6.71
3
+ Version: 2.6.73
4
4
  Summary: Biologically detailed neural network simulations and analysis.
5
5
  Author: Blue Brain Project, EPFL
6
6
  License: Apache2.0
@@ -19,7 +19,7 @@ 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.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
@@ -102,7 +102,7 @@ Main dependencies
102
102
  =================
103
103
 
104
104
  * `Python 3.9+ <https://www.python.org/downloads/release/python-390/>`_
105
- * `Neuron 8.0.2+ <https://pypi.org/project/NEURON/>`_
105
+ * `NEURON <=8.2.7 <https://pypi.org/project/NEURON/>`__
106
106
 
107
107
  Installation
108
108
  ============
@@ -67,7 +67,7 @@ Main dependencies
67
67
  =================
68
68
 
69
69
  * `Python 3.9+ <https://www.python.org/downloads/release/python-390/>`_
70
- * `Neuron 8.0.2+ <https://pypi.org/project/NEURON/>`_
70
+ * `NEURON <=8.2.7 <https://pypi.org/project/NEURON/>`__
71
71
 
72
72
  Installation
73
73
  ============
@@ -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
@@ -793,13 +793,13 @@ class Cell(InjectableMixin, PlottableMixin):
793
793
  mech, var = variable.split(".", 1)
794
794
  mobj = getattr(seg, mech, None)
795
795
  if mobj is None or not hasattr(mobj, f"_ref_{var}"):
796
- raise ValueError(
796
+ raise AttributeError(
797
797
  f"'{variable}' not recordable at {section.name()}({segx}). "
798
798
  f"Mechanisms here: {list(section.psection()['density_mechs'].keys())}"
799
799
  )
800
800
  else:
801
801
  if not hasattr(seg, f"_ref_{variable}"):
802
- raise ValueError(
802
+ raise AttributeError(
803
803
  f"'{variable}' not recordable at {section.name()}({segx}). "
804
804
  f"(Top-level vars are typically v/ina/ik/ica)"
805
805
  )
@@ -911,6 +911,150 @@ class Cell(InjectableMixin, PlottableMixin):
911
911
  def __del__(self):
912
912
  self.delete()
913
913
 
914
+ def get_section(self, section_name: str) -> NeuronSection:
915
+ """Return a single, fully specified NEURON section (e.g., 'soma[0]',
916
+ 'dend[3]').
917
+
918
+ Raises:
919
+ ValueError or TypeError if the section is not found or invalid.
920
+ """
921
+ if section_name in self.sections:
922
+ section = self.sections[section_name]
923
+ if hasattr(section, "nseg"):
924
+ return section
925
+ raise TypeError(f"'{section_name}' exists but is not a NEURON section.")
926
+
927
+ available = ", ".join(self.sections.keys())
928
+ raise ValueError(f"Section '{section_name}' not found. Available: [{available}]")
929
+
930
+ def get_sections(self, section_name: str) -> List[NeuronSection]:
931
+ """Return a list of NEURON sections.
932
+
933
+ If the section name is a fully specified one (e.g., 'dend[3]'), return it as a list of one.
934
+ If the section name is a base name (e.g., 'dend'), return all matching sections like 'dend[0]', 'dend[1]', etc.
935
+
936
+ Raises:
937
+ ValueError if no valid sections are found.
938
+ """
939
+ # Try to interpret as fully qualified section name
940
+ try:
941
+ return [self.get_section(section_name)]
942
+ except ValueError:
943
+ pass # Not a precise match; try prefix match
944
+
945
+ # Fallback to prefix-based match (e.g., 'dend' → 'dend[0]', 'dend[1]', ...)
946
+ matched = [
947
+ section for name, section in self.sections.items()
948
+ if name.startswith(f"{section_name}[")
949
+ ]
950
+ if matched:
951
+ return matched
952
+
953
+ available = ", ".join(self.sections.keys())
954
+ raise ValueError(f"Section '{section_name}' not found. Available: [{available}]")
955
+
956
+ def get_section_by_id(self, section_id: int) -> NeuronSection:
957
+ """Return NEURON section by global section index (LibSONATA
958
+ ordering)."""
959
+ if not self.psections:
960
+ self._init_psections()
961
+
962
+ try:
963
+ return self.psections[int(section_id)].hsection
964
+ except KeyError:
965
+ raise IndexError(f"Section ID {section_id} is out of range for cell {self.cell_id.id}")
966
+
967
+ def resolve_segments_from_compartment_set(self, node_id, compartment_nodes) -> List[Tuple[NeuronSection, str, float]]:
968
+ """Resolve segments for a cell using a predefined compartment node
969
+ list.
970
+
971
+ Supports both LibSONATA format ([node_id, section_id, seg]) and
972
+ name-based format ([node_id, section_name, seg]).
973
+ """
974
+ result = []
975
+ for n_id, sec_ref, seg in compartment_nodes:
976
+ if n_id != node_id:
977
+ continue
978
+
979
+ if isinstance(sec_ref, str):
980
+ # Name-based: e.g., "dend[5]"
981
+ section = self.get_section(sec_ref)
982
+ sec_name = section.name().split(".")[-1]
983
+ elif isinstance(sec_ref, int):
984
+ # ID-based: resolve by section index
985
+ try:
986
+ section = self.get_section_by_id(sec_ref)
987
+ sec_name = section.name().split(".")[-1]
988
+ except AttributeError:
989
+ raise ValueError(f"Cell object does not support section lookup by index: {sec_ref}")
990
+ else:
991
+ raise TypeError(f"Unsupported section reference type: {type(sec_ref)}")
992
+
993
+ result.append((section, sec_name, seg))
994
+ return result
995
+
996
+ def resolve_segments_from_config(self, report_cfg) -> List[Tuple[NeuronSection, str, float]]:
997
+ """Resolve segments from NEURON sections based on config."""
998
+ compartment = report_cfg.get("compartments", "center")
999
+ if compartment not in {"center", "all"}:
1000
+ raise ValueError(
1001
+ f"Unsupported 'compartments' value '{compartment}' — must be 'center' or 'all'."
1002
+ )
1003
+
1004
+ section_name = report_cfg.get("sections", "soma")
1005
+ sections = self.get_sections(section_name)
1006
+
1007
+ targets = []
1008
+ for sec in sections:
1009
+ sec_name = sec.name().split(".")[-1]
1010
+ if compartment == "center":
1011
+ targets.append((sec, sec_name, 0.5))
1012
+ elif compartment == "all":
1013
+ for seg in sec:
1014
+ targets.append((sec, sec_name, seg.x))
1015
+ return targets
1016
+
1017
+ def configure_recording(self, recording_sites, variable_name, report_name):
1018
+ """Configure recording of a variable on a single cell.
1019
+
1020
+ This function sets up the recording of the specified variable (e.g., membrane voltage)
1021
+ in the target cell, for each resolved segment.
1022
+
1023
+ Parameters
1024
+ ----------
1025
+ cell : Any
1026
+ The cell object on which to configure recordings.
1027
+
1028
+ recording_sites : list of tuples
1029
+ List of tuples (section, section_name, segment) where:
1030
+ - section is the section object in the cell.
1031
+ - section_name is the name of the section.
1032
+ - segment is the Neuron segment index (0-1).
1033
+
1034
+ variable_name : str
1035
+ The name of the variable to record (e.g., "v" for membrane voltage).
1036
+
1037
+ report_name : str
1038
+ The name of the report (used in logging).
1039
+ """
1040
+ node_id = self.cell_id.id
1041
+
1042
+ for sec, sec_name, seg in recording_sites:
1043
+ try:
1044
+ self.add_variable_recording(variable=variable_name, section=sec, segx=seg)
1045
+ logger.info(
1046
+ f"Recording '{variable_name}' at {sec_name}({seg}) on GID {node_id} for report '{report_name}'"
1047
+ )
1048
+ except AttributeError:
1049
+ logger.warning(
1050
+ f"Recording for variable '{variable_name}' is not implemented in Cell."
1051
+ )
1052
+ return
1053
+ except Exception as e:
1054
+ logger.warning(
1055
+ f"Failed to record '{variable_name}' at {sec_name}({seg}) on GID {node_id} for report '{report_name}': {e}"
1056
+ )
1057
+
914
1058
  def add_currents_recordings(
915
1059
  self,
916
1060
  section,
@@ -19,6 +19,7 @@ import os
19
19
  from pathlib import Path
20
20
  from typing import Optional
21
21
 
22
+ from bluecellulab.circuit.circuit_access.definition import CircuitAccess
22
23
  import neuron
23
24
  import pandas as pd
24
25
  from bluecellulab import circuit
@@ -42,7 +43,7 @@ if BLUEPY_AVAILABLE:
42
43
  logger = logging.getLogger(__name__)
43
44
 
44
45
 
45
- class BluepyCircuitAccess:
46
+ class BluepyCircuitAccess(CircuitAccess):
46
47
  """Bluepy implementation of CircuitAccess protocol."""
47
48
 
48
49
  def __init__(self, simulation_config: str | Path | SimulationConfig) -> None:
@@ -25,7 +25,7 @@ from bluepysnap import Circuit as SnapCircuit
25
25
  import neuron
26
26
  import pandas as pd
27
27
  from bluecellulab import circuit
28
- from bluecellulab.circuit.circuit_access.definition import EmodelProperties
28
+ from bluecellulab.circuit.circuit_access.definition import CircuitAccess, EmodelProperties
29
29
  from bluecellulab.circuit import CellId, SynapseProperty
30
30
  from bluecellulab.circuit.config import SimulationConfig
31
31
  from bluecellulab.circuit.synapse_properties import SynapseProperties
@@ -38,7 +38,7 @@ from bluecellulab.circuit.synapse_properties import (
38
38
  logger = logging.getLogger(__name__)
39
39
 
40
40
 
41
- class SonataCircuitAccess:
41
+ class SonataCircuitAccess(CircuitAccess):
42
42
  """Sonata implementation of CircuitAccess protocol."""
43
43
 
44
44
  def __init__(self, simulation_config: str | Path | SimulationConfig) -> None:
@@ -188,6 +188,16 @@ class BluepySimulationConfig:
188
188
  else:
189
189
  return None
190
190
 
191
+ @property
192
+ def tstart(self) -> Optional[float]:
193
+ return 0.0
194
+
195
+ @property
196
+ def tstop(self) -> Optional[float]:
197
+ if 'Duration' in self.impl.Run:
198
+ return float(self.impl.Run['Duration'])
199
+ return None
200
+
191
201
  def add_connection_override(
192
202
  self,
193
203
  connection_override: ConnectionOverrides
@@ -130,6 +130,8 @@ class SonataSimulationConfig:
130
130
  """Resolve the full path for the report output file."""
131
131
  output_dir = Path(self.output_root_path)
132
132
  file_name = report_cfg.get("file_name", f"{report_key}.h5")
133
+ if not file_name.endswith(".h5"):
134
+ file_name += ".h5"
133
135
  return output_dir / file_name
134
136
 
135
137
  @property
@@ -31,6 +31,6 @@ def create_cell_id(cell_id: int | tuple[str, int] | CellId) -> CellId:
31
31
  return CellId("", cell_id)
32
32
 
33
33
 
34
- def create_cell_ids(cell_ids: list[int] | list[tuple[str, int] | CellId]) -> list[CellId]:
34
+ def create_cell_ids(cell_ids: list[int | tuple[str, int] | CellId]) -> list[CellId]:
35
35
  """Make a list of CellId from a list of tuple or int."""
36
36
  return [create_cell_id(cell_id) for cell_id in cell_ids]
@@ -125,7 +125,7 @@ class CircuitSimulation:
125
125
 
126
126
  def instantiate_gids(
127
127
  self,
128
- cells: int | tuple[str, int] | list[int] | list[tuple[str, int]],
128
+ cells: int | tuple[str, int] | list[int | tuple[str, int]],
129
129
  add_replay: bool = False,
130
130
  add_stimuli: bool = False,
131
131
  add_synapses: bool = False,
@@ -223,7 +223,7 @@ class CircuitSimulation:
223
223
  will automatically set this option to
224
224
  True.
225
225
  """
226
- if not isinstance(cells, Iterable) or isinstance(cells, tuple):
226
+ if not isinstance(cells, list):
227
227
  cells = [cells]
228
228
 
229
229
  # convert to CellId objects
@@ -42,7 +42,7 @@ class PlotWindow:
42
42
  linenumber = 0
43
43
  for var_name in self.var_list:
44
44
  recording = self.cell.get_recording(var_name)
45
- if recording:
45
+ if recording is not None and recording.size > 0:
46
46
  time = self.cell.get_time()
47
47
  else:
48
48
  time = self.cell.get_time()[1:]
@@ -14,7 +14,11 @@
14
14
 
15
15
  from typing import Optional, Dict
16
16
  from bluecellulab.reports.writers import get_writer
17
- from bluecellulab.reports.utils import extract_spikes_from_cells # helper you already have / write
17
+ from bluecellulab.reports.utils import SUPPORTED_REPORT_TYPES, extract_spikes_from_cells # helper you already have / write
18
+
19
+ import logging
20
+
21
+ logger = logging.getLogger(__name__)
18
22
 
19
23
 
20
24
  class ReportManager:
@@ -52,21 +56,30 @@ class ReportManager:
52
56
 
53
57
  def _write_voltage_reports(self, cells_or_traces):
54
58
  for name, rcfg in self.cfg.get_report_entries().items():
55
- if rcfg.get("type") != "compartment":
59
+ if rcfg.get("type") not in SUPPORTED_REPORT_TYPES:
56
60
  continue
57
61
 
58
- section = rcfg.get("sections")
59
- if section == "compartment_set":
62
+ report_type = rcfg.get("type")
63
+ if report_type == "compartment_set":
60
64
  if rcfg.get("cells") is not None:
61
- raise ValueError("'cells' may not be set with 'compartment_set'")
62
- src_sets, src_type = self.cfg.get_compartment_sets(), "compartment_set"
63
- else:
64
- if rcfg.get("compartments") not in ("center", "all"):
65
+ raise ValueError("'cells' may not be set when using 'compartment_set' report type.")
66
+ if rcfg.get("sections") is not None:
67
+ raise ValueError("'sections' may not be set when using 'compartment_set' report type.")
68
+ if rcfg.get("compartments") is not None:
69
+ raise ValueError("'compartments' may not be set when using 'compartment_set' report type.")
70
+ src_sets = self.cfg.get_compartment_sets()
71
+ elif report_type == "compartment":
72
+ compartments = rcfg.get("compartments") or "center"
73
+ if compartments not in ("center", "all"):
65
74
  raise ValueError("invalid 'compartments' value")
66
- src_sets, src_type = self.cfg.get_node_sets(), "node_set"
75
+ if rcfg.get("cells") is None:
76
+ raise ValueError("'cells' must be specified when using compartment reports")
77
+ src_sets = self.cfg.get_node_sets()
78
+ else:
79
+ logger.error(f"Unsupported report type '{report_type}' in configuration for report '{name}'")
67
80
 
68
81
  rcfg["_source_sets"] = src_sets
69
- rcfg["_source_type"] = src_type
82
+ rcfg["name"] = name
70
83
 
71
84
  out_path = self.cfg.report_file_path(rcfg, name)
72
85
  writer = get_writer("compartment")(rcfg, out_path, self.dt)
@@ -0,0 +1,227 @@
1
+ # Copyright 2025 Open Brain Institute
2
+
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """Report class of bluecellulab."""
15
+
16
+ from collections import defaultdict
17
+ import logging
18
+ from typing import Dict, Any, List
19
+
20
+ from bluecellulab.tools import (
21
+ resolve_source_nodes,
22
+ )
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ SUPPORTED_REPORT_TYPES = {"compartment", "compartment_set"}
27
+
28
+
29
+ def configure_all_reports(cells, simulation_config):
30
+ """Configure recordings for all reports defined in the simulation
31
+ configuration.
32
+
33
+ This iterates through all report entries, resolves source nodes or compartments,
34
+ and configures the corresponding recordings on each cell.
35
+
36
+ Parameters
37
+ ----------
38
+ cells : dict
39
+ Mapping from (population, gid) → Cell object.
40
+
41
+ simulation_config : Any
42
+ Simulation configuration object providing report entries,
43
+ node sets, and compartment sets.
44
+ """
45
+ report_entries = simulation_config.get_report_entries()
46
+
47
+ for report_name, report_cfg in report_entries.items():
48
+ report_type = report_cfg.get("type", "compartment")
49
+ if report_type == "compartment_set":
50
+ source_sets = simulation_config.get_compartment_sets()
51
+ source_name = report_cfg.get("compartment_set")
52
+ if not source_name:
53
+ logger.warning(
54
+ f"Report '{report_name}' does not specify a node set in 'compartment_set' for {report_type}."
55
+ )
56
+ continue
57
+ elif report_type == "compartment":
58
+ source_sets = simulation_config.get_node_sets()
59
+ source_name = report_cfg.get("cells")
60
+ if not source_name:
61
+ logger.warning(
62
+ f"Report '{report_name}' does not specify a node set in 'cells' for {report_type}."
63
+ )
64
+ continue
65
+ else:
66
+ raise NotImplementedError(
67
+ f"Report type '{report_type}' is not supported. "
68
+ f"Supported types: {SUPPORTED_REPORT_TYPES}"
69
+ )
70
+
71
+ source = source_sets.get(source_name)
72
+ if not source:
73
+ logger.warning(
74
+ f"{report_type} '{source_name}' not found for report '{report_name}', skipping recording."
75
+ )
76
+ continue
77
+
78
+ population = source["population"]
79
+ node_ids, compartment_nodes = resolve_source_nodes(
80
+ source, report_type, cells, population
81
+ )
82
+ recording_sites_per_cell = build_recording_sites(
83
+ cells, node_ids, population, report_type, report_cfg, compartment_nodes
84
+ )
85
+ variable_name = report_cfg.get("variable_name", "v")
86
+
87
+ for node_id, recording_sites in recording_sites_per_cell.items():
88
+ cell = cells.get((population, node_id))
89
+ if not cell or recording_sites is None:
90
+ continue
91
+
92
+ cell.configure_recording(recording_sites, variable_name, report_name)
93
+
94
+
95
+ def build_recording_sites(
96
+ cells_or_traces, node_ids, population, report_type, report_cfg, compartment_nodes
97
+ ):
98
+ """Build per-cell recording sites based on source type and report
99
+ configuration.
100
+
101
+ This function resolves the segments (section, name, seg.x) where variables
102
+ should be recorded for each cell, based on either a node set (standard
103
+ compartment reports) or a compartment set (predefined segment list).
104
+
105
+ Parameters
106
+ ----------
107
+ cells_or_traces : dict
108
+ Either a mapping from (population, node_id) to Cell objects (live sim),
109
+ or from gid_key strings to trace dicts (gathered traces on rank 0).
110
+
111
+ node_ids : list of int
112
+ List of node IDs for which recordings should be configured.
113
+
114
+ population : str
115
+ Name of the population to which the cells belong.
116
+
117
+ report_type : str
118
+ The report type, either 'compartment_set' or 'compartment'.
119
+
120
+ report_cfg : dict
121
+ Configuration dictionary specifying report parameters
122
+
123
+ compartment_nodes : list or None
124
+ Optional list of [node_id, section_name, seg_x] defining segment locations
125
+ for each cell (used if report_type == 'compartment_set').
126
+
127
+ Returns
128
+ -------
129
+ dict
130
+ Mapping from node ID to list of recording site tuples:
131
+ (section_object, section_name, seg_x).
132
+ """
133
+ targets_per_cell = {}
134
+
135
+ for node_id in node_ids:
136
+ # Handle both (pop, id) and "pop_id" keys
137
+ key = (population, node_id)
138
+ cell_or_trace = cells_or_traces.get(key) or cells_or_traces.get(f"{population}_{node_id}")
139
+ if not cell_or_trace:
140
+ continue
141
+
142
+ if isinstance(cell_or_trace, dict): # Trace dict, not Cell
143
+ if report_type == "compartment_set":
144
+ # Find all entries matching node_id
145
+ targets = [
146
+ (None, section_name, segx)
147
+ for nid, section_name, segx in compartment_nodes
148
+ if nid == node_id
149
+ ]
150
+ elif report_type == "compartment":
151
+ section_name = report_cfg.get("sections", "soma")
152
+ segx = 0.5 if report_cfg.get("compartments", "center") == "center" else 0.0
153
+ targets = [(None, f"{section_name}[0]", segx)]
154
+ else:
155
+ raise NotImplementedError(
156
+ f"Unsupported report type '{report_type}' in trace-based output."
157
+ )
158
+ else:
159
+ # Cell object
160
+ if report_type == "compartment_set":
161
+ targets = cell_or_trace.resolve_segments_from_compartment_set(
162
+ node_id, compartment_nodes
163
+ )
164
+ elif report_type == "compartment":
165
+ targets = cell_or_trace.resolve_segments_from_config(report_cfg)
166
+ else:
167
+ raise NotImplementedError(
168
+ f"Report type '{report_type}' is not supported. "
169
+ f"Supported types: {SUPPORTED_REPORT_TYPES}"
170
+ )
171
+
172
+ targets_per_cell[node_id] = targets
173
+
174
+ return targets_per_cell
175
+
176
+
177
+ def extract_spikes_from_cells(
178
+ cells: Dict[Any, Any],
179
+ location: str = "soma",
180
+ threshold: float = -20.0,
181
+ ) -> Dict[str, Dict[int, list]]:
182
+ """Extract spike times from recorded cells, grouped by population.
183
+
184
+ Parameters
185
+ ----------
186
+ cells : dict
187
+ Mapping from (population, gid) → Cell object, or similar.
188
+
189
+ location : str
190
+ Recording location passed to Cell.get_recorded_spikes().
191
+
192
+ threshold : float
193
+ Voltage threshold (mV) used for spike detection.
194
+
195
+ Returns
196
+ -------
197
+ spikes_by_population : dict
198
+ {population → {gid_int → [spike_times_ms]}}
199
+ """
200
+ spikes_by_pop: defaultdict[str, Dict[int, List[float]]] = defaultdict(dict)
201
+
202
+ for key, cell in cells.items():
203
+ # Resolve the key to (population, gid)
204
+ if isinstance(key, tuple):
205
+ pop, gid = key
206
+ elif isinstance(key, str):
207
+ try:
208
+ pop, gid_str = key.rsplit("_", 1)
209
+ gid = int(gid_str)
210
+ except Exception:
211
+ raise ValueError(
212
+ f"Cell key '{key}' could not be parsed as 'population_gid'"
213
+ )
214
+ else:
215
+ raise ValueError(f"Cell key '{key}' is not a recognized format.")
216
+
217
+ if not hasattr(cell, "get_recorded_spikes"):
218
+ raise TypeError(
219
+ f"Cannot extract spikes: cell entry {key} is not a Cell object (got {type(cell)}). "
220
+ "If you have precomputed traces, pass them as `spikes_by_pop`."
221
+ )
222
+
223
+ times = cell.get_recorded_spikes(location=location, threshold=threshold)
224
+ # Always assign, even if empty
225
+ spikes_by_pop[pop][gid] = list(times) if times is not None else []
226
+
227
+ return dict(spikes_by_pop)