bluecellulab 2.6.71__tar.gz → 2.6.72__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.72}/PKG-INFO +1 -1
  2. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/cell/core.py +147 -3
  3. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/circuit/config/sonata_simulation_config.py +2 -0
  4. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/reports/manager.py +23 -10
  5. bluecellulab-2.6.72/bluecellulab/reports/utils.py +227 -0
  6. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/reports/writers/compartment.py +45 -28
  7. bluecellulab-2.6.72/bluecellulab/reports/writers/spikes.py +86 -0
  8. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/tools.py +9 -79
  9. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab.egg-info/PKG-INFO +1 -1
  10. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/tox.ini +1 -0
  11. bluecellulab-2.6.71/bluecellulab/reports/utils.py +0 -156
  12. bluecellulab-2.6.71/bluecellulab/reports/writers/spikes.py +0 -61
  13. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/.compile_mod.sh +0 -0
  14. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/.gitattributes +0 -0
  15. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/.github/dependabot.yml +0 -0
  16. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/.github/workflows/release.yml +0 -0
  17. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/.github/workflows/test.yml +0 -0
  18. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/.gitignore +0 -0
  19. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/.gitlab-ci.yml +0 -0
  20. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/.readthedocs.yml +0 -0
  21. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/.zenodo.json +0 -0
  22. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/AUTHORS.txt +0 -0
  23. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/CHANGELOG.rst +0 -0
  24. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/CITATION.cff +0 -0
  25. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/CONTRIBUTING.rst +0 -0
  26. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/LICENSE +0 -0
  27. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/MANIFEST.in +0 -0
  28. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/Makefile +0 -0
  29. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/README.rst +0 -0
  30. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/__init__.py +0 -0
  31. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/analysis/__init__.py +0 -0
  32. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/analysis/analysis.py +0 -0
  33. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/analysis/inject_sequence.py +0 -0
  34. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/analysis/plotting.py +0 -0
  35. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/analysis/utils.py +0 -0
  36. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/cell/__init__.py +0 -0
  37. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/cell/ballstick/__init__.py +0 -0
  38. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/cell/ballstick/emodel.hoc +0 -0
  39. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/cell/ballstick/morphology.asc +0 -0
  40. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/cell/cell_dict.py +0 -0
  41. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/cell/injector.py +0 -0
  42. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/cell/plotting.py +0 -0
  43. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/cell/random.py +0 -0
  44. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/cell/recording.py +0 -0
  45. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/cell/section_distance.py +0 -0
  46. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/cell/section_tools.py +0 -0
  47. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/cell/serialized_sections.py +0 -0
  48. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/cell/sonata_proxy.py +0 -0
  49. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/cell/stimuli_generator.py +0 -0
  50. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/cell/template.py +0 -0
  51. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/circuit/__init__.py +0 -0
  52. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/circuit/circuit_access/__init__.py +0 -0
  53. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/circuit/circuit_access/bluepy_circuit_access.py +0 -0
  54. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/circuit/circuit_access/definition.py +0 -0
  55. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/circuit/circuit_access/sonata_circuit_access.py +0 -0
  56. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/circuit/config/__init__.py +0 -0
  57. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/circuit/config/bluepy_simulation_config.py +0 -0
  58. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/circuit/config/definition.py +0 -0
  59. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/circuit/config/sections.py +0 -0
  60. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/circuit/format.py +0 -0
  61. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/circuit/iotools.py +0 -0
  62. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/circuit/node_id.py +0 -0
  63. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/circuit/simulation_access.py +0 -0
  64. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/circuit/synapse_properties.py +0 -0
  65. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/circuit/validate.py +0 -0
  66. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/circuit_simulation.py +0 -0
  67. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/connection.py +0 -0
  68. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/dendrogram.py +0 -0
  69. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/exceptions.py +0 -0
  70. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/graph.py +0 -0
  71. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/hoc/Cell.hoc +0 -0
  72. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/hoc/RNGSettings.hoc +0 -0
  73. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/hoc/TDistFunc.hoc +0 -0
  74. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/hoc/TStim.hoc +0 -0
  75. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/hoc/fileUtils.hoc +0 -0
  76. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/importer.py +0 -0
  77. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/neuron_interpreter.py +0 -0
  78. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/plotwindow.py +0 -0
  79. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/psection.py +0 -0
  80. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/psegment.py +0 -0
  81. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/reports/__init__.py +0 -0
  82. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/reports/writers/__init__.py +0 -0
  83. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/reports/writers/base_writer.py +0 -0
  84. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/rngsettings.py +0 -0
  85. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/simulation/__init__.py +0 -0
  86. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/simulation/neuron_globals.py +0 -0
  87. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/simulation/parallel.py +0 -0
  88. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/simulation/report.py +0 -0
  89. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/simulation/simulation.py +0 -0
  90. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/stimulus/__init__.py +0 -0
  91. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/stimulus/circuit_stimulus_definitions.py +0 -0
  92. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/stimulus/factory.py +0 -0
  93. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/stimulus/stimulus.py +0 -0
  94. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/synapse/__init__.py +0 -0
  95. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/synapse/synapse_factory.py +0 -0
  96. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/synapse/synapse_types.py +0 -0
  97. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/type_aliases.py +0 -0
  98. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/utils.py +0 -0
  99. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/validation/validation.py +0 -0
  100. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab/verbosity.py +0 -0
  101. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab.egg-info/SOURCES.txt +0 -0
  102. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab.egg-info/dependency_links.txt +0 -0
  103. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab.egg-info/requires.txt +0 -0
  104. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/bluecellulab.egg-info/top_level.txt +0 -0
  105. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/docs/Makefile +0 -0
  106. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/docs/images/voltage-readme.png +0 -0
  107. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/docs/make.bat +0 -0
  108. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/docs/requirements_docs.txt +0 -0
  109. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/docs/source/_static/.gitkeep +0 -0
  110. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/docs/source/api.rst +0 -0
  111. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/docs/source/changelog.rst +0 -0
  112. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/docs/source/compiling-mechanisms.rst +0 -0
  113. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/docs/source/conf.py +0 -0
  114. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/docs/source/contributing.rst +0 -0
  115. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/docs/source/index.rst +0 -0
  116. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/docs/source/list_of_stim.rst +0 -0
  117. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/docs/source/logo/BlueCelluLabBanner.jpg +0 -0
  118. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/pyproject.toml +0 -0
  119. {bluecellulab-2.6.71 → bluecellulab-2.6.72}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bluecellulab
