From f2d21f22c10d851d92a77604635cfa87380065ef Mon Sep 17 00:00:00 2001 From: Gianluca Ficarelli <26835404+GianlucaFicarelli@users.noreply.github.com> Date: Fri, 15 Mar 2024 10:11:45 +0100 Subject: [PATCH] [NSETM-2226] Add NodeSets adapter to support custom node_sets_file (#22) Support custom node_sets_file in extraction, neuron_classes and trial_steps --- CHANGELOG.rst | 9 +++ pyproject.toml | 2 +- src/blueetl/adapters/base.py | 23 +++--- src/blueetl/adapters/circuit.py | 29 ++++++-- src/blueetl/adapters/impl/__init__.py | 1 + .../adapters/{ => impl}/bluepy/__init__.py | 0 .../adapters/{ => impl}/bluepy/circuit.py | 12 +++ .../adapters/{ => impl}/bluepy/simulation.py | 2 +- .../{ => impl}/bluepysnap/__init__.py | 0 .../adapters/{ => impl}/bluepysnap/circuit.py | 14 ++++ .../adapters/impl/bluepysnap/node_sets.py | 13 ++++ .../{ => impl}/bluepysnap/simulation.py | 2 +- src/blueetl/adapters/interfaces/__init__.py | 2 +- src/blueetl/adapters/interfaces/circuit.py | 12 +++ src/blueetl/adapters/interfaces/node_sets.py | 23 ++++++ src/blueetl/adapters/node_sets.py | 33 +++++++++ src/blueetl/adapters/simulation.py | 20 ++--- src/blueetl/config/analysis.py | 23 +++++- src/blueetl/config/analysis_model.py | 6 +- src/blueetl/extract/neurons.py | 16 +++- src/blueetl/extract/simulations.py | 3 +- src/blueetl/extract/windows.py | 14 +++- src/blueetl/schemas/analysis_config.yaml | 21 +++++- .../sonata/config/analysis_config_12.yaml | 34 +++++++++ .../sonata/config/node_sets/node_sets_01.json | 6 ++ .../spikes/features/by_gid.parquet | Bin 0 -> 30885 bytes .../spikes/features/by_gid_and_trial.parquet | Bin 0 -> 37550 bytes .../spikes/features/by_neuron_class.parquet | Bin 0 -> 11302 bytes .../by_neuron_class_and_trial.parquet | Bin 0 -> 6189 bytes .../spikes/features/histograms.parquet | Bin 0 -> 12767 bytes .../spikes/repo/neuron_classes.parquet | Bin 0 -> 6077 bytes .../analysis_12/spikes/repo/neurons.parquet | Bin 0 -> 20175 bytes .../analysis_12/spikes/repo/report.parquet | Bin 0 -> 13364 bytes .../spikes/repo/simulations.parquet | Bin 0 -> 4506 bytes .../analysis_12/spikes/repo/windows.parquet | Bin 0 -> 7483 bytes tests/functional/test_analysis.py | 8 ++ tests/unit/adapters/test_circuit.py | 20 ++++- tests/unit/adapters/test_node_sets.py | 62 ++++++++++++++++ tests/unit/adapters/test_simulation.py | 7 +- tests/unit/apps/test_migrate.py | 8 +- tests/unit/config/test_analysis.py | 27 ++++++- tests/unit/data/circuit/README | 2 +- tests/unit/data/circuit/sonata/node_sets.json | 70 ++++++++++++++++++ .../data/circuit/sonata/node_sets_extra.json | 5 ++ tests/unit/data/simulation/README | 2 +- tests/unit/extract/conftest.py | 9 ++- tests/unit/extract/test_neuron_classes.py | 8 +- tests/unit/extract/test_neurons.py | 28 +++++-- tests/unit/extract/test_simulations.py | 36 ++++----- tests/unit/extract/test_windows.py | 3 + tests/unit/utils.py | 2 + tox.ini | 4 +- 52 files changed, 522 insertions(+), 99 deletions(-) create mode 100644 src/blueetl/adapters/impl/__init__.py rename src/blueetl/adapters/{ => impl}/bluepy/__init__.py (100%) rename src/blueetl/adapters/{ => impl}/bluepy/circuit.py (77%) rename src/blueetl/adapters/{ => impl}/bluepy/simulation.py (98%) rename src/blueetl/adapters/{ => impl}/bluepysnap/__init__.py (100%) rename src/blueetl/adapters/{ => impl}/bluepysnap/circuit.py (59%) create mode 100644 src/blueetl/adapters/impl/bluepysnap/node_sets.py rename src/blueetl/adapters/{ => impl}/bluepysnap/simulation.py (95%) create mode 100644 src/blueetl/adapters/interfaces/node_sets.py create mode 100644 src/blueetl/adapters/node_sets.py create mode 100644 tests/functional/data/sonata/config/analysis_config_12.yaml create mode 100644 tests/functional/data/sonata/config/node_sets/node_sets_01.json create mode 100644 tests/functional/data/sonata/expected/analysis_12/spikes/features/by_gid.parquet create mode 100644 tests/functional/data/sonata/expected/analysis_12/spikes/features/by_gid_and_trial.parquet create mode 100644 tests/functional/data/sonata/expected/analysis_12/spikes/features/by_neuron_class.parquet create mode 100644 tests/functional/data/sonata/expected/analysis_12/spikes/features/by_neuron_class_and_trial.parquet create mode 100644 tests/functional/data/sonata/expected/analysis_12/spikes/features/histograms.parquet create mode 100644 tests/functional/data/sonata/expected/analysis_12/spikes/repo/neuron_classes.parquet create mode 100644 tests/functional/data/sonata/expected/analysis_12/spikes/repo/neurons.parquet create mode 100644 tests/functional/data/sonata/expected/analysis_12/spikes/repo/report.parquet create mode 100644 tests/functional/data/sonata/expected/analysis_12/spikes/repo/simulations.parquet create mode 100644 tests/functional/data/sonata/expected/analysis_12/spikes/repo/windows.parquet create mode 100644 tests/unit/adapters/test_node_sets.py create mode 100644 tests/unit/data/circuit/sonata/node_sets.json create mode 100644 tests/unit/data/circuit/sonata/node_sets_extra.json diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1b06c7f..12e0642 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,15 @@ Changelog ========= +Version 0.8.0 +------------- + +New Features +~~~~~~~~~~~~ + +- Support custom node_sets_file in extraction, neuron_classes and trial_steps [NSETM-2226] + + Version 0.7.1 ------------- diff --git a/pyproject.toml b/pyproject.toml index b15a16f..ec18df3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,7 @@ branch = true parallel = false omit = [ "*/blueetl/_version.py", - "*/blueetl/adapters/bluepy/*.py", + "*/blueetl/adapters/impl/bluepy/*.py", "*/blueetl/external/**/*.py", ] diff --git a/src/blueetl/adapters/base.py b/src/blueetl/adapters/base.py index dbbce5c..417131b 100644 --- a/src/blueetl/adapters/base.py +++ b/src/blueetl/adapters/base.py @@ -1,13 +1,14 @@ """Base Adapter.""" from abc import ABC, abstractmethod +from pathlib import Path from typing import Any, Generic, Optional, TypeVar from blueetl.adapters.interfaces.circuit import CircuitInterface +from blueetl.adapters.interfaces.node_sets import NodeSetsInterface from blueetl.adapters.interfaces.simulation import SimulationInterface -from blueetl.types import StrOrPath -InterfaceT = TypeVar("InterfaceT", CircuitInterface, SimulationInterface) +InterfaceT = TypeVar("InterfaceT", CircuitInterface, SimulationInterface, NodeSetsInterface) BaseAdapterT = TypeVar("BaseAdapterT", bound="BaseAdapter") @@ -18,13 +19,14 @@ class AdapterError(Exception): class BaseAdapter(Generic[InterfaceT], ABC): """Base Adapter to be subclassed by other adapters.""" - def __init__(self, config: StrOrPath) -> None: - """Init the adapter from the specified config.""" - self._impl: Optional[InterfaceT] = self._load_impl(str(config)) + def __init__(self, _impl: Optional[InterfaceT]) -> None: + """Init the adapter from the specified implementation.""" + self._impl: Optional[InterfaceT] = _impl + @classmethod @abstractmethod - def _load_impl(self, config: str) -> Optional[InterfaceT]: - """Load and return the implementation object, or None if the config file doesn't exist.""" + def from_file(cls, filepath: Optional[Path]) -> "BaseAdapter": + """Load and return a new object from file.""" @property def _ensure_impl(self) -> InterfaceT: @@ -41,10 +43,3 @@ def exists(self) -> bool: def instance(self) -> Any: """Return the wrapped instance, or None if it doesn't exist.""" return self._impl.instance if self._impl is not None else None - - @classmethod - def from_impl(cls: type[BaseAdapterT], impl: InterfaceT) -> BaseAdapterT: - """Return a new adapter with the specified implementation.""" - result = object.__new__(cls) - result._impl = impl - return result diff --git a/src/blueetl/adapters/circuit.py b/src/blueetl/adapters/circuit.py index 77e0516..e2e38ca 100644 --- a/src/blueetl/adapters/circuit.py +++ b/src/blueetl/adapters/circuit.py @@ -6,22 +6,25 @@ from blueetl.adapters.base import BaseAdapter from blueetl.adapters.interfaces.circuit import CircuitInterface, NodePopulationInterface +from blueetl.adapters.node_sets import NodeSetsAdapter class CircuitAdapter(BaseAdapter[CircuitInterface]): """Circuit Adapter.""" - def _load_impl(self, config: str) -> Optional[CircuitInterface]: - """Load and return the implementation object, or None if the config file doesn't exist.""" + @classmethod + def from_file(cls, filepath: Optional[Path]) -> "CircuitAdapter": + """Load and return a new object from file.""" # pylint: disable=import-outside-toplevel - if not config or not Path(config).exists(): - return None + if not filepath or not filepath.exists(): + return cls(None) CircuitImpl: type[CircuitInterface] - if config.endswith(".json"): - from blueetl.adapters.bluepysnap.circuit import Circuit, CircuitImpl + if filepath.suffix == ".json": + from blueetl.adapters.impl.bluepysnap.circuit import Circuit, CircuitImpl else: - from blueetl.adapters.bluepy.circuit import Circuit, CircuitImpl - return CircuitImpl(Circuit(config)) + from blueetl.adapters.impl.bluepy.circuit import Circuit, CircuitImpl + impl = CircuitImpl(Circuit(str(filepath))) + return cls(impl) def checksum(self) -> str: """Return a checksum of the relevant keys in the circuit configuration.""" @@ -31,3 +34,13 @@ def checksum(self) -> str: def nodes(self) -> Mapping[Optional[str], NodePopulationInterface]: """Return the nodes as a dict: population -> nodes.""" return self._ensure_impl.nodes + + @property + def node_sets_file(self) -> Optional[Path]: + """Returns the NodeSets file used by the circuit.""" + return self._ensure_impl.node_sets_file + + @property + def node_sets(self) -> NodeSetsAdapter: + """Returns the NodeSets file used by the circuit.""" + return NodeSetsAdapter(self._ensure_impl.node_sets) diff --git a/src/blueetl/adapters/impl/__init__.py b/src/blueetl/adapters/impl/__init__.py new file mode 100644 index 0000000..a568ce3 --- /dev/null +++ b/src/blueetl/adapters/impl/__init__.py @@ -0,0 +1 @@ +"""Implementations.""" diff --git a/src/blueetl/adapters/bluepy/__init__.py b/src/blueetl/adapters/impl/bluepy/__init__.py similarity index 100% rename from src/blueetl/adapters/bluepy/__init__.py rename to src/blueetl/adapters/impl/bluepy/__init__.py diff --git a/src/blueetl/adapters/bluepy/circuit.py b/src/blueetl/adapters/impl/bluepy/circuit.py similarity index 77% rename from src/blueetl/adapters/bluepy/circuit.py rename to src/blueetl/adapters/impl/bluepy/circuit.py index 3b01bf5..2808378 100644 --- a/src/blueetl/adapters/bluepy/circuit.py +++ b/src/blueetl/adapters/impl/bluepy/circuit.py @@ -1,11 +1,13 @@ """Bluepy circuit implementation.""" from collections.abc import Mapping +from pathlib import Path from typing import Optional from bluepy import Circuit from blueetl.adapters.interfaces.circuit import CircuitInterface, NodePopulationInterface +from blueetl.adapters.interfaces.node_sets import NodeSetsInterface from blueetl.utils import checksum_json @@ -40,3 +42,13 @@ def nodes(self) -> Mapping[Optional[str], NodePopulationInterface]: The population name in the returned dict is always None, because undefined in the config. """ return {None: self._circuit.cells} + + @property + def node_sets_file(self) -> Optional[Path]: + """Returns the NodeSets file used by the circuit.""" + raise NotImplementedError + + @property + def node_sets(self) -> NodeSetsInterface: + """Returns the NodeSets used by the circuit.""" + raise NotImplementedError diff --git a/src/blueetl/adapters/bluepy/simulation.py b/src/blueetl/adapters/impl/bluepy/simulation.py similarity index 98% rename from src/blueetl/adapters/bluepy/simulation.py rename to src/blueetl/adapters/impl/bluepy/simulation.py index ca2023d..1305751 100644 --- a/src/blueetl/adapters/bluepy/simulation.py +++ b/src/blueetl/adapters/impl/bluepy/simulation.py @@ -12,7 +12,7 @@ from bluepy.impl.spike_report import SpikeReport from bluepy.simulation import PathHelpers -from blueetl.adapters.bluepy.circuit import CircuitImpl +from blueetl.adapters.impl.bluepy.circuit import CircuitImpl from blueetl.adapters.interfaces.circuit import CircuitInterface from blueetl.adapters.interfaces.simulation import ( PopulationReportInterface, diff --git a/src/blueetl/adapters/bluepysnap/__init__.py b/src/blueetl/adapters/impl/bluepysnap/__init__.py similarity index 100% rename from src/blueetl/adapters/bluepysnap/__init__.py rename to src/blueetl/adapters/impl/bluepysnap/__init__.py diff --git a/src/blueetl/adapters/bluepysnap/circuit.py b/src/blueetl/adapters/impl/bluepysnap/circuit.py similarity index 59% rename from src/blueetl/adapters/bluepysnap/circuit.py rename to src/blueetl/adapters/impl/bluepysnap/circuit.py index e7ceb7a..bf34e6a 100644 --- a/src/blueetl/adapters/bluepysnap/circuit.py +++ b/src/blueetl/adapters/impl/bluepysnap/circuit.py @@ -1,11 +1,14 @@ """Bluepysnap circuit implementation.""" from collections.abc import Mapping +from pathlib import Path from typing import Optional from bluepysnap import Circuit +from blueetl.adapters.impl.bluepysnap.node_sets import NodeSetsImpl from blueetl.adapters.interfaces.circuit import CircuitInterface, NodePopulationInterface +from blueetl.adapters.interfaces.node_sets import NodeSetsInterface from blueetl.utils import checksum_json @@ -23,3 +26,14 @@ def checksum(self) -> str: def nodes(self) -> Mapping[Optional[str], NodePopulationInterface]: """Return the nodes as a dict: population -> nodes.""" return self._circuit.nodes + + @property + def node_sets_file(self) -> Optional[Path]: + """Returns the NodeSets file used by the circuit.""" + path = self._circuit.to_libsonata.node_sets_path + return Path(path) if path else None + + @property + def node_sets(self) -> NodeSetsInterface: + """Returns the NodeSets used by the circuit.""" + return NodeSetsImpl(self._circuit.node_sets) diff --git a/src/blueetl/adapters/impl/bluepysnap/node_sets.py b/src/blueetl/adapters/impl/bluepysnap/node_sets.py new file mode 100644 index 0000000..9d60fa6 --- /dev/null +++ b/src/blueetl/adapters/impl/bluepysnap/node_sets.py @@ -0,0 +1,13 @@ +"""Bluepysnap NodeSets implementation.""" + +from bluepysnap.node_sets import NodeSets + +from blueetl.adapters.interfaces.node_sets import NodeSetsInterface + + +class NodeSetsImpl(NodeSetsInterface[NodeSets]): + """Bluepysnap NodeSets implementation.""" + + def update(self, other: NodeSetsInterface) -> None: + """Update the wrapped node sets.""" + self._node_sets.update(other.instance) diff --git a/src/blueetl/adapters/bluepysnap/simulation.py b/src/blueetl/adapters/impl/bluepysnap/simulation.py similarity index 95% rename from src/blueetl/adapters/bluepysnap/simulation.py rename to src/blueetl/adapters/impl/bluepysnap/simulation.py index 6b4c455..3dfcbe6 100644 --- a/src/blueetl/adapters/bluepysnap/simulation.py +++ b/src/blueetl/adapters/impl/bluepysnap/simulation.py @@ -7,7 +7,7 @@ from bluepysnap import Simulation -from blueetl.adapters.bluepysnap.circuit import CircuitImpl +from blueetl.adapters.impl.bluepysnap.circuit import CircuitImpl from blueetl.adapters.interfaces.circuit import CircuitInterface from blueetl.adapters.interfaces.simulation import ( PopulationReportInterface, diff --git a/src/blueetl/adapters/interfaces/__init__.py b/src/blueetl/adapters/interfaces/__init__.py index ab0cb32..7ddca4b 100644 --- a/src/blueetl/adapters/interfaces/__init__.py +++ b/src/blueetl/adapters/interfaces/__init__.py @@ -1 +1 @@ -"""Interfaces for Simulation and Circuit.""" +"""Interfaces.""" diff --git a/src/blueetl/adapters/interfaces/circuit.py b/src/blueetl/adapters/interfaces/circuit.py index e1ece14..afea8df 100644 --- a/src/blueetl/adapters/interfaces/circuit.py +++ b/src/blueetl/adapters/interfaces/circuit.py @@ -2,11 +2,13 @@ from abc import ABC, abstractmethod from collections.abc import Mapping +from pathlib import Path from typing import Generic, Optional, TypeVar import numpy as np import pandas as pd +from blueetl.adapters.interfaces.node_sets import NodeSetsInterface from blueetl.utils import CachedPropertyMixIn CircuitT = TypeVar("CircuitT") @@ -44,3 +46,13 @@ def checksum(self) -> str: @abstractmethod def nodes(self) -> Mapping[Optional[str], NodePopulationInterface]: """Return the nodes as a dict: population -> nodes.""" + + @property + @abstractmethod + def node_sets_file(self) -> Optional[Path]: + """Returns the NodeSets file used by the circuit.""" + + @property + @abstractmethod + def node_sets(self) -> NodeSetsInterface: + """Returns the NodeSets used by the circuit.""" diff --git a/src/blueetl/adapters/interfaces/node_sets.py b/src/blueetl/adapters/interfaces/node_sets.py new file mode 100644 index 0000000..159fbd9 --- /dev/null +++ b/src/blueetl/adapters/interfaces/node_sets.py @@ -0,0 +1,23 @@ +"""Interfaces for NodeSets.""" + +from abc import ABC, abstractmethod +from typing import Generic, TypeVar + +NodeSetsT = TypeVar("NodeSetsT") + + +class NodeSetsInterface(Generic[NodeSetsT], ABC): + """NodeSets Interface.""" + + def __init__(self, node_sets: NodeSetsT) -> None: + """Init the NodeSets interface with the specified NodeSetsT.""" + self._node_sets = node_sets + + @property + def instance(self) -> NodeSetsT: + """Return the wrapped instance.""" + return self._node_sets + + @abstractmethod + def update(self, other: "NodeSetsInterface") -> None: + """Update the wrapped node sets.""" diff --git a/src/blueetl/adapters/node_sets.py b/src/blueetl/adapters/node_sets.py new file mode 100644 index 0000000..71606da --- /dev/null +++ b/src/blueetl/adapters/node_sets.py @@ -0,0 +1,33 @@ +"""Adapters for NodeSets.""" + +from pathlib import Path +from typing import Optional + +from blueetl.adapters.base import BaseAdapter +from blueetl.adapters.interfaces.node_sets import NodeSetsInterface + + +class NodeSetsAdapter(BaseAdapter[NodeSetsInterface]): + """NodeSets Adapter.""" + + @classmethod + def from_file(cls, filepath: Optional[Path]) -> "NodeSetsAdapter": + """Load and return a new object from file.""" + # pylint: disable=import-outside-toplevel + from blueetl.adapters.impl.bluepysnap.node_sets import NodeSets, NodeSetsImpl + + if not filepath: + impl = NodeSetsImpl(NodeSets.from_dict({})) + else: + impl = NodeSetsImpl(NodeSets.from_file(str(filepath))) + return cls(impl) + + def update(self, other: "NodeSetsAdapter") -> None: + """Update the node sets.""" + # pylint: disable=protected-access + self._ensure_impl.update(other._ensure_impl) + + def __ior__(self, other: "NodeSetsAdapter") -> "NodeSetsAdapter": + """Support ``A |= B``.""" + self.update(other) + return self diff --git a/src/blueetl/adapters/simulation.py b/src/blueetl/adapters/simulation.py index 920563d..61dfa7b 100644 --- a/src/blueetl/adapters/simulation.py +++ b/src/blueetl/adapters/simulation.py @@ -16,17 +16,19 @@ class SimulationAdapter(BaseAdapter[SimulationInterface]): """Simulation Adapter.""" - def _load_impl(self, config: str) -> Optional[SimulationInterface]: - """Load and return the implementation object, or None if the config file doesn't exist.""" + @classmethod + def from_file(cls, filepath: Optional[Path]) -> "SimulationAdapter": + """Load and return a new object from file.""" # pylint: disable=import-outside-toplevel - if not config or not Path(config).exists(): - return None + if not filepath or not filepath.exists(): + return cls(None) SimulationImpl: type[SimulationInterface] - if config.endswith(".json"): - from blueetl.adapters.bluepysnap.simulation import Simulation, SimulationImpl + if filepath.suffix == ".json": + from blueetl.adapters.impl.bluepysnap.simulation import Simulation, SimulationImpl else: - from blueetl.adapters.bluepy.simulation import Simulation, SimulationImpl - return SimulationImpl(Simulation(config)) + from blueetl.adapters.impl.bluepy.simulation import Simulation, SimulationImpl + impl = SimulationImpl(Simulation(str(filepath))) + return cls(impl) def is_complete(self) -> bool: """Return True if the simulation is complete, False otherwise.""" @@ -35,7 +37,7 @@ def is_complete(self) -> bool: @property def circuit(self) -> CircuitAdapter: """Return the circuit used for the simulation.""" - return CircuitAdapter.from_impl(self._ensure_impl.circuit) + return CircuitAdapter(self._ensure_impl.circuit) @property def spikes(self) -> Mapping[Optional[str], PopulationSpikesReportInterface]: diff --git a/src/blueetl/config/analysis.py b/src/blueetl/config/analysis.py index 54471cb..d2de784 100644 --- a/src/blueetl/config/analysis.py +++ b/src/blueetl/config/analysis.py @@ -14,20 +14,30 @@ WindowConfig, ) from blueetl.constants import CHECKSUM_SEP -from blueetl.utils import checksum_json, dict_product +from blueetl.utils import checksum_json, dict_product, load_json from blueetl.validation import read_schema, validate_config L = logging.getLogger(__name__) def _resolve_paths(global_config: MultiAnalysisConfig, base_path: Path) -> None: - """Resolve any relative path.""" + """Resolve any relative path at the top level.""" base_path = base_path or Path() global_config.output = base_path / global_config.output global_config.simulation_campaign = base_path / global_config.simulation_campaign -def _resolve_trial_steps(global_config: MultiAnalysisConfig): +def _resolve_neuron_classes(global_config: MultiAnalysisConfig, base_path: Path): + """Resolve the relative paths in neuron_classes.""" + for config in global_config.analysis.values(): + for neuron_classes_config in config.extraction.neuron_classes.values(): + if path := neuron_classes_config.node_sets_file: + path = base_path / path + neuron_classes_config.node_sets_file = path + neuron_classes_config.node_sets_checksum = checksum_json(load_json(path)) + + +def _resolve_trial_steps(global_config: MultiAnalysisConfig, base_path: Path): """Set trial_steps_config.base_path to the same value as global_config.output. In this way, the custom function can use it as the base path to save any figure. @@ -35,6 +45,10 @@ def _resolve_trial_steps(global_config: MultiAnalysisConfig): for config in global_config.analysis.values(): for trial_steps_config in config.extraction.trial_steps.values(): trial_steps_config.base_path = str(global_config.output) + if path := trial_steps_config.node_sets_file: + path = base_path / path + trial_steps_config.node_sets_file = path + trial_steps_config.node_sets_checksum = checksum_json(load_json(path)) def _resolve_windows(global_config: MultiAnalysisConfig) -> None: @@ -155,7 +169,8 @@ def init_multi_analysis_configuration(global_config: dict, base_path: Path) -> M validate_config(global_config, schema=read_schema("analysis_config")) config = MultiAnalysisConfig(**global_config) _resolve_paths(config, base_path=base_path) - _resolve_trial_steps(config) + _resolve_neuron_classes(config, base_path=base_path) + _resolve_trial_steps(config, base_path=base_path) _resolve_windows(config) _resolve_analysis_configs(config) return config diff --git a/src/blueetl/config/analysis_model.py b/src/blueetl/config/analysis_model.py index 0c49503..1346599 100644 --- a/src/blueetl/config/analysis_model.py +++ b/src/blueetl/config/analysis_model.py @@ -89,6 +89,8 @@ class TrialStepsConfig(BaseModel): bounds: tuple[float, float] population: Optional[str] = None node_set: Optional[str] = None + node_sets_file: Optional[Path] = None + node_sets_checksum: Optional[str] = None # to invalidate the cache when the file changes limit: Optional[int] = None @model_validator(mode="after") @@ -105,6 +107,8 @@ class NeuronClassConfig(BaseModel): query: Union[dict[str, Any], list[dict[str, Any]]] = {} population: Optional[str] = None node_set: Optional[str] = None + node_sets_file: Optional[Path] = None + node_sets_checksum: Optional[str] = None # to invalidate the cache when the file changes limit: Optional[int] = None node_id: Optional[list[int]] = None @@ -121,7 +125,7 @@ class ExtractionConfig(BaseModel): @classmethod def propagate_global_values(cls, values): """Propagate global values to each dictionary in neuron_classes and trial_steps.""" - for key in ["population", "node_set", "limit"]: + for key in ["population", "node_set", "node_sets_file", "limit"]: if key in values: value = values.pop(key) for inner_key in ["neuron_classes", "trial_steps"]: diff --git a/src/blueetl/extract/neurons.py b/src/blueetl/extract/neurons.py index 9a2bebd..3c15590 100644 --- a/src/blueetl/extract/neurons.py +++ b/src/blueetl/extract/neurons.py @@ -1,12 +1,14 @@ """Neurons extractor.""" import logging +from pathlib import Path from typing import Optional import numpy as np import pandas as pd from blueetl.adapters.circuit import CircuitAdapter as Circuit +from blueetl.adapters.node_sets import NodeSetsAdapter as NodeSets from blueetl.config.analysis_model import NeuronClassConfig from blueetl.constants import CIRCUIT, CIRCUIT_ID, GID, NEURON_CLASS, NEURON_CLASS_INDEX from blueetl.extract.base import BaseExtractor @@ -15,8 +17,8 @@ L = logging.getLogger(__name__) -# cached node_ids for each (population, node_set) -CellsCache = dict[tuple[Optional[str], Optional[str]], pd.DataFrame] +# cached node_ids for each (population, node_set, node_sets_file) +CellsCache = dict[tuple[Optional[str], Optional[str], Optional[Path]], pd.DataFrame] def _get_property_names(neuron_classes: dict[str, NeuronClassConfig]) -> list[str]: @@ -34,6 +36,7 @@ def _load_cells( cells_cache: CellsCache, population: Optional[str], node_set: Optional[str], + node_sets_file: Optional[Path], ) -> pd.DataFrame: """Load and return the cells for the given population and node_set. @@ -42,10 +45,14 @@ def _load_cells( If node_set is None or empty string, all the cells of the population are loaded. """ node_set = node_set or None - key = (population, node_set) + key = (population, node_set, node_sets_file) if key not in cells_cache: - msg = f"Loading nodes using population={population}, node_set={node_set}" + msg = f"Loading nodes using {population=}, {node_set=}, {node_sets_file=}" with timed(L.info, msg): + if node_set and node_sets_file: + node_sets = NodeSets.from_file(circuit.node_sets_file) + node_sets |= NodeSets.from_file(node_sets_file) + node_set = node_sets.instance[node_set] _cells = circuit.nodes[population].get(group=node_set, properties=property_names) cells_cache[key] = _cells return cells_cache[key] @@ -65,6 +72,7 @@ def _filter_gids_by_neuron_class( cells_cache=cells_cache, population=config.population, node_set=config.node_set, + node_sets_file=config.node_sets_file, ) gids = cells.etl.q(config.query).index.to_numpy() if config.node_id: diff --git a/src/blueetl/extract/simulations.py b/src/blueetl/extract/simulations.py index d22cab8..a0e49ad 100644 --- a/src/blueetl/extract/simulations.py +++ b/src/blueetl/extract/simulations.py @@ -2,6 +2,7 @@ import logging from enum import Enum +from pathlib import Path from typing import Any, Optional, cast import pandas as pd @@ -59,7 +60,7 @@ def _build_record( ), "Simulation and Circuit must be both initialized, or both not initialized" circuit_hash = None status = SimulationStatus.MISSING - simulation = simulation or Simulation(simulation_path) + simulation = simulation or Simulation.from_file(Path(simulation_path)) if simulation.exists(): # consider the simulation only if it wasn't manually deleted status = SimulationStatus.INCOMPLETE diff --git a/src/blueetl/extract/windows.py b/src/blueetl/extract/windows.py index 2589c8f..3331cdf 100644 --- a/src/blueetl/extract/windows.py +++ b/src/blueetl/extract/windows.py @@ -1,12 +1,14 @@ """Windows extractor.""" import logging +from pathlib import Path from typing import Any, Optional, Union import numpy as np import pandas as pd from blueetl.adapters.circuit import CircuitAdapter as Circuit +from blueetl.adapters.node_sets import NodeSetsAdapter as NodeSets from blueetl.adapters.simulation import SimulationAdapter as Simulation from blueetl.config.analysis_model import TrialStepsConfig, WindowConfig from blueetl.constants import ( @@ -35,12 +37,17 @@ def _load_dynamic_gids( circuit: Circuit, population: Optional[str], node_set: Optional[str], + node_sets_file: Optional[Path], limit: Optional[int], ) -> np.ndarray: """Return the node ids to consider.""" - with timed(L.info, "Loading cells from circuit"): - cells_group = node_set or None - gids = circuit.nodes[population].ids(group=cells_group) + node_set = node_set or None + with timed(L.info, "Loading nodes from circuit for dynamic offset"): + if node_set and node_sets_file: + node_sets = NodeSets.from_file(circuit.node_sets_file) + node_sets |= NodeSets.from_file(node_sets_file) + node_set = node_sets.instance[node_set] + gids = circuit.nodes[population].ids(group=node_set) neuron_count = len(gids) if limit and neuron_count > limit: gids = np.random.choice(gids, size=limit, replace=False) @@ -81,6 +88,7 @@ def _calculate_dynamic_offset( circuit=circuit, population=trial_steps_config.population, node_set=trial_steps_config.node_set, + node_sets_file=trial_steps_config.node_sets_file, limit=trial_steps_config.limit, ) spikes_list = [] diff --git a/src/blueetl/schemas/analysis_config.yaml b/src/blueetl/schemas/analysis_config.yaml index aefeaa1..1846af7 100644 --- a/src/blueetl/schemas/analysis_config.yaml +++ b/src/blueetl/schemas/analysis_config.yaml @@ -140,8 +140,13 @@ $defs: - "null" node_set: title: NodeSet - description: | - Optional node_set used to filter the neurons. + description: Optional node_set used to filter the neurons. + type: + - string + - "null" + node_sets_file: + title: NodeSetsFile + description: Optional node_sets file used to filter the neurons. type: - string - "null" @@ -216,6 +221,12 @@ $defs: type: - string - "null" + node_sets_file: + title: NodeSetsFile + description: Optional node_sets file, specific to the current neuron class. + type: + - string + - "null" limit: title: Limit description: Optional limit to the number of neurons, specific to the current neuron class. @@ -330,6 +341,12 @@ $defs: type: - string - "null" + node_sets_file: + title: NodeSetsFile + description: Optional node_sets file used to filter the spikes, overriding the global value. + type: + - string + - "null" limit: title: Limit description: Optional limit to the number of neurons, overriding the global value. diff --git a/tests/functional/data/sonata/config/analysis_config_12.yaml b/tests/functional/data/sonata/config/analysis_config_12.yaml new file mode 100644 index 0000000..bf5668f --- /dev/null +++ b/tests/functional/data/sonata/config/analysis_config_12.yaml @@ -0,0 +1,34 @@ +--- +# configuration using custom node_sets_file +version: 3 +simulation_campaign: /gpfs/bbp.cscs.ch/project/proj12/NSE/blueetl/data/sim-campaign-sonata/a04addca-bda3-47d7-ad2d-c41187252a2b/config.json +output: analysis_output +analysis: + spikes: + extraction: + report: + type: spikes + neuron_classes: + Rt_INH: {query: {layer: [Rt]}, "node_set": Inhibitory} + Rt_INH_2: {"node_set": InhibitoryRt} + limit: 1000 + population: thalamus_neurons + node_set: null + node_sets_file: node_sets/node_sets_01.json + windows: + w1: {bounds: [20, 90], window_type: spontaneous} + w3: {bounds: [0, 25], initial_offset: 50, trial_steps_label: ts1} + trial_steps: + ts1: + function: blueetl.external.bnac.calculate_trial_step.onset_from_spikes + bounds: [-50, 25] + smoothing_width: 0.1 + histo_bins_per_ms: 5 + threshold_std_multiple: 4 + ms_post_offset: 1 + figures_path: "figures" + features: + - type: multi + groupby: [simulation_id, circuit_id, neuron_class, window] + function: blueetl.external.bnac.calculate_features.calculate_features_multi + params: {export_all_neurons: true} diff --git a/tests/functional/data/sonata/config/node_sets/node_sets_01.json b/tests/functional/data/sonata/config/node_sets/node_sets_01.json new file mode 100644 index 0000000..f47124e --- /dev/null +++ b/tests/functional/data/sonata/config/node_sets/node_sets_01.json @@ -0,0 +1,6 @@ +{ + "InhibitoryRt": { + "synapse_class": "INH", + "layer": "Rt" + } +} diff --git a/tests/functional/data/sonata/expected/analysis_12/spikes/features/by_gid.parquet b/tests/functional/data/sonata/expected/analysis_12/spikes/features/by_gid.parquet new file mode 100644 index 0000000000000000000000000000000000000000..9d6948fa32aa1db61d071ff9eba251f79dcc1a8d GIT binary patch literal 30885 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 literal 0 HcmV?d00001 diff --git a/tests/functional/data/sonata/expected/analysis_12/spikes/features/by_gid_and_trial.parquet b/tests/functional/data/sonata/expected/analysis_12/spikes/features/by_gid_and_trial.parquet new file mode 100644 index 0000000000000000000000000000000000000000..d334c556d9ea84e79e8878044572b797715824d1 GIT binary patch literal 37550 zcmeF4cUV+e*6z~r?sXYIAuPVcFn(YS?l zvJhKT@D|hjEvzj#bB>GWI8&YznMybw=m7&D1_Cey`al92m`ZvWabN#q{O4Z_&SH;| z#U+W#LYrbCwJ@+SXeN_1`17Gm^5+F-$tnNgE}mW&SLxx8HiZ{o5_7G2gv8%#$Mdb^4UxUEh_mB(sk@w$qKKU1u!xc|$4ZWG<3QG$N@;q%V?&w8je^v_>4y&EWN=oE`6=fsLFK z=O9<$kx0ZZlF1D#P=knfSBgY(TRGQ>9&?SpxMh%B`!mg@w|7smGMczenJCiJXS7zdn% zh{Hdm_``8XXLYhzr}DR0Y@iz%<+7Nz&8cY`7GQAp%$aBYY}L%TxVS(KE8u1RvJe)V z;N;}A-j8j$kei#^#*gjgB|=|6rsd`SNx`g6ABDF0vAVS=>#jCT;Kec3xcMkw)QJTFy6_^`A!cxi+e%Y{k%yx0$TIL${_!oZg)c!}zY z5BuRF>hTF=*Z6Ybu^$WI6a7Q|nDFGxnKKhKY}iP=ZFe7bo=+4$2eM_TNC;&4`b_D^ zJkFzJt_R!5#|zb7>^fg1cm}h^e1R%9n4MIkfl^-AuNM2jC*Fm-{g^soCG2d$I0r-2`_^^a zWBg$XCLBQ5%^rjU%Q^ZSO_G!!fHN)fL>}?qr*A1_#cV(iHFwcl_t#6K&dgkqo7=Y~v=ieBoAKwxt|j_1p#ftY@BOk5 z&p1&R)G1|bWChAJVCoBqcg}{X$cFKF=MZ}58&`P+?rSZWb`^~PftBRaUz9(`JNP$b ztDYjowP6XfV8XRF?94BGl7l^yk3>tYny>*aIx9NWu#z>u;U&#j$#2f6RKl*fqrnMX zSdKo7*lokAf5PoXYSx2l)3#;9rn2{EeJMoud9siCF%mCUfvyhLu*-@{LGWWKZW^JHH#_fEE}ZmZ=?Jrf8nzl$ zfAnRK-QtBTUuKHbes8u2k^MOUG2t!vXmo?X-iM8Ls}Ni~*>Q{v&X?I?6jl4MMX?g; z5O=n5wOgK~58IDm)eB(FX-q8%WJmD&Oh1g#JYgCQmn_wNFE)vWzb2T~^6}DF8n#_u z5ijvYNr|w^m)*h;{piPhVdBM5);BgwcpQMik|kt&qr6tI^h2`?g#rF(cA{`Gh>2n) zf>kg(1Yc|mWMODixrW8XdJ8v$$?nAYe(V~JtH)lfI23VM-I0~7q`o&|+tS_S5_|T{ ztx$EXC3_Zxr<>$#o4!lUv>u2VRE%wqNFLC&zC#&wP!J-C?pYs z=VB$Q#%v*l+-_%95+AD(Fr-TP7|9D)rY{%F88;x!N>YONMM;{mo9N2i)=aA}mo#Dt zv1*lI$3C~fAlcoXX@|utIrj1~f?KR%%@JfD{n=whiK>l13rvTZmZ6wYWkQ=EcwHj6 z1hV69m{2|0GB*d+cz@QV8{AS7#L95DjfQ3Nb5v4I2^YQCNky%&#fzQgGlcy?>`cB) zFbhUx$%IV0nIk;&#QemB2tQ_pEE+#%hcNf|XU{Qrxp=cF2<(p`n8|8XK7K5phQ}2z zW{L76f|(1iR#jlB(Sxf8^k(^pJ|i#YjLGzIFmqJMgiRs1$pj-mR?6212ZNcdTcN;d zupqn?TKZy=&k*+cGXu90VSt8tVDQBGvR?SpBN%Rw3X}ZcYo*XXjA{57X>bU08+}(M z31U5AO>zKxK%+fh!xqyj6Ab(1!puMnDXH+;2jfgB0j} z?U~jQvq+o;D~WYedz-Q~lc@)~vc#q|?O_V_$VzCz*cYUn8nV2-XxuOdP`C)LT+IfzT#2hTTK z$enR3SK;M6O}`Bb68hqXe!OB&3JGQrXZ%ks~{V;`&l<*F^7tRZc1K6|kdc-Y351tXY;1RnWyIY}^NECvq^8TX8~7%yG9-!Tn+cI9gM94W^%fy2!UjKnM&)3+Wge8ibo8i~0G zwGLu|$0)k+Ya#mCD9!RWO#6 zI1GhZN=&*Du@di#o!B-S!Q+*T(}jASE4IpKj4iGh|Bu67JZglleb#Pj)n52~a z`)#ywlm5%KJMou}dfRVG13K3#BUTsLvu5@UEcq!CIH3>3@^KTV52LZ12y14ZHcd}ru8bsj$QNA#+=o zo~hSbvq7_ShKEU$jxnWKoo0HPHtm}Dpkn7tuLdn#wc>`3>D~=ndS$f=-j(jtsC7`@ z=M39n#>?79qz^c<*kj((_5%(ieH7Wx&*?PuRF2C``vohyBwQ~_)@!|RmCJ-DmHTJ5 z{&7vuX|3MAdSj@OyUmnXI8KlR+j?}e+#7SL{CrFAMXdtXg`K=$=ikF|#P!lGOEw4P z_0F{R*t)b$=w^+b&&H7!KSk^qbY!mGD9i2r_HDR%beZ4P4ucL&s}ouJSLVi+&b8`3 zTa~+O*qIeBQw`d$*fZkN*1iWVx&F?hZxl`Rb2@vYYvSEf3u~_(tM-qtxv-`Gi5;sC zPJVgMF6$Dz-DAqTXSa?m54zK9dfn$bJ(;G8&CoZsanI4*?LFJHd9QR?@ICjr7VQTm zWOQ0zwxC(}DW4^aW4#x(^vUj=Y(C6)S-ZabCVVuH^Urk}ab-{P_*mOj$>W~f95Lh3 zfi=BSMeP$?7gXfC&l5RLX*?pt)c1~R*!4?~^EYeOzhCrZOxw%m;oAotUlIN^V#mNk zpZ+j*-d4GD=;;-9$(GNr?HPG>*XXD3+utl2_vpg*Q6DKZ+P+A@dGp`H{69tiKOQ*q zqAiNT{Vm>_=nk9}9sfFTrqisZ2>t8esm7`9Rq%J`Pi4Y)=TA8u*-CMi{MU1+vf{gQ zXd>-$Mrl|K?UM$D{&g-brCn}D2zy1RSsYNW&*Wvw;J?nPKVY8q@ns|EWR~TRl|UjC z2C_-m_{`KWDgM}m;4GW>^(>qCAI`E4{y5SmeLd1veLd2atG+wZ-o`wisbPb#&gl-e zN)-;aZnRl@8N?o7eaR1_Gaa3CjgYlZATw4-gai$?<~hQ=5UeXDbod>OMaRGoYeBpU zYk?6hIyZyZZEV9Mf>{P^?G%drkW#<_*%YbJK=wqzRPxa8kIO0?m&2*CShjxPE59C_ z)xtj$L1sf$L6`%dN1T@&Gtm?u}DVlCT^7hUN|kV1seoaMG|Wc%~?riD;G7hO&)tBF^v66`YXZ zhb7xVNC?D{AW!Iw)4T#(+h7)t)pmL)JBfWCo$1jBIMWwF;dmAL4@Y|?9qk)p$LSEn zMxvF^f^pE7{Lh1al0XN2MTVd|=NJ9YbH0FceuE`91A+m{tdBF9}>u}pnx9jpEaZeIKblb!PszXNf5|X)CgMw5kp!5y1+?&>Dy2~ z=r4U>5*?49IsK&*%%4qLtPX73f7A<*fBxTkK{ah^t2+Os8%(-|AvU`c`=%d!JG6h( z5#%H2Xw-;JBfX*8mia$N`I3(8|E4b#(AH+wF$G^$X4z2ZyLpI-KM?sziAa! zx^`HyVtU{1R?I(CA${AH@4#l01IjwFxFm9nGy5iLWa~Eaj{hcZ+@vFIoSgaZr-rH6 z7&0Ud8~$^&`EJOz*?irKd{aFPsNR?^P(LWYJ@fRShIIW)25BZI2Xr7g1b^H8U-gjb zWY>7d|Dub~o=M84=?>-{7}M!02Fza-Pdp~xY=HQkg zERzI95>ec01!yS0xxEzbha-;j1V|~m`A)-zx>X7~K?P?^Kb(qXLYju<#&V>qlw;&~ z(m-C3lDuMyocsMDH#m^WVuw@y?m#vzHb>|j!bZ>HBmrz1u%%pR8o^T1lqAx; z!k`>Fk^|WbOv5KNIO#LNIfNx(EyFSV z6%2(GWdREXtVD`D0g6;6vTX6idaD+!G;FS-KyV4gT3jhWrHaW<6iTs1pc_vH;an&a zq#F|=0e@61lK?kjO>K1XQkD3YkzwizGBY5~;m~`GM>isyU)TEJ{_7zTT2j(^$ju zaJLk}sF3TFFszkO!rbwygP{<_9KH%-oo1pfeC z7ea2RaKqjsm|em#CWO7@V>IW({y{iPDytqgyq24MlDpgh2(%vSZSkFI`NG}TAtm^=MuQKe~EB;Vw!WT*-hQq zOo=$IlGJf3mK;U2P;O09J}%}|IZ`pg@0AkFvR>w(8#N+8iFvV=Hm z=C6nsN?Nmh2=dvE>?MR*(g@uWNh6%?2GKZ}y~UrqVeE0Ng8;=a2a#VH%mQPvwFt%L z(SZcR8JPceYuNGFmpa`rO)Zda7@H%IY#6KlDjS{?NH#=)e^(7tFb6^YI)vGyDuB7g zYQNVI(->)p8!;Mp)66FobW)=HtCSc|QsM-&3}e9?6B}e3NQ(NTPdf#&1F_yhnTDN= zO(cc!A^vE6AuVczmH`-Rg+f*k>l~XWG!Dj;RHaiN@eZB(h>bPPQ#GVMnqnM7>i!icRRDTpLJk|g01og~S0lB6PvB*_@GsWccnt6BldBsP(PjfUl?WeCtG zyI`iU_hlc`l)@@Me5!$-jTsn|mr)=pD-^Ur82xHt5VUU0VN&z~n!O`fC%?ppFd1qw z3B`OP5srnjJVl8xD2!b}$~l~U!JjMPm=|-tDxgK*E1(PXUrIc5yIiXPmVXzv1hHLc z=J;SX4zuK@P{^TGq>@pzp zmtz|KLZA8y1;21?MzM{CZmReP-L$P4zFF)Khj5xGkZ`Ji;7Y=2h5+I8Pr=jxsjSd% zx@j3^zGLBRks?L~`Sb#5t9>=>J>p3d3K`XbWYl+xq{R6_|0bhOCs}V3gvQv|a)~J` zgEPw<*_yOO)%T+6tQbiP%qKEcoa!%awV7F^iGx$y?=;qYrX-1#_M*^O&5Mi+u;cn$ zV#SNDxN3*vac(mPtrdZ|8)7ScrXsa9>U*^{Kjy32nx>UlV1E!J!^kDkHLar6D&0r3 z_6*wVN98rwJe`1Q$r5Q{vttS7oQVX&6GDp=*)+A9A@!Xi3#-LYWD}vtX4RN!1W7Aq zsb3^u_ASk?l`E}zYVOJfdwbU5h*WUGwj2Wln^va(Qm`itSj{<|#nMg!aDfB@F#4JF zU;PX$O<&NmO>~2FbRLCSLq~Kn+@j*z@vv4+WuIJW0QpRP|RpQlW4pe zg_roVFK(`aQ7}dTCWtWB3d2yRBtvBOVNlHldkuCJ^&}|I!n_zq!&fN?p_tFKLKeP} zK$z78G0)g?p-l)IgE}3;S!P<40O>gyBYcD}zHU3HGy(rAMBl;`786J-l;9T1f@5pG z%F^<$va~nJ($9~}BtDQNGKB0v)(n64XpmnbObEe%FBhhVF;g>%x$uk>>z6`_FWZ== z5v+o6nywX&`5|kiV5VX1@v{j}7@-mN1+fd*T)YX!AkGjThe3FD5J=lTQX@dxZjM<0 z=nZXKCOF{-0g4!58$NlP7nnfsjzwKPd~m>)3w`MOb(9bh#CoAK5;bfO=837n*qIjy zHsL6)A!$4oUb+GS1VMQ*gn7g2)8zRW(#tE+=E@Mr<`pEHr(h=X^kdslpno`K;V7XU zCP2&>KarR&Coz2t`p<#B0OgRT9*CRwL!m3w3Xs+xq93g2>r}ju5QK8x!cW1B;gAiv z-BwX9kj#z(0|HrRH0*9L8lCY~ZI36lJsER(g+JS-h!PfR*mcai+d|n2v_U7qYe|AX zfO4u(ECn*15^q)_Au0Z&nbcH(2p z1JV%xN^CbAq_ev~%uiE8%&!VGD>p`o4rUG}(%i1B7|NIyM!oL)~Wv{>o+zg3|Wt%w| zOI`5Fay}m495_BlrTZ8_KRI~j8UsbXCBF5%lo(>mBo`R%h~lNsy3z-N7~FKoR|=c# z*p9SXsS|dG&&<3{gs-0ql%HL{Y5gS%nV_}9L{=_fZ+rWp1oI$QtI%NcRJE~I(u7s$ z7oeHN<|QVQwpc2ogwRe*qc6j@c42;z6rXMJsX;5~u@dVeIa0aqi-W8YSEh)PmN_#n z0&S%a4@cxeb5>noS!u%MZPensf(@-!_Skej%)?g*t$rR}5{DVDkt1UdmGntMktA$$ z!ncSRxXc-$`OJ~UJj}p{3)Nkvz}m4@<}reR6~Qq-gKK1+(Fo=RDk=mkHH(0i7x9%M z|D{y-kwQ^Bw9d6^1${?r{^vJxC7VqKK#0J%iw(^zb~IC2)XwDp`lVV4>0sCm{L9CS zf4_}vi_P9PG{ld`xGQ4L)JSBR-hi2N7T4}NNn-TAbgaE=&KWAwWHL@q&z#fWF5hIz z=^05mef_&iuHgtZuND1TC2h><8_BIXJ>Hzt)6X{LIvUFJOYPsP#fJM^loY#)J9@3k z(W`V2p$6XdvnkiYw6jQZf#1+4Q9MJek?}U1p5aYX&ajcx7FFv>%{kMiHc~y)T65m0 zq=f-~K4x^B$qn$Da4CIFF_Q4CE!S|Cox{=>v~DdCoSCb)`Bo&2Hx>g8s7R=4U?>mtKb-lblS#>Kn3%i-Z4 z>QgUAt~2FL?%WwTu|7PyZt_G+-ds;{IW;;w{9Wq6y87t4=;&~&?dsLZ;kP0q!*A6` z4h)}d${W>(4@`-!|L{(#XY{WAYW=&(^#g76^usUTsh8^MU5=FMHL1ILwLUtvesUyg zsf)Zk@LhOaePnp4Dc>M+;>4?W-lc}uUyY1RxhmC@@>X~1Bk5gNQ&XZRzx$ehVmR!* zWXfCA*9}am`w*E@mlB>54WlpD55!CK>r%t((5}n%sqawD<;c3L(e-Foea|q zro2^ZDr!x+eDy>4#DU=-&^~ekiov?esqZd_qgb@AQ26ADAK;7dbhIoreBzzx4=MHL zdM5AcqtUhx^^@xwQ`_q5Em1J~U25t;)Khn7V0~nLU3B=Z@aW6tdKXOjhPY0-6^Uk| z;qRjB2bxpUKGdUa@9sptOTCpk`Ca%O)E}7=ULU#8h&N8<#PSkO+&HF$m$b?&5ec2E zO7x`e@+0ELVKGPaWkd6h7&Mzub;MAfEQ~~%!QP$pEsVork6T2!YuzpT zB^>c+GHBXeJy~o;bVQrM8QT`GsJKkGa!@#2E6ceITbs_$oonyIT-%?#}JWZSX8?k|ep z@I79YJ2mL}u5x^XULR|AH*l@1eba#PMxCW@`c@O9y^Y%Jks1hH9#r&^dbB$4-Z*^d zc@NpJ4d*?ZO$?ju)grC&g(kf+I-C30q#-}&;;ivj9~ z6AW#-JueLn>QyHW)ATm=2n+UXJ}WH5xBc<3(7>Mh;b9>G78c>*eJ6&uj2gD_a>Rg% z4=+c?%(A-@HFVjqE4IV)mu2@Gwexaz|FK6bmJb+z{+-XjNw;Q24w~}(onds!$9GL) z(u|yCv9qk4&%|cfZK@nRze{!HkSvdVSwoiuM_3tW58PxPmpfvg)$rBH@2%qVGo7tR zh~mRmjM$u?y<+4~JFl!5_45(Sl?mn3oSTl`du>wFG5cLM+!#|l`qIj=hxsO}5|1^E zT=l~V#qw3-PItPxYW%s_2UZiJ&--+3KCxxk@aBE4#x7qy>BbLNSNB#Ye`ua$Ry3in zfmwR%HKXo_yAMjP?lj(J%Hu7+*i7xU_mR!;-;Xp|JMGQ+$h9dS?k-0my3=_x7yqOJp%ktCp0=;8rSq{pGoozj)Ja&%Fbjjch+lAhPGws)8 ztkbsHT4Xz~{efEB`JGOwTQ2BwJ*nkFmnY?S7P`Eyyg#$o7s=49-ex{S7kP@3T6ub# zMK8fdke}nV4UGZ^e`vKd*w@8wS$MxT50^EF$z<967VoxOK5)uMyPVi{^6H$S z*(0iRKjh`c`H$RjGHzv;qL22gI+wN?&W&HwXZY$#zT;c3Y5QPHYm=#i&+DyCc~SLf zt=3?L)wmgEzVZ1DeTLTLXV`D8Su*eFvH0~1dz~6MIm_I7%Z4Q({YEs(-j Q||lq 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 aw;#!V>mOc?UyB~{PPp=;goHwYjQ78+Y@>Go literal 0 HcmV?d00001 diff --git a/tests/functional/data/sonata/expected/analysis_12/spikes/features/by_neuron_class.parquet b/tests/functional/data/sonata/expected/analysis_12/spikes/features/by_neuron_class.parquet new file mode 100644 index 0000000000000000000000000000000000000000..fd018b963deea02f6c64b2a52c91113676b7184a GIT binary patch literal 11302 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 literal 0 HcmV?d00001 diff --git a/tests/functional/data/sonata/expected/analysis_12/spikes/features/by_neuron_class_and_trial.parquet b/tests/functional/data/sonata/expected/analysis_12/spikes/features/by_neuron_class_and_trial.parquet new file mode 100644 index 0000000000000000000000000000000000000000..116afa802fe448d85966922a59db56b7b49b2b46 GIT binary patch literal 6189 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 T;jd~Jey9(gr>K4SN5Q`ViX`77 literal 0 HcmV?d00001 diff --git a/tests/functional/data/sonata/expected/analysis_12/spikes/features/histograms.parquet b/tests/functional/data/sonata/expected/analysis_12/spikes/features/histograms.parquet new file mode 100644 index 0000000000000000000000000000000000000000..7855f7ff53d57a13d552a55b383813edaabc6b9c GIT binary patch literal 12767 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 literal 0 HcmV?d00001 diff --git a/tests/functional/data/sonata/expected/analysis_12/spikes/repo/neurons.parquet b/tests/functional/data/sonata/expected/analysis_12/spikes/repo/neurons.parquet new file mode 100644 index 0000000000000000000000000000000000000000..8d9e11bb0102145ad08eb05cecaba73a04a8454c GIT binary patch literal 20175 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 I#ILgZUjg+xF8}}l literal 0 HcmV?d00001 diff --git a/tests/functional/data/sonata/expected/analysis_12/spikes/repo/report.parquet b/tests/functional/data/sonata/expected/analysis_12/spikes/repo/report.parquet new file mode 100644 index 0000000000000000000000000000000000000000..0cd2bc322250ba8aea91b724d5afa912f13a0e10 GIT binary patch literal 13364 zcmch83v^V~)&IT8B-|ut!URZ|K!70$!+;Z*JV`7jd-9lf=9xDXGI=LV9wY%01f1}& zV9~}}DJam48VVLIic(bSKdHjP7HpKNP$`XCQ55^(vuY{*es?B;@UUzB*Q(zvnRCxM z`|SPO`|R^O`%ad2tur}V65Y2ndUuf}`iqEYZUV;*&9AI&N=;2Ym`4S#?ZBsiK3%|Z z+yNbZ$qVWM9rf(4tE(%~(++eG=_w@S_GnJ#+U?w*fJZgp(IoofB78yHeCLeESZ<^^0pQ*WErl?SOciBwFEdG*U0SbX> zEPfPZ0dNKLSf~*Tdo3_X?7RgAS}muIErr)FvdD@kF(Wu+*rsJ7A*@X2GPExbau&q|+W z%^ZRVbjvPd-R=BzcLT0OjdLeVzK^jE-H3tpxo7u+>YC`X#gEA4oPaX6onqVjGRZMuKAc9GuZyat)AkTpI5?d$Jy5k{2A#Fl1FC}dU%)iYB#Rw=Wjw`A zO9PU4QbTn|JgvcooR6m?8y*6H1UiAy@&r19{XLi{_B@{_(%Fxof>Y9o-yoZ}Drw=z z=z3?so z+43Z^xI|F%C{DpEgn6_zM2>GjlAWJN?0H^enNOBaU~FJMt^5nzxjUK8UC`573{EuAYauGZKvG|W z>^TD&LwYu&_a&Aojbuc#&q%X;ne4ogj{3f4oQaafoG{TzGzt?%`82HB1V$p}G0_Dq z=rw`ZV9rEaLJWe*Pj3fIpj={yOf(0!UoruZoDG?17ySLkL~me_nZ-+j#h7Ujb6d=Q z3|q~(W#sIT+0UG>m_hLdc1ompIs3}&_w^eyjLTT4g`$04#zf+kF{K5|6|CF>dmvs5 zh4pU+d!NDBA&Z|(pR-`Kj2*K8Y$XdW1p8~4rI2>{PP58F*pV?$Awndu{e>V}ft@Y% zyKte9=JtfOY=i*{NrN3N^D@noL6Xyi==fxDd3FrH(Vljl8sU^hO z87Shp9B#4J?HS17exk)u{2+&WsS{!!<#4@X|Kl7^yk-8L8 zFrK)F5WgVe9_9rhz|>(h&@h;(Mw7k^M>m`RPa#B*7qIH0h>nsl0UbL_MFTRM2ofLp zW|L?*V=z;Qp-M=}X#kv3H--8ngCa94GNETGrVK(Haf8HCC#e$dNe)&xDi1fTCp7vw(3>p|tpo^p5szg$66bUtvJaCT9 z5?9M&XHlGE@PIIjwqjMe5{m_PMu`w2!)KEPo(#;Uvmq*`qJ^6vIEU82wq*_^6pWio zM}YA*x;)sx4kq~<@a>VK1~#A0@ADltXsAR6qgfhyaV6N2G;|D_D>ZZv=YH{Wk8!? zIha8)`>~^^GHG8hg0(o8?j7XV#RC6vJ)xt2MV2u(tn;(qMIHHII!q7764(|!3Rr>d z(^K=HLVZe4KCw)GQ|~W1vkWLJ3&}?yKE%hr(g*tO804)(iAB2OsLLe zti(hG&{k)nwZ1OaZKC(V0bUd6LeAba`IUv4Nviz^Q^ zN;8PIi|sU179u@lMvaa_*$kS_WOFRwbz&9^3eU}~!-6@19klp4|Ez_k`7+rz7HAF5pE5K_Do*aFmUqxha)#@XqTZ$K@vQ?FQ$ zPPfxBA1ZvizltokQ)th2w%88dP_QL-@P=INw$pmCWIbr7=i%`&ku(K%#qO^wB0*H3 ze3^sN;M^n!Wn*rkgJOCHSfhiU#$bm7xuIgc4*CMvo^+syl(QEc^iwng4iHBsd)YzT z0r<3oGO*x+gY3}aaDrYnY?0GnximPzq+nnttp>DjzD;RyVm>R&de=7z*XwtgCV%%Ef|GpgV!^dW32|3(Z?G6Sq>(Ux!ujFv2*W*1X zhhsC{>`ac=895N4{xrw?d>^>!A35G46t3UoaGRSE`fqc*M}QB7=*!U05#8o%5IBGN z8AJm=^|i4O8aM{`1oDdSMxZ#iZ#zR3`uoVX2!?|54~P;d?n>l=a3M-usG3NX+fhkc zCgP@0$h?z?JHe|OqRE?rnzSyOD*ug=?I3E=TCp~b!5TT}uP;Qdjv?<3u?C$?-c(+* zIhKY`ie+mYdF#d66QyaISk_KM6vV1`260E_YzAq1d}W|N+?Xf&_WZC(nciW|&V)#$#v8L5;bNZ@3cAZ6JCS$s3Z;SvR)l`IQET!9R5mGd|i$)`sEfH!IBEL!$`5H{SFzyt_C(_1m&ViG=jWT?5q)Gn~Gg9 zLL;`)_ZFGMMr z$rfAv6=`g26`wp;J8w|;r#RmU|nB4|$SFjQrIR<5{+D4n; zQJoDS@X}zzFAbG?SebI?P+N;c7fTu1rl!23xi(>UlESiIALS7RC5?f}Vw-5g>O z!hQud?KcSGo4t&E?jS-FpFkK?Cb_((l2PeNGBT<$B6Vry+9M3BFY@<^oR( zOznbXCeylbNEEEWg)=H=8(sK)0G_$XfaQMxZ=&~}i{eEFDx#kvc1p3p6PUFKoG7q$ zMY!hiDpUk&En^pophd=}7YAG@hR+7JNMuHUE~XE8VUigHxj&@mesExTWG|wY!(EJ2 zMX*ab-WQ<#pE=zB;8pr^4%dHx{VRtn@f}zHJBRza5hn~3xD@FTOan)Kdj$!JA|d4c z26b4V)*DB-EmQ)U{^J5zvHv(;&ABT06-o3U7-2klkMjm$JPqT(d1a6oLiNY>_o3{o z>^~q(qCOnFNuaP)-U?eA$-M@IxYlP9w#uzJau9)b;_kmlQ)?r(ujOaZxdv5O+B$yjPC2Jz07ig$25fF?+KY)+1nF3wQZW#WvUft`sJGQ>>{=26%?H|-lZ8^g0leBj=Ux7$wLyWp?hiT7o`*L!d<^!@D#zsvgYu}5FM{o*t4 zz50VoFDLwQ;TO-?e{#n^h9CGz_CGJZWXrj7IoM7?(tNu%becqx8#d<|S)MfO3t4_t z(#+TLCl%McR$#CE+5I|)=Vga}`dW$85PxsJ)2Q6F(P^6d#LG_eyx&S(mXwqEuELDJ zY;;*~|Kw$tE&E@RB73g1pva+{_Qp7;an7@oT!mR*{>f!8nDs_cQE}}X#bveq(TR(e zzwuz{(w^vTW!3jTYb>wZwW*|{spsjKMa`SueREOU(OGXT?tG*6tsi#1-~ZN3bX@^t0K=(^MH&9nBLuG*S@{&aP3f$~hv&(d8xYJcwDwxjOh<;QkNc6FaWQ{UGU zGbQVhdvz6!dv@Jj(e(He+k%!o@ydHsB7Xfw@FLIC&kg_1^X*W{!_B{WXWLZm^Pe1B z)EfHD`L|mKLS~s%wX@@o+R3{*PdQ3=t$54*^Ia?7 zUjEiJ^SQO-mUN|VSunl({WF~pbbsLc`R>&pKlRq`JO5{J^3pY*oz%}*`{kMXzpnlI z{a^ld-Q_Po`0HIa=W^LcS zX--|kBaIs6j@owneLL#!@hWdmcx>PKhg1VEDIfm)nG5G1ZaWqh%r1YC@WIY2L*>8R zwesAS*}JN$IwY9606_2KqEH+Jt{7v8k@k^4+d`}XXx+K<+kd?K@O(~rr7kWyJT{#RmBq>>osG@4(Lpiiq%nVA z9`jY5@_3O_`2V4*ST+6>#6<^j_>!UFIP`~P(H8|v^gTiTKMm~_gfF_d5MFXmcgQ$_ z+t3jvNcOM9SK6vDfyDOKO+=zaA_|UyEtGv`kXaj^2cI=ozI4xFV@sGV`u5IX6Kn<9>>jKazSSiQQPa zP@2hgOWU}eXl>F4N$&D66&DPtGg1S0Ub>x&lX^LGl7bsAE#qcOHC&psl8c1>2t12p z)zG#d2>+PGaVk8=Cdnnk-muM_PTD1zeP@`AdlI9|p{ZZmz`c>A;f_K>lC+nbB^~D8 z!`xo<+mj9hiXi!0TUaG$g78WRyZ~Hx0m>99aUUd=aZ{5F+-uSgxlU;pOc@~8$GN03 z3EvUcfWZN7sq`?Xha-Ei=rLgW6q=6%{~1{Ropdwz3oJMb#UBH=8FFtx?gczoLD|M6 z8TTa=&%pB^&>n!XDFE=J!o%Ey(4|i5<37XOVd#+qQKUg1_lnd{)1)jtk^yX2_eH(hIcjN*U?&A2MU`a^m zIF1*>xbO%jjSL#k$)flP6N4v3$4nk4kDW3#Zrb!2p&^POZf1M}ADlQ#Ia@Vxju162 zB4{o*VQTPgikKN{K55#NiSxpPRP(u5O>#==IAvOJdd7lGK9QR+Gb$@6;r5B#jJbk( zT5!yibp8ii^1?e5sazU2s`W|<7gciU$Jsfg&CSWn&M(mE4MwdgoXnQOY^%-gDA4CP zb*`e~+>+9=spS=mXt6QxhuS6P>_WrRN}b7_Q&nB0uPH6At#dlEjXLer5?lR}0&{so zV^et1BDZx}vB6WClil3Xs?+M*OpfIxROu`=)@0k3=(I&m1@6}N(h{9%nWL>mn_VO- zzEaLjE5Fpk^OJ%?#z}%Fay$!_C5Cey8eV=|(A41ZVUe>!Vx*ljxY^UyE5=O@oxmwp zPD6yE<*T}axQVe*-ASF?)ZhqVMg~9bj@62oDYFtYIn~n1DX|kLgx|>pubIoQ?z8%k)zl&Car>99=0f{4m^IU(%eDt@Ahx+V-Li#@y`k%Dg61 z>#`i3b4hloZKJyd(akPuD>3Ls%w1{aN?MUv$}4xqMN3BbGq}F7CP1I(XT%IlRGwI_ ze6a!id2>pBN21=6KjqPJ7Lisba2!*MoVqI6|DB3MLDBhRjzk8h3R;ya4_cKBDaD$R zgy+3MTp!Q(264N2{xZ)U;rZP`-19uI{AyiXG#kMlx~j3Irgc>S{sBJbFs9ckUtAkE zM(Hi#3DL|F9pH8egI$sPW1^>wfhCuS!+~!&KBkA^2-PUeb5c(EP!As$9W(+dq_e%z z?Fk^-#>edDm5+IO<>kBL{)56>LX<^M8jYY3Ct?x*%m|oq(TeL}io@4nUh82vGHn#* ztD#))I9~bp4P%f>I~to;c-)kjggEhRmvAudlW9L<%fzS?Z;+1^ol;b87`Fv7P5=tt9(wbZU?N0h5Q z?v9Q?Y7Fo($9d(k&Ajs4AMs+k-mLppA&%Xe&dEbNsvByX-O-U;boi{GKmmlB1&`uc z=As#%Moz{o&LZYcbP$FDnK?^g**{ShH9B~G65KF3ChB8mMD^EsG-3o5SZrMEn(wcNxjeMZ-BRQ3kUt)FXFM*2 z+ODP5t)3OlEgkW*#d1ccQEDKex8w zF>^tdzf&uAlGpwaoq)!re)Y8@im%sG*I4VRS$h3g0NB#b?l#e2{EC*w>eiY7h84|i z-B%}CtD0)7J4I~GwVm!7cc&ZTcgEMYK)kV~UR+yTq)W<-UpofO^(f-ScOg;3bvUk$ z-hiae)9UWbNb^VIIwWHz#y}B6wN?#8Bksk6Y;LwSGI)cj#+F+ex;p%y%mnWEn=M@% zzrk3wyR){wwY{<0?YY@En9LMM$5@0%E1H*%uJUhe3M5+FYiiqT#jL4wdpc^@Ugx|2 zmcJR()!`c;!D@2GEx`na$Dp|>%-3DhH=Dd^`oC!U+O2!D-RtN6i$=#@mN%QdarOqo zqr3Ru-h2P?Z?AQCwzm6ErKho~-QC_T9?G_EcYAy5DiP|++V&2VQ{rfH`aI3NY_SFg2cwdLAeht{aowg$!> z4sF4R1m+9H$6W2`e11Ki#XdfMDgS()R+MvS(L#?_Y)6Geo7!r9nz)l`+zm0VStzS`K-T3?#l(dbU`tf^~M&fro7Cg5yuKg`cYzGRhuzAU-MlBaDfPs!@2 zDNSBcCfd-Kq*as^LO!<&cy89;T+voh+O@1KCB31lv{;l|QB&I0A?g|Rr!3W(Uac4B zw2d{T>21)zY!ol-tg0=^($tpa`f(eZvl`vaB~3MXet$YE%A5^|iKg5IUkx3N6{YEj zg%fe9_UkJtaq2Ba^^J8mV6X0mysmSBv3V31Z0kLpBX*nJr6nB|x~!}I7+atd_`eHx zMRV86@-jz?HGm5~-iW)RqbfzGu{JsUI;u)bH07nvh8lgr)tJ<|R@IM8WlmStj~W(X8vJPASQ{DV{CPZg;7!BcT7ryu|(+ zOl_msZsh}FpeZaW|4-Pvtc|zGOMQBBmA)&>l9zj3zE!7qI&l_|1D=&to~-ZQ>lSNU zRar>~_Nc8YCB4pF8pxsl3ykF{p6>G0lC~P%f56vlZL4m!CcDd=|EJihnoE+ao3p?} zR>a1-`n&e^mU1n*4dBj(>g2K9V#QvRv{a=c4^l?+1bbJOn?9DK#5}Nl2mgRqJAvn_ zo&Yy$yG7oB-enbKCXb(&^%V_O2CL^PFQuA1*x!}ajR793(t8@Kn|0l_f%CVjwp7;* zUsL@)wp4hk#hge<^K)EbQSpkxytFQ33(jqx=^7t2mvke?R#u5R^Nih=CgX}imnjQZ zhe5ngI>oz3ze>DHs&VaTQ^Y~jDtzTG6CVTjQxk5zO!NwLI&rW-oDW=bfqAhTxZiG! zvwucguf;mtXa0L_ROy(dqEvqQ{FU?P`{f<=VpAwywaNAc^XH4^97doU0WAU@`QTm! z{_AoN`twKdYI6hk?^XPH{=WZGy?Ql6fd>C_SG!A-aYl6-cd3|99>4!CJ5>4IE$|z4 zjP^(H_%G!#cyRv%WPoGvq^%v5*9O+Zez_L$$gLOSze)sAJQ8F`kHk0MZ_povF+uX1 zs`1~qS*Z|4JpCe2G1_XitFPJf*AG~ac@b2;Kc0BV)i&T;+;4ow=+77X6@I_#wdi1d zb)fzbC&V4Z$B%amehfLTDJjUuJd_3kCO!r}qWxUNHQ9p)_~G~8?+^OejXUn(>B)1a`S)O3j9`L$Nw+%zk$day8r+H literal 0 HcmV?d00001 diff --git a/tests/functional/data/sonata/expected/analysis_12/spikes/repo/simulations.parquet b/tests/functional/data/sonata/expected/analysis_12/spikes/repo/simulations.parquet new file mode 100644 index 0000000000000000000000000000000000000000..50e4e5b1b62970e56319b1a1d53b8ceadba2caad GIT binary patch literal 4506 zcmeHLO>7!h5MBszNa`rW$s#OqK&U}E1jDYu4pDnpu)#|)3D{wQwzA&-0E>5*ExQ=w zC{m>ya*3)OddQ)wdakM-a_F zODs(Q$|=UyvSRp-6sKiKM@dkQkUZ)V<``R04` zW*0x;$_(wHL-gAPxKS$<<$LGVr3)1P{C3^B(-gfz`|p6=M=#M!!GL#U33x|y{nYIR zT5xhXO;Ynlx!CW_{oWJ%VV)QXwiPFs=ei{4)qhK&{Xwqv5!Qxsc^ z$;d`AOVy(i(oLi`ji}Wzunxr&NR}lO6=k#@O>D}WQ6#f+R7x<+-Ay*hB36vW@zS1# zY_($?NFAf0HrLwV;K?6Psc)WA6yy4i>Sdo|#4~hmP`97BPK8O}{--#ZQ*Z|UR}`nQ zHCHmB946-n(%J=H!KB7E-hzi>rQ<=m)eYbEk#P#*#Nzj? z23HH`+d(654%m~)+1eQ!>$jAzOzVs-p8nSW> zy+!Nkh84aSdjC=Q5OgpKcahOl!goSp1L+DVjGm@}WZ9@|gBT~{jtN7S3>7?N9)Ade z zn64U4y!PRKHhMSwcnZuciZFgh!5(ICjJ0z}RKwm#5GH1DjMY;xFiDT^0GeTH;i=Y% zpKJ?aPF^iW6XwGIJtEFRpf1XJ65lb;MTabzjfKfO>a&ucwB*e%Ouz0YfKonDm)B5Wn|Y_Y@-6pT)BK-xaT|x z0?(FX_>sLa%8v?FT~DsT%Ut= za3@aSP8Pwn2H$kQCL|AQ)m<1SV10U%*R@_fS4mXa;*q#hu|;E7y|1Q4WsvV8n9H|2 zP2R}2L|xW+wcq6RWJ?qt;yFE8IJOEIPLj_VKQBPqjg@LRmaXYwavQF%JbsV!-6BrH86^)7|W^lZ^Sr?8EtL%cfxh!DMo>bS%Nhyah zajGmNP4ItM-y^XBBhyzZskl;26TiHkQjuP1%Nb&~>(z1#e8g*efOp4I>q4?EvSm}$ zCE~AADd!6NO|>zHT^hjrtPT8)Z~+f&MXNi0btF`*dNwuAhc}8^T_~x9p9Q#q>v*y4 z*dcdY+R+;Fj@E}fHh@D@G)io75$0Ty5p&0>LI&^UG#1T<{b|WL?MT#8{aUr$arSSX zPmKGNxkeiH9rl));?Jn`kWf6T1K&881z3-2x~bLHhq(xqcugn+j_q;HH1_&UsIO^m zOtD(A0HZ0g$p#woRM$04%#|CWp0$TOoFIXLe6w*Rq;UNllAIRdgu0fuw%K9LGGaBI zoURkh_wIhp$>FbH)QhI17a3G7pM|fDs`<7oXz_E!R*BKwvSsI=k&AO41XM#-G@bY@x(N4gj{|k zZ)b>{!AjC2$$;Z9hV*OukB#k7d~Tb>KN-oTheXa34`fJ21o}>KrsgDEQzTvq_fH4~ z5J?}cLD%GN}^ literal 0 HcmV?d00001 diff --git a/tests/functional/data/sonata/expected/analysis_12/spikes/repo/windows.parquet b/tests/functional/data/sonata/expected/analysis_12/spikes/repo/windows.parquet new file mode 100644 index 0000000000000000000000000000000000000000..20d9e80d08c90f84ee7abe490a9d327f2c9e5d1c GIT binary patch literal 7483 zcmcgx-D?}!6(1>yqjj9cyUA$f#ScbohR`~(J}hsvY{4Vh`l>8PN}{h)%zQ~2d1j<) zMv|2f$U`Zm^q~)ZD5Vdjl#+-10ZS>N4}IuEyOdH&*{44AArGa0L0Njvoezzqk=dno zF7eFVbM8IocRud9$Ie5x5~Z)w>-6m+yr}mnDzHhx@WBsF(eKm2Z8`w#6?$bebQP(g ztH=*hcU?0eytCzpXTayXvc|mhZPNaY1hulh5xS|%>QLbfxu>1TBJB?bf3}wT{ z_Y)sLWWMw>fA<5zsR*=rI-@)hhUiV(k!#zCdK@Ls_`EassJs4gJmeoo-N>%2GQV2c zq}MFxt&C+&?2U=nUigAP^f7%i1c*NlP8DFlq^qR+20zq$Uk zT@<0Wi;z#4Yaxod>HGB0e}Yjg?wjwzKDg>O9dhMez+Mpwp#NdKfLM4?fwTINnTP%> z-=~=0-q@s9ZD00UEnPAQ{4adLFMQ0)Rpy`XY=%7KERpW%>n-{tJL2n~5^&V+zWMgZ zPs@Pa{aI7&eN!q>oP33>`25|eCD)A;-QWhMDSz|@|Kej_-(Ry%vJ+aEUd8ACifj>GgOe^HsxRMO-)%rQ+j<86w{KwtusGew=J0wvqZRO zP)I#93x!$F(26A%0LQk#%qv`rV@idIV*wP8H@D|3kW33c^96tJWBzfAd3npWU`C8% z!3^~)$D+Is`(P}OMcFeJZYpedEP=H8?*~&$0^)E$>a;5w{|+**g0?L$+=3iiW~kn_ zF!VR*SmuuWZ@)U4ja&=t4(QIr=u-Id}YX*Vdh_;;UQh?;%uVx-JYCXAL-3 z!M;Nk@fIbE&8q2Iw~Fh`Vnoq6vCp$PvyOyoBK6IQ6CjF4U2e-Qg~7na=Ekc_x_>X{ z&iw!HJLaPAsLQ`L-av)_0zTPk{zs;Q^<$rSSGvSmRU)2-;9lU`B3vm`2EVuMeNQ z(O^_5GYO>H1O$0-=KXDj6zAFg^B2y)U z6FGx}<VXcY#XH3u#f|ls>0S;0&qV8^{8uc-r{@sV+0YzM*G_N&NbA0c$sx}{a=Z(gTs~WuUQRYd#Xv`Mvx> zSTcE@ox?^gs^D5)tLl6j^)>s=+SzG6=Ak#6 z0qmKl^>`&AWDyf9i?u`_*i}I z$Vte%s5`bL@O@R|;|0ipjBe#?l&_}}?i@j%9xsVM`WViFBgf25c7pK%xu?x~L4mmL z<+IHWpDQV?dPK*(;nj>`#UvuAN{3$>`^to?S=c;nP=?~cVeCBYPCnGCkw&coSPz}^ z*m^W>!`YqYnwnIbT1m0;eK8kj`Br@>xpPeAv&oKl(1~S`T zKP|pzUd{Ppv6jrmIjTT>;I622oO+yF$n{gPUNGyOS%Vh+rBwW&L^k{cn>>kQ^&RsZTP?w$( zAES(Jet0DBp5?U&x%th0Vw`5#gG1~UMC=F-uuVAF#RU_56tG`N_3*QV53{MzcSbuq zFiE8Hrk_zA9h`~YAk#D@Z+7GX!n~tI}d<+2ufk3?EVEOIEcP!{*ZPhb&RbM zFw<;)em_}YwlZWt>1QJKcq!7X10gqnov1I*8mJ%Dm5)eglqKu!Rtun@s2r&;BQ&TF zHj?%2gSKridqy^)61TlMoIACPJ$U5cWuIF5EqlS+Fy`@&nricDvuVc!LY{vss0Eazt>`!~g8&fOQipjn17jpqS=<`#M?Ttxa|FcsoP@FFec$Ya@WQK79qH~8LAGVTfe{yoy< cZ&^+Jmf7BKyn;X7p7W221}W+x{I}`<05eZB!vFvP literal 0 HcmV?d00001 diff --git a/tests/functional/test_analysis.py b/tests/functional/test_analysis.py index 47e5add..5eb8c0a 100644 --- a/tests/functional/test_analysis.py +++ b/tests/functional/test_analysis.py @@ -1,3 +1,4 @@ +import os import shutil from pathlib import Path @@ -142,7 +143,14 @@ def test_update_expected_files(analysis_config_path, expected_path, tmp_path): def test_analyzer(analysis_config_path, expected_path, tmp_path): np.random.seed(0) + # copy the config file analysis_config_path = shutil.copy(analysis_config_path, tmp_path) + # copy any subdirectory + analysis_config_dir = analysis_config_path.parent + with os.scandir(analysis_config_path.parent) as it: + for entry in it: + if not entry.name.startswith(".") and entry.is_dir(): + shutil.copytree(analysis_config_dir / entry.name, tmp_path / entry.name) # test without cache with MultiAnalyzer.from_file(analysis_config_path) as multi_analyzer: diff --git a/tests/unit/adapters/test_circuit.py b/tests/unit/adapters/test_circuit.py index f38fe05..ac70e26 100644 --- a/tests/unit/adapters/test_circuit.py +++ b/tests/unit/adapters/test_circuit.py @@ -1,9 +1,11 @@ import pickle +from pathlib import Path import pytest from blueetl.adapters import circuit as test_module from blueetl.adapters.base import AdapterError +from blueetl.adapters.node_sets import NodeSetsAdapter from tests.unit.utils import BLUEPY_AVAILABLE, TEST_DATA_PATH, assert_isinstance @@ -39,7 +41,7 @@ def test_circuit_adapter(path, population, expected_classes, monkeypatch): path = TEST_DATA_PATH / "circuit" / path # enter the circuit dir to resolve relative paths in bluepy monkeypatch.chdir(path.parent) - obj = test_module.CircuitAdapter(path) + obj = test_module.CircuitAdapter.from_file(path) assert_isinstance(obj.instance, expected_classes["circuit"]) # access methods and properties @@ -61,8 +63,8 @@ def test_circuit_adapter(path, population, expected_classes, monkeypatch): def test_circuit_adapter_with_nonexistent_path(): - path = "path/to/circuit_config.json" - obj = test_module.CircuitAdapter(path) + path = Path("path/to/circuit_config.json") + obj = test_module.CircuitAdapter.from_file(path) assert obj.instance is None assert obj.exists() is False @@ -79,3 +81,15 @@ def test_circuit_adapter_with_nonexistent_path(): assert isinstance(loaded, test_module.CircuitAdapter) assert loaded.instance is None + + +def test_circuit_adapter_node_sets(): + path = TEST_DATA_PATH / "circuit" / "sonata" / "circuit_config.json" + obj = test_module.CircuitAdapter.from_file(path) + + assert_isinstance(obj.instance, "bluepysnap.Circuit") + + assert isinstance(obj.node_sets_file, Path) + assert obj.node_sets_file.name == "node_sets.json" + + assert isinstance(obj.node_sets, NodeSetsAdapter) diff --git a/tests/unit/adapters/test_node_sets.py b/tests/unit/adapters/test_node_sets.py new file mode 100644 index 0000000..43b52e8 --- /dev/null +++ b/tests/unit/adapters/test_node_sets.py @@ -0,0 +1,62 @@ +from pathlib import Path + +import pytest + +from blueetl.adapters import node_sets as test_module +from tests.unit.utils import TEST_NODE_SETS_FILE, TEST_NODE_SETS_FILE_EXTRA, assert_isinstance + + +def test_node_sets_adapter(): + path = TEST_NODE_SETS_FILE + expected_class_name = "bluepysnap.node_sets.NodeSets" + + obj = test_module.NodeSetsAdapter.from_file(path) + + assert_isinstance(obj.instance, expected_class_name) + assert obj.exists() is True + + +def test_node_sets_adapter_update(): + path = TEST_NODE_SETS_FILE + path_extra = TEST_NODE_SETS_FILE_EXTRA + + obj = test_module.NodeSetsAdapter.from_file(path) + obj_extra = test_module.NodeSetsAdapter.from_file(path_extra) + + assert "Layer2" in obj.instance + assert "ExtraLayer2" not in obj.instance + assert "ExtraLayer2" in obj_extra.instance + + obj.update(obj_extra) + assert "ExtraLayer2" in obj.instance + + +def test_node_sets_adapter_ior(): + path = TEST_NODE_SETS_FILE + path_extra = TEST_NODE_SETS_FILE_EXTRA + + obj = test_module.NodeSetsAdapter.from_file(path) + obj_extra = test_module.NodeSetsAdapter.from_file(path_extra) + + assert "Layer2" in obj.instance + assert "ExtraLayer2" not in obj.instance + assert "ExtraLayer2" in obj_extra.instance + + obj |= obj_extra + assert "ExtraLayer2" in obj.instance + + +def test_node_sets_adapter_with_none(): + expected_class_name = "bluepysnap.node_sets.NodeSets" + obj = test_module.NodeSetsAdapter.from_file(None) + + assert_isinstance(obj.instance, expected_class_name) + assert obj.exists() is True + assert obj.instance.content == {} + + +def test_node_sets_adapter_with_nonexistent_path(): + path = Path("path/to/non/existent/file.json") + + with pytest.raises(FileNotFoundError, match="No such file or directory"): + test_module.NodeSetsAdapter.from_file(path) diff --git a/tests/unit/adapters/test_simulation.py b/tests/unit/adapters/test_simulation.py index 41ee21b..72cd2d1 100644 --- a/tests/unit/adapters/test_simulation.py +++ b/tests/unit/adapters/test_simulation.py @@ -1,4 +1,5 @@ import pickle +from pathlib import Path import pytest @@ -47,7 +48,7 @@ def test_simulation_adapter(path, population, reports, expected_classes, monkeyp path = TEST_DATA_PATH / "simulation" / path # enter the circuit dir to resolve relative paths in bluepy monkeypatch.chdir(path.parent) - obj = test_module.SimulationAdapter(path) + obj = test_module.SimulationAdapter.from_file(path) assert_isinstance(obj.instance, expected_classes["simulation"]) assert obj.exists() is True @@ -76,8 +77,8 @@ def test_simulation_adapter(path, population, reports, expected_classes, monkeyp def test_simulation_adapter_with_nonexistent_path(): - path = "path/to/simulation_config.json" - obj = test_module.SimulationAdapter(path) + path = Path("path/to/simulation_config.json") + obj = test_module.SimulationAdapter.from_file(path) assert obj.instance is None assert obj.exists() is False diff --git a/tests/unit/apps/test_migrate.py b/tests/unit/apps/test_migrate.py index 070c7dc..85ada28 100644 --- a/tests/unit/apps/test_migrate.py +++ b/tests/unit/apps/test_migrate.py @@ -14,8 +14,8 @@ def test_migrate_config(tmp_path): "output": "output_dir", "extraction": { "neuron_classes": { - "L1_EXC": {"layer": [1], "synapse_class": ["EXC"]}, - "L1_EXC_gids": {"layer": [1], "synapse_class": ["EXC"], "gid": [1, 2]}, + "L1_EXC": {"layer": ["1"], "synapse_class": ["EXC"]}, + "L1_EXC_gids": {"layer": ["1"], "synapse_class": ["EXC"], "gid": [1, 2]}, }, "limit": None, "target": None, @@ -42,9 +42,9 @@ def test_migrate_config(tmp_path): "extraction": { "report": {"type": "spikes"}, "neuron_classes": { - "L1_EXC": {"query": {"layer": [1], "synapse_class": ["EXC"]}}, + "L1_EXC": {"query": {"layer": ["1"], "synapse_class": ["EXC"]}}, "L1_EXC_gids": { - "query": {"layer": [1], "synapse_class": ["EXC"]}, + "query": {"layer": ["1"], "synapse_class": ["EXC"]}, "node_id": [1, 2], }, }, diff --git a/tests/unit/config/test_analysis.py b/tests/unit/config/test_analysis.py index 397be01..6a27058 100644 --- a/tests/unit/config/test_analysis.py +++ b/tests/unit/config/test_analysis.py @@ -1,8 +1,10 @@ import pytest -import blueetl.config.analysis from blueetl.config import analysis as test_module -from blueetl.config.analysis_model import FeaturesConfig +from blueetl.config.analysis_model import FeaturesConfig, MultiAnalysisConfig +from blueetl.utils import load_yaml +from tests.functional.utils import TEST_DATA_PATH as TEST_DATA_PATH_FUNCTIONAL +from tests.unit.utils import TEST_DATA_PATH as TEST_DATA_PATH_UNIT from tests.unit.utils import assert_not_duplicates @@ -146,7 +148,7 @@ def test_config__resolve_features(config_list, expected_list): config_list = [FeaturesConfig(**d) for d in config_list] expected_list = [FeaturesConfig(**d) for d in expected_list] - result = blueetl.config.analysis._resolve_features(config_list) + result = test_module._resolve_features(config_list) assert result == expected_list assert_not_duplicates(result) @@ -168,3 +170,22 @@ def test_config__resolve_features_error(): with pytest.raises(ValueError, match="All the zip params must have the same length"): test_module._resolve_features(config_list) + + +@pytest.mark.parametrize( + "config_file", + [ + pytest.param(f, id=f"{f.relative_to(f.parents[2])}") + for base in ( + TEST_DATA_PATH_UNIT / "analysis", + TEST_DATA_PATH_FUNCTIONAL / "sonata" / "config", + TEST_DATA_PATH_FUNCTIONAL / "bbp" / "config", + ) + for f in base.glob("*.yaml") + ], +) +def test_init_multi_analysis_configuration(config_file): + config_dict = load_yaml(config_file) + base_path = config_file.parent + result = test_module.init_multi_analysis_configuration(config_dict, base_path=base_path) + assert isinstance(result, MultiAnalysisConfig) diff --git a/tests/unit/data/circuit/README b/tests/unit/data/circuit/README index af4f1a4..49735bb 100644 --- a/tests/unit/data/circuit/README +++ b/tests/unit/data/circuit/README @@ -1 +1 @@ -WARNING: This directory contains circuits used for tests, but they aren't full and valid. +WARNING: This directory contains circuits used for tests, but they aren't complete and valid. diff --git a/tests/unit/data/circuit/sonata/node_sets.json b/tests/unit/data/circuit/sonata/node_sets.json new file mode 100644 index 0000000..0dc7a00 --- /dev/null +++ b/tests/unit/data/circuit/sonata/node_sets.json @@ -0,0 +1,70 @@ +{ + "Layer2": { + "layer": "2" + }, + "Layer23": { + "layer": [ + "2", + "3" + ] + }, + "Empty_nodes": { + "node_id": [] + }, + "Node2012": { + "node_id": [ + 2, + 0, + 1, + 2 + ] + }, + "Node12_L6_Y": { + "mtype": "L6_Y", + "node_id": [ + 1, + 2 + ] + }, + "Node2_L6_Y": { + "mtype": "L6_Y", + "node_id": [ + 2 + ] + }, + "Node0_L6_Y": { + "mtype": "L6_Y", + "node_id": [ + 0 + ] + }, + "Empty_L6_Y": { + "node_id": [], + "mtype": "L6_Y" + }, + "Population_default": { + "population": "default" + }, + "Population_default2": { + "population": "default2" + }, + "Population_default_L6_Y": { + "population": "default", + "mtype": "L6_Y" + }, + "Population_default_L6_Y_Node2": { + "population": "default", + "mtype": "L6_Y", + "node_id": [ + 2 + ] + }, + "combined_Node0_L6_Y__Node12_L6_Y": [ + "Node0_L6_Y", + "Node12_L6_Y" + ], + "combined_combined_Node0_L6_Y__Node12_L6_Y__": [ + "combined_Node0_L6_Y__Node12_L6_Y", + "Layer23" + ] +} diff --git a/tests/unit/data/circuit/sonata/node_sets_extra.json b/tests/unit/data/circuit/sonata/node_sets_extra.json new file mode 100644 index 0000000..8c6d28b --- /dev/null +++ b/tests/unit/data/circuit/sonata/node_sets_extra.json @@ -0,0 +1,5 @@ +{ + "ExtraLayer2": { + "layer": "2" + } +} diff --git a/tests/unit/data/simulation/README b/tests/unit/data/simulation/README index fa3c991..d55cdb5 100644 --- a/tests/unit/data/simulation/README +++ b/tests/unit/data/simulation/README @@ -1 +1 @@ -WARNING: This directory contains simulations used for tests, but they aren't full and valid. \ No newline at end of file +WARNING: This directory contains simulations used for tests, but they aren't complete and valid. \ No newline at end of file diff --git a/tests/unit/extract/conftest.py b/tests/unit/extract/conftest.py index d902a99..1850359 100644 --- a/tests/unit/extract/conftest.py +++ b/tests/unit/extract/conftest.py @@ -3,13 +3,15 @@ import pandas as pd import pytest +from tests.unit.utils import TEST_NODE_SETS_FILE + def _get_cells(): """Return a DataFrame as returned by circuit.nodes[population].get().""" return pd.DataFrame( [ { - "layer": 1, + "layer": "1", "mtype": "L1_DAC", "etype": "cNAC", "region": "S1FL", @@ -19,7 +21,7 @@ def _get_cells(): "z": -1710.8, }, { - "layer": 2, + "layer": "2", "mtype": "L2_TPC:A", "etype": "cADpyr", "region": "S1FL", @@ -29,7 +31,7 @@ def _get_cells(): "z": -1987.2, }, { - "layer": 4, + "layer": "4", "mtype": "L4_BP", "etype": "cNAC", "region": "S1FL", @@ -47,6 +49,7 @@ def _get_cells(): def mock_circuit(): """Simplified mock of circuit, providing only get() and ids() for a node population.""" mock = MagicMock() + mock.node_sets_file = str(TEST_NODE_SETS_FILE) df = _get_cells() # circuit.nodes[population] mock_population = mock.nodes.__getitem__.return_value diff --git a/tests/unit/extract/test_neuron_classes.py b/tests/unit/extract/test_neuron_classes.py index be6ef16..7765602 100644 --- a/tests/unit/extract/test_neuron_classes.py +++ b/tests/unit/extract/test_neuron_classes.py @@ -31,13 +31,13 @@ def test_neuron_classes_from_neurons(): "L23_EXC": NeuronClassConfig( **{ "population": "thalamus_neurons", - "query": {"layer": [2, 3], "synapse_class": ["EXC"]}, + "query": {"layer": ["2", "3"], "synapse_class": ["EXC"]}, } ), "L4_INH": NeuronClassConfig( **{ "population": "thalamus_neurons", - "query": {"layer": [4], "synapse_class": ["INH"]}, + "query": {"layer": ["4"], "synapse_class": ["INH"]}, } ), }, @@ -54,7 +54,7 @@ def test_neuron_classes_from_neurons(): "population": "thalamus_neurons", "node_set": None, "gids": None, - "query": json.dumps({"layer": [2, 3], "synapse_class": ["EXC"]}), + "query": json.dumps({"layer": ["2", "3"], "synapse_class": ["EXC"]}), }, { CIRCUIT_ID: 0, @@ -64,7 +64,7 @@ def test_neuron_classes_from_neurons(): "population": "thalamus_neurons", "node_set": None, "gids": None, - "query": json.dumps({"layer": [4], "synapse_class": ["INH"]}), + "query": json.dumps({"layer": ["4"], "synapse_class": ["INH"]}), }, ] ) diff --git a/tests/unit/extract/test_neurons.py b/tests/unit/extract/test_neurons.py index 1f985f0..dff17f5 100644 --- a/tests/unit/extract/test_neurons.py +++ b/tests/unit/extract/test_neurons.py @@ -9,6 +9,7 @@ from blueetl.constants import CIRCUIT, CIRCUIT_ID, GID, NEURON_CLASS, SIMULATION, SIMULATION_ID from blueetl.extract.neurons import Neurons from blueetl.utils import ensure_dtypes +from tests.unit.utils import TEST_NODE_SETS_FILE_EXTRA def test_neurons_from_simulations(mock_circuit): @@ -25,13 +26,24 @@ def test_neurons_from_simulations(mock_circuit): mock_simulations = Mock() type(mock_simulations).df = mock_simulations_df neuron_classes = { - "L1_INH": NeuronClassConfig( - **{"population": "thalamus_neurons", "query": {"layer": [1], "synapse_class": ["INH"]}} + "L1_INH": NeuronClassConfig.model_validate( + { + "population": "thalamus_neurons", + "query": {"layer": ["1"], "synapse_class": ["INH"]}, + } ), "MY_GIDS": NeuronClassConfig(**{"population": "thalamus_neurons", "node_id": [200, 300]}), - "EMPTY": NeuronClassConfig(**{"population": "thalamus_neurons", "query": {"layer": [999]}}), - "LIMITED": NeuronClassConfig( - **{"population": "thalamus_neurons", "query": {"synapse_class": ["INH"]}, "limit": 1} + "EMPTY": NeuronClassConfig.model_validate( + {"population": "thalamus_neurons", "query": {"layer": ["999"]}} + ), + "LIMITED": NeuronClassConfig.model_validate( + { + "population": "thalamus_neurons", + "query": {"synapse_class": ["INH"]}, + "limit": 1, + "node_set": "ExtraLayer2", + "node_sets_file": TEST_NODE_SETS_FILE_EXTRA, + } ), } result = Neurons.from_simulations( @@ -70,7 +82,7 @@ def test_neurons_from_simulations(mock_circuit): expected_df = ensure_dtypes(expected_df) assert isinstance(result, Neurons) assert_frame_equal(result.df, expected_df) - assert mock_circuit.nodes.__getitem__.return_value.get.call_count == 1 + assert mock_circuit.nodes.__getitem__.return_value.get.call_count == 2 assert mock_simulations_df.call_count == 1 @@ -86,7 +98,9 @@ def test_neurons_from_simulations_without_neurons(mock_circuit): mock_simulations = Mock() type(mock_simulations).df = mock_simulations_df neuron_classes = { - "EMPTY": NeuronClassConfig(**{"population": "thalamus_neurons", "query": {"layer": [999]}}), + "EMPTY": NeuronClassConfig( + **{"population": "thalamus_neurons", "query": {"layer": ["999"]}} + ), } with pytest.raises(RuntimeError, match="No data extracted to Neurons"): diff --git a/tests/unit/extract/test_simulations.py b/tests/unit/extract/test_simulations.py index 916cc51..d3d7f5d 100644 --- a/tests/unit/extract/test_simulations.py +++ b/tests/unit/extract/test_simulations.py @@ -25,7 +25,7 @@ def test_simulations_from_config(mock_simulation_class): mock_simulation1 = _get_mock_simulation() mock_circuit0 = mock_simulation0.circuit mock_circuit1 = mock_simulation1.circuit - mock_simulation_class.side_effect = [mock_simulation0, mock_simulation1] + mock_simulation_class.from_file.side_effect = [mock_simulation0, mock_simulation1] config = MagicMock(SimulationCampaign) config.get.return_value = pd.DataFrame( [ @@ -62,7 +62,7 @@ def test_simulations_from_config(mock_simulation_class): assert_frame_equal(result.df, expected_df) assert config.get.call_count == 1 - assert mock_simulation_class.call_count == 2 + assert mock_simulation_class.from_file.call_count == 2 assert mock_circuit0 != mock_circuit1 assert mock_simulation0.exists.call_count == 1 assert mock_simulation1.exists.call_count == 1 @@ -76,7 +76,7 @@ def test_simulations_from_config_filtered_by_simulation_id(mock_simulation_class mock_simulation1 = _get_mock_simulation() mock_circuit0 = mock_simulation0.circuit mock_circuit1 = mock_simulation1.circuit - mock_simulation_class.side_effect = [mock_simulation0, mock_simulation1] + mock_simulation_class.from_file.side_effect = [mock_simulation0, mock_simulation1] config = MagicMock(SimulationCampaign) config.get.return_value = pd.DataFrame( [ @@ -104,7 +104,7 @@ def test_simulations_from_config_filtered_by_simulation_id(mock_simulation_class assert_frame_equal(result.df, expected_df) assert config.get.call_count == 1 - assert mock_simulation_class.call_count == 2 + assert mock_simulation_class.from_file.call_count == 2 assert mock_circuit0 != mock_circuit1 assert mock_simulation0.exists.call_count == 1 assert mock_simulation1.exists.call_count == 1 @@ -118,7 +118,7 @@ def test_simulations_from_config_without_spikes(mock_simulation_class): mock_simulation1 = _get_mock_simulation(is_complete=True) mock_circuit0 = mock_simulation0.circuit mock_circuit1 = mock_simulation1.circuit - mock_simulation_class.side_effect = [mock_simulation0, mock_simulation1] + mock_simulation_class.from_file.side_effect = [mock_simulation0, mock_simulation1] config = MagicMock(SimulationCampaign) config.get.return_value = pd.DataFrame( [ @@ -147,7 +147,7 @@ def test_simulations_from_config_without_spikes(mock_simulation_class): assert_frame_equal(result.df, expected_df) assert config.get.call_count == 1 - assert mock_simulation_class.call_count == 2 + assert mock_simulation_class.from_file.call_count == 2 assert mock_circuit0 != mock_circuit1 assert mock_simulation0.exists.call_count == 1 assert mock_simulation1.exists.call_count == 1 @@ -161,7 +161,7 @@ def test_simulations_from_config_first_nonexistent(mock_simulation_class): mock_simulation1 = _get_mock_simulation(exists=True) mock_circuit0 = mock_simulation0.circuit mock_circuit1 = mock_simulation1.circuit - mock_simulation_class.side_effect = [mock_simulation0, mock_simulation1] + mock_simulation_class.from_file.side_effect = [mock_simulation0, mock_simulation1] config = MagicMock(SimulationCampaign) config.get.return_value = pd.DataFrame( [ @@ -192,7 +192,7 @@ def test_simulations_from_config_first_nonexistent(mock_simulation_class): assert_frame_equal(result.df, expected_df) assert config.get.call_count == 1 - assert mock_simulation_class.call_count == 2 + assert mock_simulation_class.from_file.call_count == 2 assert mock_circuit0 != mock_circuit1 assert mock_simulation0.exists.call_count == 1 assert mock_simulation1.exists.call_count == 1 @@ -206,7 +206,7 @@ def test_simulations_from_pandas_load_complete_campaign(mock_simulation_class): mock_simulation1 = _get_mock_simulation() mock_circuit0 = mock_simulation0.circuit mock_circuit1 = mock_simulation1.circuit - mock_simulation_class.side_effect = [mock_simulation0, mock_simulation1] + mock_simulation_class.from_file.side_effect = [mock_simulation0, mock_simulation1] df = pd.DataFrame( [ { @@ -252,7 +252,7 @@ def test_simulations_from_pandas_load_complete_campaign(mock_simulation_class): assert isinstance(result, test_module.Simulations) assert_frame_equal(result.df, expected_df) - assert mock_simulation_class.call_count == 2 + assert mock_simulation_class.from_file.call_count == 2 assert mock_circuit0 != mock_circuit1 assert mock_simulation0.exists.call_count == 1 assert mock_simulation1.exists.call_count == 1 @@ -266,7 +266,7 @@ def test_simulations_from_pandas_load_incomplete_campaign(mock_simulation_class) mock_simulation1 = _get_mock_simulation() mock_circuit0 = mock_simulation0.circuit mock_circuit1 = mock_simulation1.circuit - mock_simulation_class.side_effect = [mock_simulation0, mock_simulation1] + mock_simulation_class.from_file.side_effect = [mock_simulation0, mock_simulation1] df = pd.DataFrame( [ { @@ -312,7 +312,7 @@ def test_simulations_from_pandas_load_incomplete_campaign(mock_simulation_class) assert isinstance(result, test_module.Simulations) assert_frame_equal(result.df, expected_df) - assert mock_simulation_class.call_count == 2 + assert mock_simulation_class.from_file.call_count == 2 assert mock_circuit0 != mock_circuit1 assert mock_simulation0.exists.call_count == 1 assert mock_simulation1.exists.call_count == 1 @@ -328,7 +328,7 @@ def test_simulations_from_pandas_load_inconsistent_campaign(mock_simulation_clas mock_simulation1 = _get_mock_simulation(1) mock_circuit0 = mock_simulation0.circuit mock_circuit1 = mock_simulation1.circuit - mock_simulation_class.side_effect = [mock_simulation0, mock_simulation1] + mock_simulation_class.from_file.side_effect = [mock_simulation0, mock_simulation1] df = pd.DataFrame( [ { @@ -350,7 +350,7 @@ def test_simulations_from_pandas_load_inconsistent_campaign(mock_simulation_clas with pytest.raises(test_module.InconsistentSimulations, match="Inconsistent hash and id"): test_module.Simulations.from_pandas(df) - assert mock_simulation_class.call_count == 2 + assert mock_simulation_class.from_file.call_count == 2 assert mock_circuit0 != mock_circuit1 assert mock_simulation0.exists.call_count == 1 assert mock_simulation1.exists.call_count == 1 @@ -364,7 +364,7 @@ def test_simulations_from_pandas_first_nonexistent(mock_simulation_class): mock_simulation1 = _get_mock_simulation(exists=True) mock_circuit0 = mock_simulation0.circuit mock_circuit1 = mock_simulation1.circuit - mock_simulation_class.side_effect = [mock_simulation0, mock_simulation1] + mock_simulation_class.from_file.side_effect = [mock_simulation0, mock_simulation1] df = pd.DataFrame( [ { @@ -386,7 +386,7 @@ def test_simulations_from_pandas_first_nonexistent(mock_simulation_class): with pytest.raises(test_module.InconsistentSimulations, match="Inconsistent cache"): test_module.Simulations.from_pandas(df) - assert mock_simulation_class.call_count == 2 + assert mock_simulation_class.from_file.call_count == 2 assert mock_circuit0 != mock_circuit1 assert mock_simulation0.exists.call_count == 1 assert mock_simulation1.exists.call_count == 1 @@ -400,7 +400,7 @@ def test_simulations_from_pandas_filtered_by_simulation_id(mock_simulation_class mock_simulation1 = _get_mock_simulation() mock_circuit0 = mock_simulation0.circuit mock_circuit1 = mock_simulation1.circuit - mock_simulation_class.side_effect = [mock_simulation0, mock_simulation1] + mock_simulation_class.from_file.side_effect = [mock_simulation0, mock_simulation1] df = pd.DataFrame( [ @@ -439,7 +439,7 @@ def test_simulations_from_pandas_filtered_by_simulation_id(mock_simulation_class assert_frame_equal(result.df, expected_df) - assert mock_simulation_class.call_count == 2 + assert mock_simulation_class.from_file.call_count == 2 assert mock_circuit0 != mock_circuit1 assert mock_simulation0.exists.call_count == 1 assert mock_simulation1.exists.call_count == 1 diff --git a/tests/unit/extract/test_windows.py b/tests/unit/extract/test_windows.py index 48bdbf7..e488b40 100644 --- a/tests/unit/extract/test_windows.py +++ b/tests/unit/extract/test_windows.py @@ -21,6 +21,7 @@ from blueetl.extract import windows as test_module from blueetl.resolver import AttrResolver from blueetl.utils import ensure_dtypes +from tests.unit.utils import TEST_NODE_SETS_FILE_EXTRA def _myfunc1(spikes, params): @@ -114,6 +115,8 @@ def test_windows_from_simulations(mock_simulation, mock_circuit): "ts2": TrialStepsConfig( function=f"{__name__}._myfunc2", bounds=[-20, 200], + node_set="ExtraLayer2", + node_sets_file=TEST_NODE_SETS_FILE_EXTRA, ), } diff --git a/tests/unit/utils.py b/tests/unit/utils.py index 0b061c7..ae2ffe9 100644 --- a/tests/unit/utils.py +++ b/tests/unit/utils.py @@ -13,6 +13,8 @@ BLUEPY_AVAILABLE = False TEST_DATA_PATH = Path(__file__).parent / "data" +TEST_NODE_SETS_FILE = TEST_DATA_PATH / "circuit" / "sonata" / "node_sets.json" +TEST_NODE_SETS_FILE_EXTRA = TEST_DATA_PATH / "circuit" / "sonata" / "node_sets_extra.json" def assert_frame_equal(actual, expected): diff --git a/tox.ini b/tox.ini index ef1272d..ec66292 100644 --- a/tox.ini +++ b/tox.ini @@ -69,12 +69,12 @@ deps = mypy types-PyYAML commands = + isort --check-only --diff {[base]path} + black --check . ruff check {[base]path} pycodestyle {[base]path} pydocstyle {[base]path} pylint {[base]path} - isort --check-only --diff {[base]path} - black --check . mypy --show-error-codes --ignore-missing-imports --allow-redefinition {[base]path} [testenv:format]