3
- Version: 2.6.71
3
+ Version: 2.6.72
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 @@ 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,
@@ -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
@@ -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)
@@ -16,10 +16,11 @@ from pathlib import Path
16
16
  import numpy as np
17
17
  import h5py
18
18
  from typing import Dict, List
19
+
19
20
  from .base_writer import BaseReportWriter
20
21
  from bluecellulab.reports.utils import (
22
+ build_recording_sites,
21
23
  resolve_source_nodes,
22
- resolve_segments,
23
24
  )
24
25
  import logging
25
26
 
@@ -31,52 +32,67 @@ class CompartmentReportWriter(BaseReportWriter):
31
32
 
32
33
  def write(self, cells: Dict, tstart=0):
33
34
  report_name = self.cfg.get("name", "unnamed")
34
- # section = self.cfg.get("sections")
35
35
  variable = self.cfg.get("variable_name", "v")
36
+ report_type = self.cfg.get("type", "compartment")
36
37
 
38
+ # Resolve source set
37
39
  source_sets = self.cfg["_source_sets"]
38
- source_type = self.cfg["_source_type"]
39
- src_name = self.cfg.get("cells") if source_type == "node_set" else self.cfg.get("compartments")
40
+ if report_type == "compartment":
41
+ src_name = self.cfg.get("cells")
42
+ elif report_type == "compartment_set":
43
+ src_name = self.cfg.get("compartment_set")
44
+ else:
45
+ raise NotImplementedError(
46
+ f"Unsupported report type '{report_type}' in configuration for report '{report_name}'"
47
+ )
48
+
40
49
  src = source_sets.get(src_name)
41
50
  if not src:
42
- logger.warning(f"{source_type.title()} '{src_name}' not found – skipping '{report_name}'.")
51
+ logger.warning(f"{report_type} '{src_name}' not found – skipping '{report_name}'.")
43
52
  return
44
53
 
45
54
  population = src["population"]
46
- node_ids, comp_nodes = resolve_source_nodes(src, source_type, cells, population)
55
+ node_ids, comp_nodes = resolve_source_nodes(src, report_type, cells, population)
56
+ recording_sites_per_cell = build_recording_sites(
57
+ cells, node_ids, population, report_type, self.cfg, comp_nodes
58
+ )
59
+
60
+ # Detect trace mode
61
+ sample_cell = next(iter(cells.values()))
62
+ is_trace_mode = isinstance(sample_cell, dict)
47
63
 
48
64
  data_matrix: List[np.ndarray] = []
49
65
  node_id_list: List[int] = []
50
66
  idx_ptr: List[int] = [0]
51
67
  elem_ids: List[int] = []
52
68
 
53
- for nid in node_ids:
69
+ for nid in sorted(recording_sites_per_cell):
70
+ recording_sites = recording_sites_per_cell[nid]
54
71
  cell = cells.get((population, nid)) or cells.get(f"{population}_{nid}")
55
72
  if cell is None:
73
+ logger.warning(f"Cell or trace for ({population}, {nid}) not found – skipping.")
56
74
  continue
57
75
 
58
- if isinstance(cell, dict):
59
- # No section/segment structure to resolve for traces
60
- trace = np.asarray(cell["voltage"], dtype=np.float32)
61
- data_matrix.append(trace)
62
- node_id_list.append(nid)
63
- elem_ids.append(len(elem_ids))
64
- idx_ptr.append(idx_ptr[-1] + 1)
65
- continue
66
-
67
- targets = resolve_segments(cell, self.cfg, nid, comp_nodes, source_type)
68
- for sec, sec_name, seg in targets:
69
- try:
70
- if hasattr(cell, "get_variable_recording"):
71
- trace = cell.get_variable_recording(variable=variable, section=sec, segx=seg)
72
- else:
73
- trace = np.asarray(cell["voltage"], dtype=np.float32)
74
- data_matrix.append(trace)
76
+ if is_trace_mode:
77
+ voltage = np.asarray(cell["voltage"], dtype=np.float32)
78
+ for sec, sec_name, seg in recording_sites:
79
+ data_matrix.append(voltage)
75
80
  node_id_list.append(nid)
76
81
  elem_ids.append(len(elem_ids))
77
82
  idx_ptr.append(idx_ptr[-1] + 1)
78
- except Exception as e:
79
- logger.warning(f"Failed recording {nid}:{sec_name}@{seg}: {e}")
83
+ else:
84
+ for sec, sec_name, seg in recording_sites:
85
+ try:
86
+ if hasattr(cell, "get_variable_recording"):
87
+ trace = cell.get_variable_recording(variable=variable, section=sec, segx=seg)
88
+ else:
89
+ trace = np.asarray(cell["voltage"], dtype=np.float32)
90
+ data_matrix.append(trace)
91
+ node_id_list.append(nid)
92
+ elem_ids.append(len(elem_ids))
93
+ idx_ptr.append(idx_ptr[-1] + 1)
94
+ except Exception as e:
95
+ logger.warning(f"Failed recording {nid}:{sec_name}@{seg}: {e}")
80
96
 
81
97
  if not data_matrix:
82
98
  logger.warning(f"No data for report '{report_name}'.")
@@ -148,7 +164,8 @@ class CompartmentReportWriter(BaseReportWriter):
148
164
  if dt_report < sim_dt:
149
165
  logger.warning(
150
166
  f"Requested report dt={dt_report} ms is finer than simulation dt={sim_dt} ms. "
151
- f"Clamping report dt to {sim_dt} ms."
167
+ f"Clamping report dt to {sim_dt} ms. "
168
+ f"To achieve finer temporal resolution, reduce the simulation dt in your config."
152
169
  )
153
170
  dt_report = sim_dt
154
171
 
@@ -161,7 +178,7 @@ class CompartmentReportWriter(BaseReportWriter):
161
178
  # Downsample the data if needed
162
179
  # Compute start and end indices in the original data
163
180
  start_index = int(round((start_time - tstart) / sim_dt))
164
- end_index = int(round((end_time - tstart) / sim_dt)) + 1 # inclusive
181
+ end_index = int(round((end_time - tstart) / sim_dt))
165
182
 
166
183
  # Now slice and downsample
167
184
  data_matrix_downsampled = [