BlueBrain / snap

@@ -31,193 +31,39 @@
Loading
31 31
from bluepysnap.circuit_ids import CircuitNodeId, CircuitNodeIds
32 32
from bluepysnap.exceptions import BluepySnapError
33 33
from bluepysnap.network import NetworkObject
34 -
from bluepysnap.sonata_constants import DEFAULT_NODE_TYPE, DYNAMICS_PREFIX, ConstContainer, Node
34 +
from bluepysnap.sonata_constants import DYNAMICS_PREFIX, ConstContainer, Node
35 35
36 36
37 -
class Nodes(
38 -
    NetworkObject,
39 -
    metaclass=AbstractDocSubstitutionMeta,
40 -
    source_word="NetworkObject",
41 -
    target_word="Node",
42 -
):
43 -
    """The top level Nodes accessor."""
44 -
45 -
    def __init__(self, circuit):  # pylint: disable=useless-super-delegation
46 -
        """Initialize the top level Nodes accessor."""
47 -
        super().__init__(circuit)
48 -
49 -
    def _collect_populations(self):
50 -
        return self._get_populations(NodeStorage, self._config["networks"]["nodes"])
51 -
52 -
    def property_values(self, prop):
53 -
        """Returns all the values for a given Nodes property."""
54 -
        return set(
55 -
            value
56 -
            for pop in self.values()
57 -
            if prop in pop.property_names
58 -
            for value in pop.property_values(prop)
59 -
        )
60 -
61 -
    def ids(self, group=None, sample=None, limit=None):
62 -
        """Returns the CircuitNodeIds corresponding to the nodes from ``group``.
63 -
64 -
        Args:
65 -
            group (CircuitNodeId/CircuitNodeIds/int/sequence/str/mapping/None): Which IDs will be
66 -
            returned depends on the type of the ``group`` argument:
67 -
                - ``CircuitNodeId``: return the ID in a CircuitNodeIds object if it belongs to
68 -
                    the circuit.
69 -
                - ``CircuitNodeIds``: return the IDs in a CircuitNodeIds object if they belong to
70 -
                    the circuit.
71 -
                - ``int``: if the node ID is present in all populations, returns a CircuitNodeIds
72 -
                    object containing the corresponding node ID for all populations.
73 -
                - ``sequence``: if all the values contained in the sequence are present in all
74 -
                    populations, returns a CircuitNodeIds object containing the corresponding node
75 -
                    IDs for all populations.
76 -
                - ``str``: use a node set name as input. Returns a CircuitNodeIds object containing
77 -
                    nodes selected by the node set.
78 -
                - ``mapping``: Returns a CircuitNodeIds object containing nodes matching a
79 -
                    properties filter.
80 -
                - ``None``: return all node IDs of the circuit in a CircuitNodeIds object.
81 -
            sample (int): If specified, randomly choose ``sample`` number of
82 -
                IDs from the match result. If the size of the sample is greater than
83 -
                the size of all the NodePopulations then all ids are taken and shuffled.
84 -
            limit (int): If specified, return the first ``limit`` number of
85 -
                IDs from the match result. If limit is greater than the size of all the populations,
86 -
                all node IDs are returned.
87 -
88 -
        Returns:
89 -
            CircuitNodeIds: returns a CircuitNodeIds containing all the node IDs and the
90 -
                corresponding populations. All the explicitly requested IDs must be present inside
91 -
                the circuit.
92 -
93 -
        Raises:
94 -
            BluepySnapError: when a population from a CircuitNodeIds is not present in the circuit.
95 -
            BluepySnapError: when an id query via a int, sequence, or CircuitNodeIds is not present
96 -
                in the circuit.
97 -
98 -
        Examples:
99 -
            The available group parameter values (example with 2 node populations pop1 and pop2):
100 -
101 -
            >>> nodes = circuit.nodes
102 -
            >>> nodes.ids(group=None)  #  returns all CircuitNodeIds from the circuit
103 -
            >>> node_ids = CircuitNodeIds.from_arrays(["pop1", "pop2"], [1, 3])
104 -
            >>> nodes.ids(group=node_ids)  #  returns ID 1 from pop1 and ID 3 from pop2
105 -
            >>> nodes.ids(group=0)  #  returns CircuitNodeIds 0 from pop1 and pop2
106 -
            >>> nodes.ids(group=[0, 1])  #  returns CircuitNodeIds 0 and 1 from pop1 and pop2
107 -
            >>> nodes.ids(group="node_set_name")  # returns CircuitNodeIds matching node set
108 -
            >>> nodes.ids(group={Node.LAYER: 2})  # returns CircuitNodeIds matching layer==2
109 -
            >>> nodes.ids(group={Node.LAYER: [2, 3]})  # returns CircuitNodeIds with layer in [2,3]
110 -
            >>> nodes.ids(group={Node.X: (0, 1)})  # returns CircuitNodeIds with 0 < x < 1
111 -
            >>> # returns CircuitNodeIds matching one of the queries inside the 'or' list
112 -
            >>> nodes.ids(group={'$or': [{ Node.LAYER: [2, 3]},
113 -
            >>>                          { Node.X: (0, 1), Node.MTYPE: 'L1_SLAC' }]})
114 -
            >>> # returns CircuitNodeIds matching all the queries inside the 'and' list
115 -
            >>> nodes.ids(group={'$and': [{ Node.LAYER: [2, 3]},
116 -
            >>>                           { Node.X: (0, 1), Node.MTYPE: 'L1_SLAC' }]})
117 -
        """
118 -
        if isinstance(group, CircuitNodeIds):
119 -
            diff = np.setdiff1d(group.get_populations(unique=True), self.population_names)
120 -
            if diff.size != 0:
121 -
                raise BluepySnapError(f"Population {diff} does not exist in the circuit.")
122 -
123 -
        fun = lambda x: (x.ids(group, raise_missing_property=False), x.name)
124 -
        return self._get_ids_from_pop(fun, CircuitNodeIds, sample=sample, limit=limit)
125 -
126 -
    def get(self, group=None, properties=None):  # pylint: disable=arguments-differ
127 -
        """Node properties as a pandas DataFrame.
128 -
129 -
        Args:
130 -
            group (CircuitNodeIds/int/sequence/str/mapping/None): Which nodes will have their
131 -
                properties returned depends on the type of the ``group`` argument:
132 -
                See :py:class:`~bluepysnap.nodes.Nodes.ids`.
133 -
134 -
            properties (str/list): If specified, return only the properties in the list.
135 -
                Otherwise return all properties.
136 -
137 -
        Returns:
138 -
            pandas.DataFrame: Return a pandas DataFrame indexed by NodeCircuitIds containing the
139 -
                properties from ``properties``.
140 -
141 -
        Notes:
142 -
            The NodePopulation.property_names function will give you all the usable properties
143 -
            for the `properties` argument.
144 -
        """
145 -
        if properties is None:
146 -
            properties = self.property_names
147 -
        return super().get(group, properties)
148 -
149 -
150 -
class NodeStorage:
151 -
    """Node storage access."""
37 +
class NodePopulation:
38 +
    """Node population access."""
152 39
153 -
    def __init__(self, config, circuit):
154 -
        """Initializes a NodeStorage object from a node config and a Circuit.
40 +
    def __init__(self, circuit, population_name):
41 +
        """Initializes a NodePopulation object.
155 42
156 43
        Args:
157 -
            config (dict): a node config from the global circuit config
158 -
            circuit (bluepysnap.Circuit): the circuit object that contains the NodePopulation
159 -
            from this storage.
44 +
            circuit (bluepysnap.Circuit): the circuit object containing the node population
45 +
            population_name (str): the name of the node population
160 46
161 47
        Returns:
162 -
            NodeStorage: A NodeStorage object.
48 +
            NodePopulation: A NodePopulation object.
163 49
        """
164 -
        self._h5_filepath = config["nodes_file"]
165 -
        self._csv_filepath = config.get("node_types_file")
166 -
        self._populations_config = config.get("populations", {})
167 50
        self._circuit = circuit
168 -
        self._populations = {}
51 +
        self.name = population_name
169 52
170 53
    @property
171 -
    def storage(self):
172 -
        """Access to the libsonata node storage."""
173 -
        return libsonata.NodeStorage(self._h5_filepath)
54 +
    def _node_sets(self):
55 +
        """Node sets defined for this node population."""
56 +
        return self._circuit.node_sets
174 57
175 58
    @cached_property
176 -
    def population_names(self):
177 -
        """Returns all population names inside this file."""
178 -
        return self.storage.population_names
179 -
180 -
    @property
181 -
    def h5_filepath(self):
182 -
        """Returns the filepath of the Storage."""
183 -
        return self._h5_filepath
184 -
185 -
    @property
186 -
    def csv_filepath(self):
187 -
        """Returns the csv filepath of the Storage."""
188 -
        return self._csv_filepath
189 -
190 -
    @property
191 -
    def circuit(self):
192 -
        """Returns the circuit object containing this storage."""
193 -
        return self._circuit
194 -
195 -
    def population(self, population_name):
196 -
        """Access the different populations from the storage."""
197 -
        if population_name not in self._populations:
198 -
            population_config = self._populations_config.get(population_name)
199 -
            self._populations[population_name] = NodePopulation(
200 -
                self, population_name, population_config=population_config
201 -
            )
202 -
203 -
        return self._populations[population_name]
204 -
205 -
    def load_population_data(self, population):
206 -
        """Load node properties from SONATA Nodes.
207 -
208 -
        Args:
209 -
            population (str): a population name .
210 -
211 -
        Returns:
212 -
            pandas.DataFrame with node properties (zero-based index).
213 -
        """
214 -
        nodes = self.storage.open_population(population)
59 +
    def _data(self):
60 +
        """Collect data for the node population as a pandas.DataFrame."""
61 +
        nodes = self._population
215 62
        categoricals = nodes.enumeration_names
216 63
217 -
        node_count = nodes.size
218 -
        result = pd.DataFrame(index=np.arange(node_count))
64 +
        _all = nodes.select_all()
65 +
        result = pd.DataFrame(index=np.arange(_all.flat_size))
219 66
220 -
        _all = libsonata.Selection([(0, node_count)])
221 67
        for attr in sorted(nodes.attribute_names):
222 68
            if attr in categoricals:
223 69
                enumeration = np.asarray(nodes.get_enumeration(attr, _all))
@@ -234,55 +80,23 @@
Loading
234 80
            result[attr] = nodes.get_dynamics_attribute(attr.split(DYNAMICS_PREFIX)[1], _all)
235 81
        return result
236 82
237 -
238 -
class NodePopulation:
239 -
    """Node population access."""
240 -
241 -
    def __init__(self, node_storage, population_name, population_config=None):
242 -
        """Initializes a NodePopulation object from a NodeStorage and population name.
243 -
244 -
        Args:
245 -
            node_storage (NodeStorage): the node storage containing the node population
246 -
            population_name (str): the name of the node population
247 -
            population_config (dict): the config for the population
248 -
249 -
        Returns:
250 -
            NodePopulation: A NodePopulation object.
251 -
        """
252 -
        self._config = population_config or {}
253 -
        self._node_storage = node_storage
254 -
        self.name = population_name
255 -
256 83
    @property
257 -
    def _node_sets(self):
258 -
        """Node sets defined for this node population."""
259 -
        return self._node_storage.circuit.node_sets
84 +
    def _properties(self):
85 +
        return self._circuit.to_libsonata.node_population_properties(self.name)
260 86
261 -
    @cached_property
262 -
    def _data(self):
263 -
        """Collected data for the node population as a pandas.DataFrame."""
264 -
        return self._node_storage.load_population_data(self.name)
265 -
266 -
    @cached_property
87 +
    @property
267 88
    def _population(self):
268 -
        return self._node_storage.storage.open_population(self.name)
89 +
        return self._circuit.to_libsonata.node_population(self.name)
269 90
270 91
    @cached_property
271 92
    def size(self):
272 93
        """Node population size."""
273 94
        return self._population.size
274 95
275 -
    @cached_property
276 -
    def config(self):
277 -
        """Population config dictionary combined with the components dictionary."""
278 -
        components = deepcopy(self._node_storage.circuit.config.get("components", {}))
279 -
        components.update(self._config)
280 -
        return components
281 -
282 96
    @property
283 97
    def type(self):
284 98
        """Population type."""
285 -
        return self.config.get("type", DEFAULT_NODE_TYPE)
99 +
        return self._properties.type
286 100
287 101
    @cached_property
288 102
    def _property_names(self):
@@ -300,9 +114,7 @@
Loading
300 114
            source.
301 115
        """
302 116
        return set(
303 -
            edge.name
304 -
            for edge in self._node_storage.circuit.edges.values()
305 -
            if self.name == edge.source.name
117 +
            edge.name for edge in self._circuit.edges.values() if self.name == edge.source.name
306 118
        )
307 119
308 120
    def target_in_edges(self):
@@ -313,9 +125,7 @@
Loading
313 125
            target.
314 126
        """
315 127
        return set(
316 -
            edge.name
317 -
            for edge in self._node_storage.circuit.edges.values()
318 -
            if self.name == edge.target.name
128 +
            edge.name for edge in self._circuit.edges.values() if self.name == edge.target.name
319 129
        )
320 130
321 131
    @property
@@ -747,9 +557,9 @@
Loading
747 557
        from bluepysnap.morph import MorphHelper
748 558
749 559
        return MorphHelper(
750 -
            self.config.get("morphologies_dir"),
560 +
            self._properties.morphologies_dir,
751 561
            self,
752 -
            alternate_morphologies=self.config.get("alternate_morphologies"),
562 +
            alternate_morphologies=self._properties.alternate_morphology_formats,
753 563
        )
754 564
755 565
    @cached_property
@@ -757,9 +567,134 @@
Loading
757 567
        """Access to node neuron models."""
758 568
        from bluepysnap.neuron_models import NeuronModelsHelper
759 569
760 -
        return NeuronModelsHelper(self.config, self)
570 +
        return NeuronModelsHelper(self._properties, self)
761 571
762 -
    @property
572 +
    @cached_property
763 573
    def h5_filepath(self):
764 574
        """Get the H5 nodes file associated with population."""
765 -
        return self._node_storage.h5_filepath
575 +
        for node_conf in self._circuit.config["networks"]["nodes"]:
576 +
            if self.name in node_conf["populations"]:
577 +
                return node_conf["nodes_file"]
578 +
        for node_conf in self._circuit.config["networks"]["nodes"]:
579 +
            h5_filepath = node_conf["nodes_file"]
580 +
            storage = libsonata.NodeStorage(h5_filepath)
581 +
            if self.name in storage.population_names:  # pylint: disable=unsupported-membership-test
582 +
                return h5_filepath
583 +
        raise BluepySnapError(f"h5_filepath not found for population '{self.name}'")
584 +
585 +
586 +
class Nodes(
587 +
    NetworkObject,
588 +
    metaclass=AbstractDocSubstitutionMeta,
589 +
    source_word="NetworkObject",
590 +
    target_word="Node",
591 +
):
592 +
    """The top level Nodes accessor."""
593 +
594 +
    _population_class = NodePopulation
595 +
596 +
    def __init__(self, circuit):  # pylint: disable=useless-super-delegation
597 +
        """Initialize the top level Nodes accessor."""
598 +
        super().__init__(circuit)
599 +
600 +
    @cached_property
601 +
    def population_names(self):
602 +
        """Defines all sorted node population names from the Circuit."""
603 +
        return sorted(self._circuit.to_libsonata.node_populations)
604 +
605 +
    def property_values(self, prop):
606 +
        """Returns all the values for a given Nodes property."""
607 +
        return set(
608 +
            value
609 +
            for pop in self.values()
610 +
            if prop in pop.property_names
611 +
            for value in pop.property_values(prop)
612 +
        )
613 +
614 +
    def ids(self, group=None, sample=None, limit=None):
615 +
        """Returns the CircuitNodeIds corresponding to the nodes from ``group``.
616 +
617 +
        Args:
618 +
            group (CircuitNodeId/CircuitNodeIds/int/sequence/str/mapping/None): Which IDs will be
619 +
            returned depends on the type of the ``group`` argument:
620 +
                - ``CircuitNodeId``: return the ID in a CircuitNodeIds object if it belongs to
621 +
                    the circuit.
622 +
                - ``CircuitNodeIds``: return the IDs in a CircuitNodeIds object if they belong to
623 +
                    the circuit.
624 +
                - ``int``: if the node ID is present in all populations, returns a CircuitNodeIds
625 +
                    object containing the corresponding node ID for all populations.
626 +
                - ``sequence``: if all the values contained in the sequence are present in all
627 +
                    populations, returns a CircuitNodeIds object containing the corresponding node
628 +
                    IDs for all populations.
629 +
                - ``str``: use a node set name as input. Returns a CircuitNodeIds object containing
630 +
                    nodes selected by the node set.
631 +
                - ``mapping``: Returns a CircuitNodeIds object containing nodes matching a
632 +
                    properties filter.
633 +
                - ``None``: return all node IDs of the circuit in a CircuitNodeIds object.
634 +
            sample (int): If specified, randomly choose ``sample`` number of
635 +
                IDs from the match result. If the size of the sample is greater than
636 +
                the size of all the NodePopulations then all ids are taken and shuffled.
637 +
            limit (int): If specified, return the first ``limit`` number of
638 +
                IDs from the match result. If limit is greater than the size of all the populations,
639 +
                all node IDs are returned.
640 +
641 +
        Returns:
642 +
            CircuitNodeIds: returns a CircuitNodeIds containing all the node IDs and the
643 +
                corresponding populations. All the explicitly requested IDs must be present inside
644 +
                the circuit.
645 +
646 +
        Raises:
647 +
            BluepySnapError: when a population from a CircuitNodeIds is not present in the circuit.
648 +
            BluepySnapError: when an id query via a int, sequence, or CircuitNodeIds is not present
649 +
                in the circuit.
650 +
651 +
        Examples:
652 +
            The available group parameter values (example with 2 node populations pop1 and pop2):
653 +
654 +
            >>> nodes = circuit.nodes
655 +
            >>> nodes.ids(group=None)  #  returns all CircuitNodeIds from the circuit
656 +
            >>> node_ids = CircuitNodeIds.from_arrays(["pop1", "pop2"], [1, 3])
657 +
            >>> nodes.ids(group=node_ids)  #  returns ID 1 from pop1 and ID 3 from pop2
658 +
            >>> nodes.ids(group=0)  #  returns CircuitNodeIds 0 from pop1 and pop2
659 +
            >>> nodes.ids(group=[0, 1])  #  returns CircuitNodeIds 0 and 1 from pop1 and pop2
660 +
            >>> nodes.ids(group="node_set_name")  # returns CircuitNodeIds matching node set
661 +
            >>> nodes.ids(group={Node.LAYER: 2})  # returns CircuitNodeIds matching layer==2
662 +
            >>> nodes.ids(group={Node.LAYER: [2, 3]})  # returns CircuitNodeIds with layer in [2,3]
663 +
            >>> nodes.ids(group={Node.X: (0, 1)})  # returns CircuitNodeIds with 0 < x < 1
664 +
            >>> # returns CircuitNodeIds matching one of the queries inside the 'or' list
665 +
            >>> nodes.ids(group={'$or': [{ Node.LAYER: [2, 3]},
666 +
            >>>                          { Node.X: (0, 1), Node.MTYPE: 'L1_SLAC' }]})
667 +
            >>> # returns CircuitNodeIds matching all the queries inside the 'and' list
668 +
            >>> nodes.ids(group={'$and': [{ Node.LAYER: [2, 3]},
669 +
            >>>                           { Node.X: (0, 1), Node.MTYPE: 'L1_SLAC' }]})
670 +
        """
671 +
        if isinstance(group, CircuitNodeIds):
672 +
            diff = np.setdiff1d(group.get_populations(unique=True), self.population_names)
673 +
            if diff.size != 0:
674 +
                raise BluepySnapError(f"Population {diff} does not exist in the circuit.")
675 +
676 +
        fun = lambda x: (x.ids(group, raise_missing_property=False), x.name)
677 +
        return self._get_ids_from_pop(fun, CircuitNodeIds, sample=sample, limit=limit)
678 +
679 +
    def get(self, group=None, properties=None):  # pylint: disable=arguments-differ
680 +
        """Node properties as a pandas DataFrame.
681 +
682 +
        Args:
683 +
            group (CircuitNodeIds/int/sequence/str/mapping/None): Which nodes will have their
684 +
                properties returned depends on the type of the ``group`` argument:
685 +
                See :py:class:`~bluepysnap.nodes.Nodes.ids`.
686 +
687 +
            properties (str/list): If specified, return only the properties in the list.
688 +
                Otherwise return all properties.
689 +
690 +
        Returns:
691 +
            pandas.DataFrame: Return a pandas DataFrame indexed by NodeCircuitIds containing the
692 +
                properties from ``properties``.
693 +
694 +
        Notes:
695 +
            The NodePopulation.property_names function will give you all the usable properties
696 +
            for the `properties` argument.
697 +
        """
698 +
        if properties is None:
699 +
            properties = self.property_names
700 +
        return super().get(group, properties)

@@ -17,10 +17,12 @@
Loading
17 17
18 18
"""SONATA network config parsing."""
19 19
20 +
import json
20 21
from collections.abc import Iterable, Mapping
21 22
from pathlib import Path
22 23
23 -
from bluepysnap import utils
24 +
import libsonata
25 +
24 26
from bluepysnap.exceptions import BluepySnapError
25 27
26 28
# List of keys which are expected to have paths
@@ -29,7 +31,8 @@
Loading
29 31
    "biophysical_neuron_models_dir",
30 32
    "vasculature_file",
31 33
    "vasculature_mesh",
32 -
    "end_feet_area",
34 +
    "endfeet_meshes_file",
35 +
    "microdomains_file",
33 36
    "neurolucida-asc",
34 37
    "h5v1",
35 38
    "edges_file",
@@ -37,32 +40,32 @@
Loading
37 40
    "edges_type_file",
38 41
    "nodes_type_file",
39 42
    "node_sets_file",
43 +
    "output_dir",
44 +
    "network",
45 +
    "mechanisms_dir",
40 46
}
41 47
42 48
43 -
class Config:
49 +
class Parser:
44 50
    """SONATA network config parser.
45 51
46 52
    See Also:
47 53
        https://github.com/AllenInstitute/sonata/blob/master/docs/SONATA_DEVELOPER_GUIDE.md#network_config
48 54
    """
49 55
50 -
    def __init__(self, config):
51 -
        """Initializes a Config object from a path to the actual config.
56 +
    def __init__(self, config, config_dir):
57 +
        """Initializes a Resolver object.
52 58
53 59
        Args:
54 -
            config (str/dict): Path to the SONATA configuration file or dict containing the config.
60 +
            config (dict): Dict containing the config.
61 +
            config_dir(str):  Path to the directory containing the config file.
55 62
56 63
        Returns:
57 -
             Config: A Config object.
64 +
             Parser: A Parser object.
58 65
        """
59 -
        if isinstance(config, dict):
60 -
            content = config.copy()
61 -
            configdir = None
62 -
        else:
63 -
            configdir = str(Path(config).parent.resolve())
64 -
            content = utils.load_json(str(config))
65 -
        self.manifest = Config._resolve_manifest(content.pop("manifest", {}), configdir)
66 +
        content = config.copy()
67 +
68 +
        self.manifest = Parser._resolve_manifest(content.pop("manifest", {}), config_dir)
66 69
        self.content = content
67 70
68 71
    @staticmethod
@@ -73,8 +76,6 @@
Loading
73 76
            if not isinstance(v, str):
74 77
                raise BluepySnapError(f"{v} should be a string value.")
75 78
            if not Path(v).is_absolute() and not v.startswith("$"):
76 -
                if configdir is None:
77 -
                    raise BluepySnapError("Dictionary config with relative paths is not allowed.")
78 79
                result[k] = str(Path(configdir, v).resolve())
79 80
80 81
        while True:
@@ -110,9 +111,7 @@
Loading
110 111
            return str(Path(*vs))
111 112
        # only way to know if value is a relative path or a normal string
112 113
        elif value.startswith(".") or key in EXPECTED_PATH_KEYS:
113 -
            if self.manifest["${configdir}"] is not None:
114 -
                return str(Path(self.manifest["${configdir}"], value).resolve())
115 -
            raise BluepySnapError("Dictionary config with relative paths is not allowed.")
114 +
            return str(Path(self.manifest["${configdir}"], value).resolve())
116 115
        else:
117 116
            # we cannot know if a string is a path or not if it does not contain anchor or .
118 117
            return value
@@ -132,6 +131,48 @@
Loading
132 131
        return self._resolve(self.content)
133 132
134 133
    @staticmethod
135 -
    def parse(filepath):
134 +
    def parse(config, configdir):
136 135
        """Parse SONATA network config."""
137 -
        return Config(filepath).resolve()
136 +
        return Parser(config, configdir).resolve()
137 +
138 +
139 +
class Config:
140 +
    """Common config class."""
141 +
142 +
    def __init__(self, config, config_class):
143 +
        """Initializes the Config class.
144 +
145 +
        Args:
146 +
            config (str): Path to the configuration file
147 +
            config_class (class): libsonata class corresponding to the configuration file, either
148 +
                libsonata.CircuitConfig or libsonata.SimulationConfig
149 +
        """
150 +
        self._config_dir = str(Path(config).parent.absolute())
151 +
        self._libsonata = config_class.from_file(config)
152 +
153 +
    @property
154 +
    def to_libsonata(self):
155 +
        """Return the libsonata instance of the config."""
156 +
        return self._libsonata
157 +
158 +
    def to_dict(self):
159 +
        """Return the configuration as a dict with absolute paths."""
160 +
        return Parser.parse(json.loads(self._libsonata.expanded_json), self._config_dir)
161 +
162 +
163 +
class CircuitConfig(Config):
164 +
    """Handle CircuitConfig."""
165 +
166 +
    @classmethod
167 +
    def from_config(cls, config_path):
168 +
        """Instantiate the config class from circuit configuration."""
169 +
        return cls(config_path, libsonata.CircuitConfig)
170 +
171 +
172 +
class SimulationConfig(Config):
173 +
    """Handle SimulationConfig."""
174 +
175 +
    @classmethod
176 +
    def from_config(cls, config_path):
177 +
        """Instantiate the config class from simulation configuration."""
178 +
        return cls(config_path, libsonata.SimulationConfig)

@@ -27,23 +27,21 @@
Loading
27 27
class NeuronModelsHelper:
28 28
    """Collection of neuron models related methods."""
29 29
30 -
    def __init__(self, config_components, population):
30 +
    def __init__(self, properties, population):
31 31
        """Constructor.
32 32
33 33
        Args:
34 -
            config_components (dict): 'components' part of circuit's config
34 +
            properties (libsonata.PopulationProperties): properties of the population
35 35
            population (NodePopulation): NodePopulation object used to query the nodes.
36 36
37 37
        Returns:
38 38
            NeuronModelsHelper: A NeuronModelsHelper object.
39 39
        """
40 40
        # all nodes from a population must have the same model type
41 -
        if population.get(0, Node.MODEL_TYPE) not in {"biophysical", "point_neuron"}:
42 -
            raise BluepySnapError(
43 -
                "Neuron models can be only in biophysical or point node population."
44 -
            )
41 +
        if properties.type != "biophysical":
42 +
            raise BluepySnapError("Neuron models can be only in biophysical node population.")
45 43
46 -
        self._config_components = config_components
44 +
        self._properties = properties
47 45
        self._population = population
48 46
49 47
    def get_filepath(self, node_id):
@@ -58,14 +56,7 @@
Loading
58 56
        if not is_node_id(node_id):
59 57
            raise BluepySnapError("node_id must be a int or a CircuitNodeId")
60 58
        node = self._population.get(node_id, [Node.MODEL_TYPE, Node.MODEL_TEMPLATE])
61 -
        if node[Node.MODEL_TYPE] == "biophysical":
62 -
            models_dir = self._config_components.get("biophysical_neuron_models_dir")
63 -
            if models_dir is None:
64 -
                raise BluepySnapError("Missing 'biophysical_neuron_models_dir' in Sonata config")
65 -
        else:
66 -
            models_dir = self._config_components.get("point_neuron_models_dir")
67 -
            if models_dir is None:
68 -
                raise BluepySnapError("Missing 'point_neuron_models_dir' in Sonata config")
59 +
        models_dir = self._properties.biophysical_neuron_models_dir
69 60
70 61
        template = node[Node.MODEL_TEMPLATE]
71 62
        assert ":" in template, "Format of 'model_template' must be <schema>:<resource>."

@@ -19,7 +19,7 @@
Loading
19 19
20 20
from bluepysnap.sonata_constants import DYNAMICS_PREFIX, Edge, Node
21 21
22 -
NODE_TYPES = {"biophysical", "virtual", "astrocyte", "single_compartment", "point_neuron"}
22 +
NODE_TYPES = {"biophysical", "virtual", "astrocyte", "single_compartment"}
23 23
EDGE_TYPES = {"chemical", "electrical", "synapse_astrocyte", "endfoot"}
24 24
25 25

@@ -28,40 +28,25 @@
Loading
28 28
class NetworkObject(abc.ABC):
29 29
    """Abstract class for the top level NetworkObjects accessor."""
30 30
31 +
    _population_class = None
32 +
31 33
    def __init__(self, circuit):
32 34
        """Initialize the top level NetworkObjects accessor."""
33 35
        self._circuit = circuit
34 36
35 -
    def _get_populations(self, cls, config):
37 +
    def _get_populations(self, cls):
36 38
        """Collects the different NetworkObjectPopulation and returns them as a dict."""
37 -
        res = {}
38 -
        for file_config in config:
39 -
            storage = cls(file_config, self._circuit)
40 -
            for population in storage.population_names:  # pylint: disable=not-an-iterable
41 -
                if population in res:
42 -
                    raise BluepySnapError(
43 -
                        f"Duplicated {self.__class__.__name__} population: '{population}'"
44 -
                    )
45 -
                res[population] = storage.population(population)
46 -
        return res
47 -
48 -
    @abc.abstractmethod
49 -
    def _collect_populations(self):
50 -
        """Should specify the self._get_populations arguments."""
51 -
52 -
    @cached_property
53 -
    def _config(self):
54 -
        return self._circuit.config
39 +
        return {name: cls(self._circuit, name) for name in self.population_names}
55 40
56 41
    @cached_property
57 42
    def _populations(self):
58 43
        """Cached population dictionary."""
59 -
        return self._collect_populations()
44 +
        return self._get_populations(self._population_class)
60 45
61 46
    @cached_property
47 +
    @abc.abstractmethod
62 48
    def population_names(self):
63 -
        """Returns all the sorted NetworkObjects population names from the Circuit."""
64 -
        return sorted(self._populations)
49 +
        """Should define all sorted NetworkObjects population names from the Circuit."""
65 50
66 51
    @cached_property
67 52
    def property_dtypes(self):
@@ -70,7 +55,7 @@
Loading
70 55
        def _update(d, index, value):
71 56
            if d.setdefault(index, value) != value:
72 57
                raise BluepySnapError(
73 -
                    "Same property with different " f"dtype. {index}: {value}!= {d[index]}"
58 +
                    f"Same property with different dtype. {index}: {value}!= {d[index]}"
74 59
                )
75 60
76 61
        res = {}

@@ -18,30 +18,11 @@
Loading
18 18
19 19
from cached_property import cached_property
20 20
21 -
from bluepysnap.config import Config
21 +
from bluepysnap.config import SimulationConfig
22 22
from bluepysnap.exceptions import BluepySnapError
23 23
from bluepysnap.node_sets import NodeSets
24 24
25 25
26 -
def _resolve_config(config):
27 -
    """Resolve the config file if global ('network' and 'simulation' keys).
28 -
29 -
    Args:
30 -
        config (str/dict): the path to the configuration file or a dict containing the config.
31 -
32 -
    Returns:
33 -
        dict: the complete simulation config file.
34 -
    """
35 -
    content = Config(config).resolve()
36 -
    if "simulation" in content and "network" in content:
37 -
        simulation_path = content["simulation"]
38 -
        res = Config(str(simulation_path)).resolve()
39 -
        if "network" not in res:
40 -
            res["network"] = str(content["network"])
41 -
        return res
42 -
    return content
43 -
44 -
45 26
def _collect_frame_reports(sim):
46 27
    """Collect the different frame reports."""
47 28
    res = {}
@@ -57,6 +38,7 @@
Loading
57 38
            cls = CompartmentReport
58 39
        else:
59 40
            raise BluepySnapError(f"Report {name}: format {report_type} not yet supported.")
41 +
60 42
        res[name] = cls(sim, name)
61 43
    return res
62 44
@@ -73,26 +55,26 @@
Loading
73 55
        Returns:
74 56
            Simulation: A Simulation object.
75 57
        """
76 -
        self._config = _resolve_config(config)
58 +
        self._config = SimulationConfig.from_config(config)
77 59
78 60
    @property
79 61
    def config(self):
80 62
        """Simulation config dictionary."""
81 -
        return self._config
63 +
        return self._config.to_dict()
82 64
83 65
    @cached_property
84 66
    def circuit(self):
85 67
        """Access to the circuit used for the simulation."""
86 68
        from bluepysnap.circuit import Circuit
87 69
88 -
        if "network" not in self._config:
70 +
        if "network" not in self.config:
89 71
            raise BluepySnapError("No 'network' set in the simulation/global config file.")
90 -
        return Circuit(self._config["network"])
72 +
        return Circuit(self.config["network"])
91 73
92 74
    @property
93 75
    def run(self):
94 76
        """Access to the complete run dictionary for this simulation."""
95 -
        return self._config["run"]
77 +
        return self.config["run"]
96 78
97 79
    @property
98 80
    def time_start(self):
@@ -118,18 +100,18 @@
Loading
118 100
    @property
119 101
    def conditions(self):
120 102
        """Access to the conditions dictionary for this simulation."""
121 -
        return self._config.get("conditions", {})
103 +
        return self.config.get("conditions", {})
122 104
123 105
    @property
124 106
    def simulator(self):
125 107
        """Returns the targeted simulator."""
126 -
        return self._config["target_simulator"]
108 +
        return self.config.get("target_simulator")
127 109
128 110
    @cached_property
129 111
    def node_sets(self):
130 112
        """Returns the NodeSets object bound to the simulation."""
131 -
        if "node_sets_file" in self._config:
132 -
            return NodeSets(self._config["node_sets_file"])
113 +
        if "node_sets_file" in self.config:
114 +
            return NodeSets(self.config["node_sets_file"])
133 115
        return {}
134 116
135 117
    @cached_property

@@ -18,7 +18,6 @@
Loading
18 18
"""Edge population access."""
19 19
import inspect
20 20
from collections.abc import Mapping
21 -
from copy import deepcopy
22 21
23 22
import libsonata
24 23
import numpy as np
@@ -31,597 +30,614 @@
Loading
31 30
from bluepysnap.circuit_ids import CircuitEdgeId, CircuitEdgeIds, CircuitNodeId, CircuitNodeIds
32 31
from bluepysnap.exceptions import BluepySnapError
33 32
from bluepysnap.network import NetworkObject
34 -
from bluepysnap.sonata_constants import DEFAULT_EDGE_TYPE, DYNAMICS_PREFIX, ConstContainer, Edge
33 +
from bluepysnap.sonata_constants import DYNAMICS_PREFIX, ConstContainer, Edge
35 34
from bluepysnap.utils import IDS_DTYPE, Deprecate
36 35
37 36
38 -
class Edges(
39 -
    NetworkObject,
40 -
    metaclass=AbstractDocSubstitutionMeta,
41 -
    source_word="NetworkObject",
42 -
    target_word="Edge",
43 -
):
44 -
    """The top level Edges accessor."""
37 +
def _is_empty(xs):
38 +
    return (xs is not None) and (len(xs) == 0)
45 39
46 -
    def __init__(self, circuit):  # pylint: disable=useless-super-delegation
47 -
        """Initialize the top level Edges accessor."""
48 -
        super().__init__(circuit)
49 40
50 -
    def _collect_populations(self):
51 -
        return self._get_populations(EdgeStorage, self._config["networks"]["edges"])
41 +
def _estimate_range_size(func, node_ids, n=3):
42 +
    """Median size of index second level for some node IDs from the provided list."""
43 +
    assert len(node_ids) > 0
44 +
    if len(node_ids) > n:
45 +
        node_ids = np.random.choice(node_ids, size=n, replace=False)
46 +
    return np.median([len(func(node_id).ranges) for node_id in node_ids])
47 +
52 48
53 -
    def ids(self, group=None, sample=None, limit=None):
54 -
        """Edge CircuitEdgeIds corresponding to edges ``edge_ids``.
49 +
class EdgePopulation:
50 +
    """Edge population access."""
51 +
52 +
    def __init__(self, circuit, population_name):
53 +
        """Initializes a EdgePopulation object from a EdgeStorage and a population name.
55 54
56 55
        Args:
57 -
            group (None/int/CircuitEdgeId/CircuitEdgeIds/sequence): Which IDs will be
58 -
            returned depends on the type of the ``group`` argument:
59 -
                - ``None``: return all CircuitEdgeIds.
60 -
                - ``CircuitEdgeId``: return the ID in a CircuitEdgeIds object.
61 -
                - ``CircuitEdgeIds``: return the IDs in a CircuitNodeIds object.
62 -
                - ``int``: returns a CircuitEdgeIds object containing the corresponding edge ID
63 -
                    for all populations.
64 -
                - ``sequence``: returns a CircuitEdgeIds object containing the corresponding edge
65 -
                    IDs for all populations.
66 -
            sample (int): If specified, randomly choose ``sample`` number of
67 -
                IDs from the match result. If the size of the sample is greater than
68 -
                the size of all the EdgePopulations then all ids are taken and shuffled.
69 -
            limit (int): If specified, return the first ``limit`` number of
70 -
                IDs from the match result. If limit is greater than the size of all the populations
71 -
                all node IDs are returned.
56 +
            circuit (bluepysnap.Circuit): the circuit object containing the edge population
57 +
            population_name (str): the name of the edge population
72 58
73 59
        Returns:
74 -
            CircuitEdgeIds: returns a CircuitEdgeIds containing all the edge IDs and the
75 -
                corresponding populations. For performance reasons we do not test if the edge ids
76 -
                are present or not in the circuit.
77 -
78 -
        Notes:
79 -
            This envision also the maybe future selection of edges on queries.
60 +
            EdgePopulation: An EdgePopulation object.
80 61
        """
81 -
        if isinstance(group, CircuitEdgeIds):
82 -
            diff = np.setdiff1d(group.get_populations(unique=True), self.population_names)
83 -
            if diff.size != 0:
84 -
                raise BluepySnapError(f"Population {diff} does not exist in the circuit.")
85 -
        fun = lambda x: (x.ids(group), x.name)
86 -
        return self._get_ids_from_pop(fun, CircuitEdgeIds, sample=sample, limit=limit)
62 +
        self._circuit = circuit
63 +
        self.name = population_name
87 64
88 -
    def get(self, edge_ids=None, properties=None):  # pylint: disable=arguments-renamed
89 -
        """Edge properties as pandas DataFrame.
65 +
    @property
66 +
    def _properties(self):
67 +
        return self._circuit.to_libsonata.edge_population_properties(self.name)
90 68
91 -
        Args:
92 -
            edge_ids (int/CircuitEdgeId/CircuitEdgeIds/sequence): same as Edges.ids().
93 -
            properties (None/str/list): an edge property name or a list of edge property names.
94 -
                If set to None ids are returned.
69 +
    @property
70 +
    def _population(self):
71 +
        return self._circuit.to_libsonata.edge_population(self.name)
95 72
96 -
        Returns:
97 -
            pandas.Series/pandas.DataFrame:
98 -
                A pandas Series indexed by edge IDs if ``properties`` is scalar.
99 -
                A pandas DataFrame indexed by edge IDs if ``properties`` is list.
73 +
    @staticmethod
74 +
    def _resolve_node_ids(nodes, group):
75 +
        """Node IDs corresponding to node group filter."""
76 +
        if group is None:
77 +
            return None
78 +
        return nodes.ids(group)
100 79
101 -
        Notes:
102 -
            The Edges.property_names function will give you all the usable properties
103 -
            for the `properties` argument.
104 -
        """
105 -
        if edge_ids is None:
106 -
            raise BluepySnapError("You need to set edge_ids in get.")
107 -
        if properties is None:
108 -
            Deprecate.warn(
109 -
                "Returning ids with get/properties is deprecated and will be removed in 1.0.0. "
110 -
                "Please use Edges.ids instead."
111 -
            )
112 -
            return edge_ids
113 -
        return super().get(edge_ids, properties)
80 +
    @property
81 +
    def size(self):
82 +
        """Population size."""
83 +
        return self._population.size
114 84
115 -
    def properties(self, edge_ids, properties):
116 -
        """Doc is overridden below."""
117 -
        Deprecate.warn(
118 -
            "Edges.properties function is deprecated and will be removed in 1.0.0. "
119 -
            "Please use Edges.get instead."
120 -
        )
121 -
        return self.get(edge_ids, properties)
85 +
    def _nodes(self, population_name):
86 +
        """Returns the NodePopulation corresponding to population."""
87 +
        result = self._circuit.nodes[population_name]
88 +
        return result
122 89
123 -
    properties.__doc__ = get.__doc__
90 +
    @property
91 +
    def type(self):
92 +
        """Population type."""
93 +
        return self._properties.type
124 94
125 -
    def afferent_nodes(self, target, unique=True):
126 -
        """Get afferent CircuitNodeIDs for given target ``node_id``.
95 +
    @cached_property
96 +
    def source(self):
97 +
        """Source NodePopulation."""
98 +
        return self._nodes(self._population.source)
127 99
128 -
        Notes:
129 -
            Afferent nodes are nodes projecting an outgoing edge to one of the ``target`` node.
100 +
    @cached_property
101 +
    def target(self):
102 +
        """Target NodePopulation."""
103 +
        return self._nodes(self._population.target)
130 104
131 -
        Args:
132 -
            target (CircuitNodeIds/int/sequence/str/mapping/None): the target you want to resolve
133 -
            and use as target nodes.
134 -
            unique (bool): If ``True``, return only unique afferent node IDs.
105 +
    @cached_property
106 +
    def _attribute_names(self):
107 +
        return set(self._population.attribute_names)
135 108
136 -
        Returns:
137 -
            CircuitNodeIDs: Afferent CircuitNodeIDs for all the targets from all edge population.
138 -
        """
139 -
        target_ids = self._circuit.nodes.ids(target)
140 -
        result = self._get_ids_from_pop(
141 -
            lambda x: (x.afferent_nodes(target_ids), x.source.name), CircuitNodeIds
142 -
        )
143 -
        if unique:
144 -
            result.unique(inplace=True)
145 -
        return result
109 +
    @cached_property
110 +
    def _dynamics_params_names(self):
111 +
        return set(utils.add_dynamic_prefix(self._population.dynamics_attribute_names))
146 112
147 -
    def efferent_nodes(self, source, unique=True):
148 -
        """Get efferent node IDs for given source ``node_id``.
113 +
    @property
114 +
    def _topology_property_names(self):
115 +
        return {Edge.SOURCE_NODE_ID, Edge.TARGET_NODE_ID}
116 +
117 +
    @property
118 +
    def property_names(self):
119 +
        """Set of available edge properties.
149 120
150 121
        Notes:
151 -
            Efferent nodes are nodes receiving an incoming edge from one of the ``source`` node.
122 +
            Properties are a combination of the group attributes, the dynamics_params and the
123 +
            topology properties.
124 +
        """
125 +
        return self._attribute_names | self._dynamics_params_names | self._topology_property_names
152 126
153 -
        Args:
154 -
            source (CircuitNodeIds/int/sequence/str/mapping/None): the source you want to resolve
155 -
                and use as source nodes.
156 -
            unique (bool): If ``True``, return only unique afferent node IDs.
127 +
    @cached_property
128 +
    def property_dtypes(self):
129 +
        """Returns the dtypes of all the properties.
157 130
158 131
        Returns:
159 -
            numpy.ndarray: Efferent node IDs for all the sources.
132 +
            pandas.Series: series indexed by field name with the corresponding dtype as value.
160 133
        """
161 -
        source_ids = self._circuit.nodes.ids(source)
162 -
        result = self._get_ids_from_pop(
163 -
            lambda x: (x.efferent_nodes(source_ids), x.target.name), CircuitNodeIds
164 -
        )
165 -
        if unique:
166 -
            result.unique(inplace=True)
167 -
        return result
134 +
        return self.get([0], list(self.property_names)).dtypes.sort_index()
168 135
169 -
    def pathway_edges(self, source=None, target=None, properties=None):
170 -
        """Get edges corresponding to ``source`` -> ``target`` connections.
136 +
    def container_property_names(self, container):
137 +
        """Lists the ConstContainer properties shared with the EdgePopulation.
171 138
172 139
        Args:
173 -
            source: source node group
174 -
            target: target node group
175 -
            properties: None / edge property name / list of edge property names
140 +
            container (ConstContainer): a container class for edge properties.
176 141
177 142
        Returns:
178 -
            CircuitEdgeIDs, if ``properties`` is None;
179 -
            Pandas Series indexed by CircuitEdgeIDs if ``properties`` is string;
180 -
            Pandas DataFrame indexed by CircuitEdgeIDs if ``properties`` is list.
181 -
        """
182 -
        if source is None and target is None:
183 -
            raise BluepySnapError("Either `source` or `target` should be specified")
184 -
185 -
        source_ids = self._circuit.nodes.ids(source)
186 -
        target_ids = self._circuit.nodes.ids(target)
143 +
            list: A list of strings corresponding to the properties that you can use from the
144 +
                container class
187 145
188 -
        result = self._get_ids_from_pop(
189 -
            lambda x: (x.pathway_edges(source_ids, target_ids), x.name), CircuitEdgeIds
190 -
        )
146 +
        Examples:
147 +
            >>> from bluepysnap.sonata_constants import Edge
148 +
            >>> print(my_edge_population.container_property_names(Edge))
149 +
            >>> ["AXONAL_DELAY", "SYN_WEIGHT"] # values you can use with my_edge_population
150 +
        """
151 +
        if not inspect.isclass(container) or not issubclass(container, ConstContainer):
152 +
            raise BluepySnapError("'container' must be a subclass of ConstContainer")
153 +
        in_file = self.property_names
154 +
        return [k for k in container.key_set() if container.get(k) in in_file]
191 155
192 -
        if properties:
193 -
            return self.get(result, properties)
156 +
    def _get_property(self, prop, selection):
157 +
        if prop == Edge.SOURCE_NODE_ID:
158 +
            result = utils.ensure_ids(self._population.source_nodes(selection))
159 +
        elif prop == Edge.TARGET_NODE_ID:
160 +
            result = utils.ensure_ids(self._population.target_nodes(selection))
161 +
        elif prop in self._attribute_names:
162 +
            result = self._population.get_attribute(prop, selection)
163 +
        elif prop in self._dynamics_params_names:
164 +
            result = self._population.get_dynamics_attribute(
165 +
                prop.split(DYNAMICS_PREFIX)[1], selection
166 +
            )
167 +
        else:
168 +
            raise BluepySnapError(f"No such property: {prop}")
194 169
        return result
195 170
196 -
    def afferent_edges(self, node_id, properties=None):
197 -
        """Get afferent edges for given ``node_id``.
171 +
    def _get(self, selection, properties=None):
172 +
        """Get an array of edge IDs or DataFrame with edge properties."""
173 +
        edge_ids = utils.ensure_ids(selection.flatten())
174 +
        if properties is None:
175 +
            Deprecate.warn(
176 +
                "Returning ids with get/properties is deprecated and will be removed in 1.0.0. "
177 +
                "Please use EdgePopulation.ids instead."
178 +
            )
179 +
            return edge_ids
198 180
199 -
        Args:
200 -
            node_id (int): Target node ID.
201 -
            properties: An edge property name, a list of edge property names, or None.
181 +
        if utils.is_iterable(properties):
182 +
            if len(edge_ids) == 0:
183 +
                result = pd.DataFrame(columns=properties)
184 +
            else:
185 +
                result = pd.DataFrame(index=edge_ids)
186 +
                for p in properties:
187 +
                    result[p] = self._get_property(p, selection)
188 +
        else:
189 +
            if len(edge_ids) == 0:
190 +
                result = pd.Series(name=properties, dtype=np.float64)
191 +
            else:
192 +
                result = pd.Series(
193 +
                    self._get_property(properties, selection), index=edge_ids, name=properties
194 +
                )
202 195
203 -
        Returns:
204 -
            pandas.Series/pandas.DataFrame/list:
205 -
                A pandas Series indexed by edge ID if ``properties`` is a string.
206 -
                A pandas DataFrame indexed by edge ID if ``properties`` is a list.
207 -
                A list of edge IDs, if ``properties`` is None.
208 -
        """
209 -
        return self.pathway_edges(source=None, target=node_id, properties=properties)
196 +
        return result
210 197
211 -
    def efferent_edges(self, node_id, properties=None):
212 -
        """Get efferent edges for given ``node_id``.
198 +
    def _edge_ids_by_filter(self, queries, raise_missing_prop):
199 +
        """Return edge IDs if their properties match the `queries` dict.
213 200
214 -
        Args:
215 -
            node_id: source node ID
216 -
            properties: None / edge property name / list of edge property names
201 +
        `props` values could be:
202 +
            pairs (range match for floating dtype fields)
203 +
            scalar or iterables (exact or "one of" match for other fields)
204 +
205 +
        You can use the special operators '$or' and '$and' also to combine different queries
206 +
        together.
207 +
208 +
        Examples:
209 +
            >>> self._edge_ids_by_filter({ Edge.POST_SECTION_ID: (0, 1),
210 +
            >>>                            Edge.AXONAL_DELAY: (.5, 2.) })
211 +
            >>> self._edge_ids_by_filter({'$or': [{ Edge.PRE_X_CENTER: [2, 3]},
212 +
            >>>                              { Edge.POST_SECTION_POS: (0, 1),
213 +
            >>>                              Edge.SYN_WEIGHT: (0.,1.4) }]})
217 214
218 -
        Returns:
219 -
            List of edge IDs, if ``properties`` is None;
220 -
            Pandas Series indexed by edge IDs if ``properties`` is string;
221 -
            Pandas DataFrame indexed by edge IDs if ``properties`` is list.
222 215
        """
223 -
        return self.pathway_edges(source=node_id, target=None, properties=properties)
216 +
        properties = query.get_properties(queries)
217 +
        unknown_props = properties - self.property_names
218 +
        if raise_missing_prop and unknown_props:
219 +
            raise BluepySnapError(f"Unknown edge properties: {unknown_props}")
220 +
        res = []
221 +
        ids = self.ids(None)
222 +
        chunk_size = int(1e8)
223 +
        for chunk in np.array_split(ids, 1 + len(ids) // chunk_size):
224 +
            data = self.get(chunk, properties - unknown_props)
225 +
            res.extend(chunk[query.resolve_ids(data, self.name, queries)])
226 +
        return np.array(res, dtype=IDS_DTYPE)
224 227
225 -
    def pair_edges(self, source_node_id, target_node_id, properties=None):
226 -
        """Get edges corresponding to ``source_node_id`` -> ``target_node_id`` connection.
228 +
    def ids(self, group=None, limit=None, sample=None, raise_missing_property=True):
229 +
        """Edge IDs corresponding to edges ``edge_ids``.
227 230
228 231
        Args:
229 -
            source_node_id: source node ID
230 -
            target_node_id: target node ID
231 -
            properties: None / edge property name / list of edge property names
232 +
            group (None/int/CircuitEdgeId/CircuitEdgeIds/sequence): Which IDs will be
233 +
                returned depends on the type of the ``group`` argument:
234 +
                - ``None``: return all IDs.
235 +
                - ``int``, ``CircuitEdgeId``: return a single edge ID.
236 +
                - ``CircuitEdgeIds`` return IDs of edges the edge population in an array.
237 +
                - ``sequence``: return IDs of edges in an array.
238 +
239 +
            sample (int): If specified, randomly choose ``sample`` number of
240 +
                IDs from the match result. If the size of the sample is greater than
241 +
                the size of the EdgePopulation then all ids are taken and shuffled.
242 +
243 +
            limit (int): If specified, return the first ``limit`` number of
244 +
                IDs from the match result. If limit is greater than the size of the population
245 +
                all node IDs are returned.
246 +
247 +
            raise_missing_property (bool): if True, raises if a property is not listed in this
248 +
                population. Otherwise the ids are just not selected if a property is missing.
232 249
233 250
        Returns:
234 -
            List of edge IDs, if ``properties`` is None;
235 -
            Pandas Series indexed by edge IDs if ``properties`` is string;
236 -
            Pandas DataFrame indexed by edge IDs if ``properties`` is list.
251 +
            numpy.array: A numpy array of IDs.
237 252
        """
238 -
        return self.pathway_edges(
239 -
            source=source_node_id, target=target_node_id, properties=properties
240 -
        )
253 +
        if group is None:
254 +
            result = self._population.select_all().flatten()
255 +
        elif isinstance(group, CircuitEdgeIds):
256 +
            result = group.filter_population(self.name).get_ids()
257 +
        elif isinstance(group, np.ndarray):
258 +
            result = group
259 +
        elif isinstance(group, Mapping):
260 +
            result = self._edge_ids_by_filter(
261 +
                queries=group, raise_missing_prop=raise_missing_property
262 +
            )
263 +
        else:
264 +
            result = utils.ensure_list(group)
265 +
            # test if first value is a CircuitEdgeId if yes then all values must be CircuitEdgeId
266 +
            if isinstance(first(result, None), CircuitEdgeId):
267 +
                try:
268 +
                    result = [cid.id for cid in result if cid.population == self.name]
269 +
                except AttributeError as e:
270 +
                    raise BluepySnapError(
271 +
                        "All values from a list must be of type int or CircuitEdgeId."
272 +
                    ) from e
273 +
        if sample is not None:
274 +
            if len(result) > 0:
275 +
                result = np.random.choice(result, min(sample, len(result)), replace=False)
276 +
        if limit is not None:
277 +
            result = result[:limit]
278 +
        return utils.ensure_ids(result)
241 279
242 -
    @staticmethod
243 -
    def _add_circuit_ids(its, source, target):
244 -
        """Generator comprehension adding the CircuitIds to the iterator.
280 +
    def get(self, edge_ids, properties):
281 +
        """Edge properties as pandas DataFrame.
282 +
283 +
        Args:
284 +
            edge_ids (array-like): array-like of edge IDs
285 +
            properties (str/list): an edge property name or a list of edge property names
286 +
287 +
        Returns:
288 +
            pandas.Series/pandas.DataFrame:
289 +
                A pandas Series indexed by edge IDs if ``properties`` is scalar.
290 +
                A pandas DataFrame indexed by edge IDs if ``properties`` is list.
245 291
246 292
        Notes:
247 -
            Using closures or lambda functions would result in override functions and so the
248 -
            source and target would be the same for all the populations.
293 +
            The EdgePopulation.property_names function will give you all the usable properties
294 +
            for the `properties` argument.
249 295
        """
250 -
        return (
251 -
            (CircuitNodeId(source, source_id), CircuitNodeId(target, target_id), count)
252 -
            for source_id, target_id, count in its
253 -
        )
296 +
        edge_ids = self.ids(edge_ids)
297 +
        selection = libsonata.Selection(edge_ids)
298 +
        return self._get(selection, properties)
254 299
255 -
    @staticmethod
256 -
    def _add_edge_ids(its, source, target, pop_name):
257 -
        """Generator comprehension adding the CircuitIds to the iterator."""
258 -
        return (
259 -
            (
260 -
                CircuitNodeId(source, source_id),
261 -
                CircuitNodeId(target, target_id),
262 -
                CircuitEdgeIds.from_dict({pop_name: edge_id}),
263 -
            )
264 -
            for source_id, target_id, edge_id in its
300 +
    def properties(self, edge_ids, properties):
301 +
        """Doc is overridden below."""
302 +
        Deprecate.warn(
303 +
            "EdgePopulation.properties function is deprecated and will be removed in 1.0.0. "
304 +
            "Please use EdgePopulation.get instead."
265 305
        )
306 +
        return self.get(edge_ids, properties)
266 307
267 -
    @staticmethod
268 -
    def _omit_edge_count(its, source, target):
269 -
        """Generator comprehension adding the CircuitIds to the iterator."""
270 -
        return (
271 -
            (CircuitNodeId(source, source_id), CircuitNodeId(target, target_id))
272 -
            for source_id, target_id in its
273 -
        )
308 +
    properties.__doc__ = get.__doc__
274 309
275 -
    def iter_connections(
276 -
        self, source=None, target=None, return_edge_ids=False, return_edge_count=False
277 -
    ):
278 -
        """Iterate through ``source`` -> ``target`` connections.
310 +
    def positions(self, edge_ids, side, kind):
311 +
        """Edge positions as a pandas DataFrame.
279 312
280 313
        Args:
281 -
            source (CircuitNodeIds/int/sequence/str/mapping/None): source node group
282 -
            target (CircuitNodeIds/int/sequence/str/mapping/None): target node group
283 -
            return_edge_count: if True, edge count is added to yield result
284 -
            return_edge_ids: if True, edge ID list is added to yield result
285 -
286 -
        ``return_edge_count`` and ``return_edge_ids`` are mutually exclusive.
314 +
            edge_ids (array-like): array-like of edge IDs
315 +
            side (str): ``afferent`` or ``efferent``
316 +
            kind (str): ``center`` or ``surface``
287 317
288 -
        Yields:
289 -
            (source_node_id, target_node_id, edge_ids) if return_edge_ids == True;
290 -
            (source_node_id, target_node_id, edge_count) if return_edge_count == True;
291 -
            (source_node_id, target_node_id) otherwise.
318 +
        Returns:
319 +
            Pandas Dataframe with ('x', 'y', 'z') columns indexed by edge IDs.
292 320
        """
293 -
        if return_edge_ids and return_edge_count:
294 -
            raise BluepySnapError(
295 -
                "`return_edge_count` and `return_edge_ids` are mutually exclusive"
296 -
            )
297 -
        for name, pop in self.items():
298 -
            it = pop.iter_connections(
299 -
                source=source,
300 -
                target=target,
301 -
                return_edge_ids=return_edge_ids,
302 -
                return_edge_count=return_edge_count,
303 -
            )
304 -
            source_pop = pop.source.name
305 -
            target_pop = pop.target.name
306 -
            if return_edge_count:
307 -
                yield from self._add_circuit_ids(it, source_pop, target_pop)
308 -
            elif return_edge_ids:
309 -
                yield from self._add_edge_ids(it, source_pop, target_pop, name)
310 -
            else:
311 -
                yield from self._omit_edge_count(it, source_pop, target_pop)
312 -
321 +
        assert side in ("afferent", "efferent")
322 +
        assert kind in ("center", "surface")
323 +
        props = {f"{side}_{kind}_{p}": p for p in ["x", "y", "z"]}
324 +
        result = self.get(edge_ids, list(props))
325 +
        result.rename(columns=props, inplace=True)
326 +
        result.sort_index(axis=1, inplace=True)
327 +
        return result
313 328
314 -
class EdgeStorage:
315 -
    """Edge storage access."""
329 +
    def afferent_nodes(self, target, unique=True):
330 +
        """Get afferent node IDs for given target ``node_id``.
316 331
317 -
    def __init__(self, config, circuit):
318 -
        """Initializes a EdgeStorage object from a edge config and a Circuit.
332 +
        Notes:
333 +
            Afferent nodes are nodes projecting an outgoing edge to one of the ``target`` node.
319 334
320 335
        Args:
321 -
            config (dict): a edge config from the global circuit config
322 -
            circuit (bluepysnap.Circuit): the circuit object that contains the EdgePopulations
323 -
            from this storage.
336 +
            target (CircuitNodeIds/int/sequence/str/mapping/None): the target you want to resolve
337 +
            and use as target nodes.
338 +
            unique (bool): If ``True``, return only unique afferent node IDs.
324 339
325 340
        Returns:
326 -
            EdgeStorage: A EdgeStorage object.
341 +
            numpy.ndarray: Afferent node IDs for all the targets.
327 342
        """
328 -
        self._h5_filepath = config["edges_file"]
329 -
        self._csv_filepath = config.get("edge_types_file")
330 -
        self._populations_config = config.get("populations", {})
331 -
        self._circuit = circuit
332 -
        self._populations = {}
333 -
334 -
    @property
335 -
    def storage(self):
336 -
        """Access to the libsonata edge storage."""
337 -
        return libsonata.EdgeStorage(self._h5_filepath)
343 +
        if target is not None:
344 +
            selection = self._population.afferent_edges(self._resolve_node_ids(self.target, target))
345 +
        else:
346 +
            selection = self._population.select_all()
347 +
        result = self._population.source_nodes(selection)
348 +
        if unique:
349 +
            result = np.unique(result)
350 +
        return utils.ensure_ids(result)
338 351
339 -
    @cached_property
340 -
    def population_names(self):
341 -
        """Returns all population names inside this file."""
342 -
        return self.storage.population_names
352 +
    def efferent_nodes(self, source, unique=True):
353 +
        """Get efferent node IDs for given source ``node_id``.
343 354
344 -
    @property
345 -
    def h5_filepath(self):
346 -
        """Returns the filepath of the Storage."""
347 -
        return self._h5_filepath
355 +
        Notes:
356 +
            Efferent nodes are nodes receiving an incoming edge from one of the ``source`` node.
348 357
349 -
    @property
350 -
    def csv_filepath(self):
351 -
        """Returns the csv filepath of the Storage."""
352 -
        return self._csv_filepath
358 +
        Args:
359 +
            source (CircuitNodeIds/int/sequence/str/mapping/None): the source you want to resolve
360 +
                and use as source nodes.
361 +
            unique (bool): If ``True``, return only unique afferent node IDs.
353 362
354 -
    @property
355 -
    def circuit(self):
356 -
        """Returns the circuit object containing this storage."""
357 -
        return self._circuit
363 +
        Returns:
364 +
            numpy.ndarray: Efferent node IDs for all the sources.
365 +
        """
366 +
        if source is not None:
367 +
            selection = self._population.efferent_edges(self._resolve_node_ids(self.source, source))
368 +
        else:
369 +
            selection = self._population.select_all()
370 +
        result = self._population.target_nodes(selection)
371 +
        if unique:
372 +
            result = np.unique(result)
373 +
        return utils.ensure_ids(result)
358 374
359 -
    def population(self, population_name):
360 -
        """Access the different populations from the storage."""
361 -
        if population_name not in self._populations:
362 -
            population_config = self._populations_config.get(population_name, {})
375 +
    def pathway_edges(self, source=None, target=None, properties=None):
376 +
        """Get edges corresponding to ``source`` -> ``target`` connections.
363 377
364 -
            self._populations[population_name] = EdgePopulation(
365 -
                self, population_name, population_config
366 -
            )
378 +
        Args:
379 +
            source (CircuitNodeIds/int/sequence/str/mapping/None): source node group
380 +
            target (CircuitNodeIds/int/sequence/str/mapping/None): target node group
381 +
            properties: None / edge property name / list of edge property names
367 382
368 -
        return self._populations[population_name]
383 +
        Returns:
384 +
            List of edge IDs, if ``properties`` is None;
385 +
            Pandas Series indexed by edge IDs if ``properties`` is string;
386 +
            Pandas DataFrame indexed by edge IDs if ``properties`` is list.
387 +
        """
388 +
        if source is None and target is None:
389 +
            raise BluepySnapError("Either `source` or `target` should be specified")
369 390
391 +
        source_node_ids = self._resolve_node_ids(self.source, source)
392 +
        target_edge_ids = self._resolve_node_ids(self.target, target)
370 393
371 -
def _is_empty(xs):
372 -
    return (xs is not None) and (len(xs) == 0)
394 +
        if source_node_ids is None:
395 +
            selection = self._population.afferent_edges(target_edge_ids)
396 +
        elif target_edge_ids is None:
397 +
            selection = self._population.efferent_edges(source_node_ids)
398 +
        else:
399 +
            selection = self._population.connecting_edges(source_node_ids, target_edge_ids)
373 400
401 +
        if properties:
402 +
            return self._get(selection, properties)
403 +
        return utils.ensure_ids(selection.flatten())
374 404
375 -
def _estimate_range_size(func, node_ids, n=3):
376 -
    """Median size of index second level for some node IDs from the provided list."""
377 -
    assert len(node_ids) > 0
378 -
    if len(node_ids) > n:
379 -
        node_ids = np.random.choice(node_ids, size=n, replace=False)
380 -
    return np.median([len(func(node_id).ranges) for node_id in node_ids])
381 -
382 -
383 -
class EdgePopulation:
384 -
    """Edge population access."""
385 -
386 -
    def __init__(self, edge_storage, population_name, population_config=None):
387 -
        """Initializes a EdgePopulation object from a EdgeStorage and a population name.
405 +
    def afferent_edges(self, node_id, properties=None):
406 +
        """Get afferent edges for given ``node_id``.
388 407
389 408
        Args:
390 -
            edge_storage (EdgeStorage): the edge storage containing the edge population
391 -
            population_name (str): the name of the edge population
392 -
            population_config (dict): the config for the population
409 +
            node_id (CircuitNodeIds/int/sequence/str/mapping/None) : Target node ID.
410 +
            properties: An edge property name, a list of edge property names, or None.
393 411
394 412
        Returns:
395 -
            EdgePopulation: An EdgePopulation object.
413 +
            pandas.Series/pandas.DataFrame/list:
414 +
                A pandas Series indexed by edge ID if ``properties`` is a string.
415 +
                A pandas DataFrame indexed by edge ID if ``properties`` is a list.
416 +
                A list of edge IDs, if ``properties`` is None.
396 417
        """
397 -
        self._config = population_config or {}
398 -
        self._edge_storage = edge_storage
399 -
        self.name = population_name
400 -
401 -
    @cached_property
402 -
    def _population(self):
403 -
        return self._edge_storage.storage.open_population(self.name)
418 +
        return self.pathway_edges(source=None, target=node_id, properties=properties)
404 419
405 -
    @staticmethod
406 -
    def _resolve_node_ids(nodes, group):
407 -
        """Node IDs corresponding to node group filter."""
408 -
        if group is None:
409 -
            return None
410 -
        return nodes.ids(group)
420 +
    def efferent_edges(self, node_id, properties=None):
421 +
        """Get efferent edges for given ``node_id``.
411 422
412 -
    @property
413 -
    def size(self):
414 -
        """Population size."""
415 -
        return self._population.size
423 +
        Args:
424 +
            node_id (CircuitNodeIds/int/sequence/str/mapping/None): source node ID
425 +
            properties: None / edge property name / list of edge property names
416 426
417 -
    def _nodes(self, population_name):
418 -
        """Returns the NodePopulation corresponding to population."""
419 -
        result = self._edge_storage.circuit.nodes[population_name]
420 -
        return result
427 +
        Returns:
428 +
            List of edge IDs, if ``properties`` is None;
429 +
            Pandas Series indexed by edge IDs if ``properties`` is string;
430 +
            Pandas DataFrame indexed by edge IDs if ``properties`` is list.
431 +
        """
432 +
        return self.pathway_edges(source=node_id, target=None, properties=properties)
421 433
422 -
    @cached_property
423 -
    def config(self):
424 -
        """Population config dictionary combined with the components dictionary."""
425 -
        components = deepcopy(self._edge_storage.circuit.config.get("components", {}))
426 -
        components.update(self._config)
427 -
        return components
434 +
    def pair_edges(self, source_node_id, target_node_id, properties=None):
435 +
        """Get edges corresponding to ``source_node_id`` -> ``target_node_id`` connection.
428 436
429 -
    @property
430 -
    def type(self):
431 -
        """Population type."""
432 -
        return self.config.get("type", DEFAULT_EDGE_TYPE)
437 +
        Args:
438 +
            source_node_id (CircuitNodeIds/int/sequence/str/mapping/None): source node ID
439 +
            target_node_id (CircuitNodeIds/int/sequence/str/mapping/None): target node ID
440 +
            properties: None / edge property name / list of edge property names
433 441
434 -
    @cached_property
435 -
    def source(self):
436 -
        """Source NodePopulation."""
437 -
        return self._nodes(self._population.source)
442 +
        Returns:
443 +
            List of edge IDs, if ``properties`` is None;
444 +
            Pandas Series indexed by edge IDs if ``properties`` is string;
445 +
            Pandas DataFrame indexed by edge IDs if ``properties`` is list.
446 +
        """
447 +
        return self.pathway_edges(
448 +
            source=source_node_id, target=target_node_id, properties=properties
449 +
        )
438 450
439 -
    @cached_property
440 -
    def target(self):
441 -
        """Target NodePopulation."""
442 -
        return self._nodes(self._population.target)
451 +
    def _iter_connections(self, source_node_ids, target_node_ids, unique_node_ids, shuffle):
452 +
        """Iterate through `source_node_ids` -> `target_node_ids` connections."""
453 +
        # pylint: disable=too-many-branches,too-many-locals
454 +
        def _optimal_direction():
455 +
            """Choose between source and target node IDs for iterating."""
456 +
            if target_node_ids is None and source_node_ids is None:
457 +
                raise BluepySnapError("Either `source` or `target` should be specified")
458 +
            if source_node_ids is None:
459 +
                return "target"
460 +
            if target_node_ids is None:
461 +
                return "source"
462 +
            else:
463 +
                # Checking the indexing 'direction'. One direction has contiguous indices.
464 +
                range_size_source = _estimate_range_size(
465 +
                    self._population.efferent_edges, source_node_ids
466 +
                )
467 +
                range_size_target = _estimate_range_size(
468 +
                    self._population.afferent_edges, target_node_ids
469 +
                )
470 +
                return "source" if (range_size_source < range_size_target) else "target"
443 471
444 -
    @cached_property
445 -
    def _attribute_names(self):
446 -
        return set(self._population.attribute_names)
472 +
        if _is_empty(source_node_ids) or _is_empty(target_node_ids):
473 +
            return
447 474
448 -
    @cached_property
449 -
    def _dynamics_params_names(self):
450 -
        return set(utils.add_dynamic_prefix(self._population.dynamics_attribute_names))
475 +
        direction = _optimal_direction()
476 +
        if direction == "target":
477 +
            primary_node_ids, secondary_node_ids = target_node_ids, source_node_ids
478 +
            get_connected_node_ids = self.afferent_nodes
479 +
        else:
480 +
            primary_node_ids, secondary_node_ids = source_node_ids, target_node_ids
481 +
            get_connected_node_ids = self.efferent_nodes
451 482
452 -
    @property
453 -
    def _topology_property_names(self):
454 -
        return {Edge.SOURCE_NODE_ID, Edge.TARGET_NODE_ID}
483 +
        primary_node_ids = np.unique(primary_node_ids)
484 +
        if shuffle:
485 +
            np.random.shuffle(primary_node_ids)
455 486
456 -
    @property
457 -
    def property_names(self):
458 -
        """Set of available edge properties.
487 +
        if secondary_node_ids is not None:
488 +
            secondary_node_ids = np.unique(secondary_node_ids)
459 489
460 -
        Notes:
461 -
            Properties are a combination of the group attributes, the dynamics_params and the
462 -
            topology properties.
463 -
        """
464 -
        return self._attribute_names | self._dynamics_params_names | self._topology_property_names
490 +
        secondary_node_ids_used = set()
465 491
466 -
    @cached_property
467 -
    def property_dtypes(self):
468 -
        """Returns the dtypes of all the properties.
492 +
        for key_node_id in primary_node_ids:
493 +
            connected_node_ids = get_connected_node_ids(key_node_id, unique=False)
494 +
            # [[secondary_node_id, count], ...]
495 +
            connected_node_ids_with_count = np.stack(
496 +
                np.unique(connected_node_ids, return_counts=True)
497 +
            ).transpose()
498 +
            # np.stack(uint64, int64) -> float64
499 +
            connected_node_ids_with_count = connected_node_ids_with_count.astype(np.uint32)
500 +
            if secondary_node_ids is not None:
501 +
                mask = np.in1d(
502 +
                    connected_node_ids_with_count[:, 0], secondary_node_ids, assume_unique=True
503 +
                )
504 +
                connected_node_ids_with_count = connected_node_ids_with_count[mask]
505 +
            if shuffle:
506 +
                np.random.shuffle(connected_node_ids_with_count)
469 507
470 -
        Returns:
471 -
            pandas.Series: series indexed by field name with the corresponding dtype as value.
472 -
        """
473 -
        return self.get([0], list(self.property_names)).dtypes.sort_index()
508 +
            for conn_node_id, edge_count in connected_node_ids_with_count:
509 +
                if unique_node_ids and (conn_node_id in secondary_node_ids_used):
510 +
                    continue
511 +
                if direction == "target":
512 +
                    yield conn_node_id, key_node_id, edge_count
513 +
                else:
514 +
                    yield key_node_id, conn_node_id, edge_count
515 +
                if unique_node_ids:
516 +
                    secondary_node_ids_used.add(conn_node_id)
517 +
                    break
474 518
475 -
    def container_property_names(self, container):
476 -
        """Lists the ConstContainer properties shared with the EdgePopulation.
519 +
    def iter_connections(
520 +
        self,
521 +
        source=None,
522 +
        target=None,
523 +
        unique_node_ids=False,
524 +
        shuffle=False,
525 +
        return_edge_ids=False,
526 +
        return_edge_count=False,
527 +
    ):
528 +
        """Iterate through ``source`` -> ``target`` connections.
477 529
478 530
        Args:
479 -
            container (ConstContainer): a container class for edge properties.
531 +
            source (CircuitNodeIds/int/sequence/str/mapping/None): source node group
532 +
            target (CircuitNodeIds/int/sequence/str/mapping/None): target node group
533 +
            unique_node_ids: if True, no node ID will be used more than once as source or
534 +
                target for edges. Careful, this flag does not provide unique (source, target)
535 +
                pairs but unique node IDs.
536 +
            shuffle: if True, result order would be (somewhat) randomized
537 +
            return_edge_count: if True, edge count is added to yield result
538 +
            return_edge_ids: if True, edge ID list is added to yield result
480 539
481 -
        Returns:
482 -
            list: A list of strings corresponding to the properties that you can use from the
483 -
                container class
540 +
        ``return_edge_count`` and ``return_edge_ids`` are mutually exclusive.
484 541
485 -
        Examples:
486 -
            >>> from bluepysnap.sonata_constants import Edge
487 -
            >>> print(my_edge_population.container_property_names(Edge))
488 -
            >>> ["AXONAL_DELAY", "SYN_WEIGHT"] # values you can use with my_edge_population
542 +
        Yields:
543 +
            (source_node_id, target_node_id, edge_ids) if return_edge_ids == True;
544 +
            (source_node_id, target_node_id, edge_count) if return_edge_count == True;
545 +
            (source_node_id, target_node_id) otherwise.
489 546
        """
490 -
        if not inspect.isclass(container) or not issubclass(container, ConstContainer):
491 -
            raise BluepySnapError("'container' must be a subclass of ConstContainer")
492 -
        in_file = self.property_names
493 -
        return [k for k in container.key_set() if container.get(k) in in_file]
494 -
495 -
    def _get_property(self, prop, selection):
496 -
        if prop == Edge.SOURCE_NODE_ID:
497 -
            result = utils.ensure_ids(self._population.source_nodes(selection))
498 -
        elif prop == Edge.TARGET_NODE_ID:
499 -
            result = utils.ensure_ids(self._population.target_nodes(selection))
500 -
        elif prop in self._attribute_names:
501 -
            result = self._population.get_attribute(prop, selection)
502 -
        elif prop in self._dynamics_params_names:
503 -
            result = self._population.get_dynamics_attribute(
504 -
                prop.split(DYNAMICS_PREFIX)[1], selection
547 +
        if return_edge_ids and return_edge_count:
548 +
            raise BluepySnapError(
549 +
                "`return_edge_count` and `return_edge_ids` are mutually exclusive"
505 550
            )
506 -
        else:
507 -
            raise BluepySnapError(f"No such property: {prop}")
508 -
        return result
509 551
510 -
    def _get(self, selection, properties=None):
511 -
        """Get an array of edge IDs or DataFrame with edge properties."""
512 -
        edge_ids = utils.ensure_ids(selection.flatten())
513 -
        if properties is None:
514 -
            Deprecate.warn(
515 -
                "Returning ids with get/properties is deprecated and will be removed in 1.0.0. "
516 -
                "Please use EdgePopulation.ids instead."
517 -
            )
518 -
            return edge_ids
552 +
        source_node_ids = self._resolve_node_ids(self.source, source)
553 +
        target_node_ids = self._resolve_node_ids(self.target, target)
519 554
520 -
        if utils.is_iterable(properties):
521 -
            if len(edge_ids) == 0:
522 -
                result = pd.DataFrame(columns=properties)
523 -
            else:
524 -
                result = pd.DataFrame(index=edge_ids)
525 -
                for p in properties:
526 -
                    result[p] = self._get_property(p, selection)
555 +
        it = self._iter_connections(source_node_ids, target_node_ids, unique_node_ids, shuffle)
556 +
557 +
        if return_edge_count:
558 +
            return it
559 +
        elif return_edge_ids:
560 +
            add_edge_ids = lambda x: (x[0], x[1], self.pair_edges(x[0], x[1]))
561 +
            return map(add_edge_ids, it)
527 562
        else:
528 -
            if len(edge_ids) == 0:
529 -
                result = pd.Series(name=properties, dtype=np.float64)
530 -
            else:
531 -
                result = pd.Series(
532 -
                    self._get_property(properties, selection), index=edge_ids, name=properties
533 -
                )
563 +
            omit_edge_count = lambda x: x[:2]
564 +
            return map(omit_edge_count, it)
534 565
535 -
        return result
566 +
    @cached_property
567 +
    def h5_filepath(self):
568 +
        """Get the H5 edges file associated with population."""
569 +
        for edge_conf in self._circuit.config["networks"]["edges"]:
570 +
            if self.name in edge_conf["populations"]:
571 +
                return edge_conf["edges_file"]
572 +
        for edge_conf in self._circuit.config["networks"]["edges"]:
573 +
            h5_filepath = edge_conf["edges_file"]
574 +
            storage = libsonata.EdgeStorage(h5_filepath)
575 +
            if self.name in storage.population_names:  # pylint: disable=unsupported-membership-test
576 +
                return h5_filepath
577 +
        raise BluepySnapError(f"h5_filepath not found for population '{self.name}'")
536 578
537 -
    def _edge_ids_by_filter(self, queries, raise_missing_prop):
538 -
        """Return edge IDs if their properties match the `queries` dict.
539 579
540 -
        `props` values could be:
541 -
            pairs (range match for floating dtype fields)
542 -
            scalar or iterables (exact or "one of" match for other fields)
580 +
class Edges(
581 +
    NetworkObject,
582 +
    metaclass=AbstractDocSubstitutionMeta,
583 +
    source_word="NetworkObject",
584 +
    target_word="Edge",
585 +
):
586 +
    """The top level Edges accessor."""
543 587
544 -
        You can use the special operators '$or' and '$and' also to combine different queries
545 -
        together.
588 +
    _population_class = EdgePopulation
546 589
547 -
        Examples:
548 -
            >>> self._edge_ids_by_filter({ Edge.POST_SECTION_ID: (0, 1),
549 -
            >>>                            Edge.AXONAL_DELAY: (.5, 2.) })
550 -
            >>> self._edge_ids_by_filter({'$or': [{ Edge.PRE_X_CENTER: [2, 3]},
551 -
            >>>                              { Edge.POST_SECTION_POS: (0, 1),
552 -
            >>>                              Edge.SYN_WEIGHT: (0.,1.4) }]})
590 +
    def __init__(self, circuit):  # pylint: disable=useless-super-delegation
591 +
        """Initialize the top level Edges accessor."""
592 +
        super().__init__(circuit)
553 593
554 -
        """
555 -
        properties = query.get_properties(queries)
556 -
        unknown_props = properties - self.property_names
557 -
        if raise_missing_prop and unknown_props:
558 -
            raise BluepySnapError(f"Unknown edge properties: {unknown_props}")
559 -
        res = []
560 -
        ids = self.ids(None)
561 -
        chunk_size = int(1e8)
562 -
        for chunk in np.array_split(ids, 1 + len(ids) // chunk_size):
563 -
            data = self.get(chunk, properties - unknown_props)
564 -
            res.extend(chunk[query.resolve_ids(data, self.name, queries)])
565 -
        return np.array(res, dtype=IDS_DTYPE)
594 +
    @cached_property
595 +
    def population_names(self):
596 +
        """Defines all sorted edge population names from the Circuit."""
597 +
        return sorted(self._circuit.to_libsonata.edge_populations)
566 598
567 -
    def ids(self, group=None, limit=None, sample=None, raise_missing_property=True):
568 -
        """Edge IDs corresponding to edges ``edge_ids``.
599 +
    def ids(self, group=None, sample=None, limit=None):
600 +
        """Edge CircuitEdgeIds corresponding to edges ``edge_ids``.
569 601
570 602
        Args:
571 603
            group (None/int/CircuitEdgeId/CircuitEdgeIds/sequence): Which IDs will be
572 -
                returned depends on the type of the ``group`` argument:
573 -
                - ``None``: return all IDs.
574 -
                - ``int``, ``CircuitEdgeId``: return a single edge ID.
575 -
                - ``CircuitEdgeIds`` return IDs of edges the edge population in an array.
576 -
                - ``sequence``: return IDs of edges in an array.
577 -
604 +
            returned depends on the type of the ``group`` argument:
605 +
                - ``None``: return all CircuitEdgeIds.
606 +
                - ``CircuitEdgeId``: return the ID in a CircuitEdgeIds object.
607 +
                - ``CircuitEdgeIds``: return the IDs in a CircuitNodeIds object.
608 +
                - ``int``: returns a CircuitEdgeIds object containing the corresponding edge ID
609 +
                    for all populations.
610 +
                - ``sequence``: returns a CircuitEdgeIds object containing the corresponding edge
611 +
                    IDs for all populations.
578 612
            sample (int): If specified, randomly choose ``sample`` number of
579 613
                IDs from the match result. If the size of the sample is greater than
580 -
                the size of the EdgePopulation then all ids are taken and shuffled.
581 -
614 +
                the size of all the EdgePopulations then all ids are taken and shuffled.
582 615
            limit (int): If specified, return the first ``limit`` number of
583 -
                IDs from the match result. If limit is greater than the size of the population
616 +
                IDs from the match result. If limit is greater than the size of all the populations
584 617
                all node IDs are returned.
585 618
586 -
            raise_missing_property (bool): if True, raises if a property is not listed in this
587 -
                population. Otherwise the ids are just not selected if a property is missing.
588 -
589 619
        Returns:
590 -
            numpy.array: A numpy array of IDs.
591 -
        """
592 -
        if group is None:
593 -
            result = self._population.select_all().flatten()
594 -
        elif isinstance(group, CircuitEdgeIds):
595 -
            result = group.filter_population(self.name).get_ids()
596 -
        elif isinstance(group, np.ndarray):
597 -
            result = group
598 -
        elif isinstance(group, Mapping):
599 -
            result = self._edge_ids_by_filter(
600 -
                queries=group, raise_missing_prop=raise_missing_property
601 -
            )
602 -
        else:
603 -
            result = utils.ensure_list(group)
604 -
            # test if first value is a CircuitEdgeId if yes then all values must be CircuitEdgeId
605 -
            if isinstance(first(result, None), CircuitEdgeId):
606 -
                try:
607 -
                    result = [cid.id for cid in result if cid.population == self.name]
608 -
                except AttributeError as e:
609 -
                    raise BluepySnapError(
610 -
                        "All values from a list must be of type int or CircuitEdgeId."
611 -
                    ) from e
612 -
        if sample is not None:
613 -
            if len(result) > 0:
614 -
                result = np.random.choice(result, min(sample, len(result)), replace=False)
615 -
        if limit is not None:
616 -
            result = result[:limit]
617 -
        return utils.ensure_ids(result)
620 +
            CircuitEdgeIds: returns a CircuitEdgeIds containing all the edge IDs and the
621 +
                corresponding populations. For performance reasons we do not test if the edge ids
622 +
                are present or not in the circuit.
623 +
624 +
        Notes:
625 +
            This envision also the maybe future selection of edges on queries.
626 +
        """
627 +
        if isinstance(group, CircuitEdgeIds):
628 +
            diff = np.setdiff1d(group.get_populations(unique=True), self.population_names)
629 +
            if diff.size != 0:
630 +
                raise BluepySnapError(f"Population {diff} does not exist in the circuit.")
631 +
        fun = lambda x: (x.ids(group), x.name)
632 +
        return self._get_ids_from_pop(fun, CircuitEdgeIds, sample=sample, limit=limit)
618 633
619 -
    def get(self, edge_ids, properties):
634 +
    def get(self, edge_ids=None, properties=None):  # pylint: disable=arguments-renamed
620 635
        """Edge properties as pandas DataFrame.
621 636
622 637
        Args:
623 -
            edge_ids (array-like): array-like of edge IDs
624 -
            properties (str/list): an edge property name or a list of edge property names
638 +
            edge_ids (int/CircuitEdgeId/CircuitEdgeIds/sequence): same as Edges.ids().
639 +
            properties (None/str/list): an edge property name or a list of edge property names.
640 +
                If set to None ids are returned.
625 641
626 642
        Returns:
627 643
            pandas.Series/pandas.DataFrame:
@@ -629,44 +645,31 @@
Loading
629 645
                A pandas DataFrame indexed by edge IDs if ``properties`` is list.
630 646
631 647
        Notes:
632 -
            The EdgePopulation.property_names function will give you all the usable properties
648 +
            The Edges.property_names function will give you all the usable properties
633 649
            for the `properties` argument.
634 650
        """
635 -
        edge_ids = self.ids(edge_ids)
636 -
        selection = libsonata.Selection(edge_ids)
637 -
        return self._get(selection, properties)
651 +
        if edge_ids is None:
652 +
            raise BluepySnapError("You need to set edge_ids in get.")
653 +
        if properties is None:
654 +
            Deprecate.warn(
655 +
                "Returning ids with get/properties is deprecated and will be removed in 1.0.0. "
656 +
                "Please use Edges.ids instead."
657 +
            )
658 +
            return edge_ids
659 +
        return super().get(edge_ids, properties)
638 660
639 661
    def properties(self, edge_ids, properties):
640 662
        """Doc is overridden below."""
641 663
        Deprecate.warn(
642 -
            "EdgePopulation.properties function is deprecated and will be removed in 1.0.0. "
643 -
            "Please use EdgePopulation.get instead."
664 +
            "Edges.properties function is deprecated and will be removed in 1.0.0. "
665 +
            "Please use Edges.get instead."
644 666
        )
645 667
        return self.get(edge_ids, properties)
646 668
647 669
    properties.__doc__ = get.__doc__
648 670
649 -
    def positions(self, edge_ids, side, kind):
650 -
        """Edge positions as a pandas DataFrame.
651 -
652 -
        Args:
653 -
            edge_ids (array-like): array-like of edge IDs
654 -
            side (str): ``afferent`` or ``efferent``
655 -
            kind (str): ``center`` or ``surface``
656 -
657 -
        Returns:
658 -
            Pandas Dataframe with ('x', 'y', 'z') columns indexed by edge IDs.
659 -
        """
660 -
        assert side in ("afferent", "efferent")
661 -
        assert kind in ("center", "surface")
662 -
        props = {f"{side}_{kind}_{p}": p for p in ["x", "y", "z"]}
663 -
        result = self.get(edge_ids, list(props))
664 -
        result.rename(columns=props, inplace=True)
665 -
        result.sort_index(axis=1, inplace=True)
666 -
        return result
667 -
668 671
    def afferent_nodes(self, target, unique=True):
669 -
        """Get afferent node IDs for given target ``node_id``.
672 +
        """Get afferent CircuitNodeIDs for given target ``node_id``.
670 673
671 674
        Notes:
672 675
            Afferent nodes are nodes projecting an outgoing edge to one of the ``target`` node.
@@ -677,16 +680,15 @@
Loading
677 680
            unique (bool): If ``True``, return only unique afferent node IDs.
678 681
679 682
        Returns:
680 -
            numpy.ndarray: Afferent node IDs for all the targets.
683 +
            CircuitNodeIDs: Afferent CircuitNodeIDs for all the targets from all edge population.
681 684
        """
682 -
        if target is not None:
683 -
            selection = self._population.afferent_edges(self._resolve_node_ids(self.target, target))
684 -
        else:
685 -
            selection = self._population.select_all()
686 -
        result = self._population.source_nodes(selection)
685 +
        target_ids = self._circuit.nodes.ids(target)
686 +
        result = self._get_ids_from_pop(
687 +
            lambda x: (x.afferent_nodes(target_ids), x.source.name), CircuitNodeIds
688 +
        )
687 689
        if unique:
688 -
            result = np.unique(result)
689 -
        return utils.ensure_ids(result)
690 +
            result.unique(inplace=True)
691 +
        return result
690 692
691 693
    def efferent_nodes(self, source, unique=True):
692 694
        """Get efferent node IDs for given source ``node_id``.
@@ -702,50 +704,46 @@
Loading
702 704
        Returns:
703 705
            numpy.ndarray: Efferent node IDs for all the sources.
704 706
        """
705 -
        if source is not None:
706 -
            selection = self._population.efferent_edges(self._resolve_node_ids(self.source, source))
707 -
        else:
708 -
            selection = self._population.select_all()
709 -
        result = self._population.target_nodes(selection)
707 +
        source_ids = self._circuit.nodes.ids(source)
708 +
        result = self._get_ids_from_pop(
709 +
            lambda x: (x.efferent_nodes(source_ids), x.target.name), CircuitNodeIds
710 +
        )
710 711
        if unique:
711 -
            result = np.unique(result)
712 -
        return utils.ensure_ids(result)
712 +
            result.unique(inplace=True)
713 +
        return result
713 714
714 715
    def pathway_edges(self, source=None, target=None, properties=None):
715 716
        """Get edges corresponding to ``source`` -> ``target`` connections.
716 717
717 718
        Args:
718 -
            source (CircuitNodeIds/int/sequence/str/mapping/None): source node group
719 -
            target (CircuitNodeIds/int/sequence/str/mapping/None): target node group
719 +
            source: source node group
720 +
            target: target node group
720 721
            properties: None / edge property name / list of edge property names
721 722
722 723
        Returns:
723 -
            List of edge IDs, if ``properties`` is None;
724 -
            Pandas Series indexed by edge IDs if ``properties`` is string;
725 -
            Pandas DataFrame indexed by edge IDs if ``properties`` is list.
724 +
            CircuitEdgeIDs, if ``properties`` is None;
725 +
            Pandas Series indexed by CircuitEdgeIDs if ``properties`` is string;
726 +
            Pandas DataFrame indexed by CircuitEdgeIDs if ``properties`` is list.
726 727
        """
727 728
        if source is None and target is None:
728 729
            raise BluepySnapError("Either `source` or `target` should be specified")
729 730
730 -
        source_node_ids = self._resolve_node_ids(self.source, source)
731 -
        target_edge_ids = self._resolve_node_ids(self.target, target)
731 +
        source_ids = self._circuit.nodes.ids(source)
732 +
        target_ids = self._circuit.nodes.ids(target)
732 733
733 -
        if source_node_ids is None:
734 -
            selection = self._population.afferent_edges(target_edge_ids)
735 -
        elif target_edge_ids is None:
736 -
            selection = self._population.efferent_edges(source_node_ids)
737 -
        else:
738 -
            selection = self._population.connecting_edges(source_node_ids, target_edge_ids)
734 +
        result = self._get_ids_from_pop(
735 +
            lambda x: (x.pathway_edges(source_ids, target_ids), x.name), CircuitEdgeIds
736 +
        )
739 737
740 738
        if properties:
741 -
            return self._get(selection, properties)
742 -
        return utils.ensure_ids(selection.flatten())
739 +
            return self.get(result, properties)
740 +
        return result
743 741
744 742
    def afferent_edges(self, node_id, properties=None):
745 743
        """Get afferent edges for given ``node_id``.
746 744
747 745
        Args:
748 -
            node_id (CircuitNodeIds/int/sequence/str/mapping/None) : Target node ID.
746 +
            node_id (int): Target node ID.
749 747
            properties: An edge property name, a list of edge property names, or None.
750 748
751 749
        Returns:
@@ -760,7 +758,7 @@
Loading
760 758
        """Get efferent edges for given ``node_id``.
761 759
762 760
        Args:
763 -
            node_id (CircuitNodeIds/int/sequence/str/mapping/None): source node ID
761 +
            node_id: source node ID
764 762
            properties: None / edge property name / list of edge property names
765 763
766 764
        Returns:
@@ -774,8 +772,8 @@
Loading
774 772
        """Get edges corresponding to ``source_node_id`` -> ``target_node_id`` connection.
775 773
776 774
        Args:
777 -
            source_node_id (CircuitNodeIds/int/sequence/str/mapping/None): source node ID
778 -
            target_node_id (CircuitNodeIds/int/sequence/str/mapping/None): target node ID
775 +
            source_node_id: source node ID
776 +
            target_node_id: target node ID
779 777
            properties: None / edge property name / list of edge property names
780 778
781 779
        Returns:
@@ -787,92 +785,47 @@
Loading
787 785
            source=source_node_id, target=target_node_id, properties=properties
788 786
        )
789 787
790 -
    def _iter_connections(self, source_node_ids, target_node_ids, unique_node_ids, shuffle):
791 -
        """Iterate through `source_node_ids` -> `target_node_ids` connections."""
792 -
        # pylint: disable=too-many-branches,too-many-locals
793 -
        def _optimal_direction():
794 -
            """Choose between source and target node IDs for iterating."""
795 -
            if target_node_ids is None and source_node_ids is None:
796 -
                raise BluepySnapError("Either `source` or `target` should be specified")
797 -
            if source_node_ids is None:
798 -
                return "target"
799 -
            if target_node_ids is None:
800 -
                return "source"
801 -
            else:
802 -
                # Checking the indexing 'direction'. One direction has contiguous indices.
803 -
                range_size_source = _estimate_range_size(
804 -
                    self._population.efferent_edges, source_node_ids
805 -
                )
806 -
                range_size_target = _estimate_range_size(
807 -
                    self._population.afferent_edges, target_node_ids
808 -
                )
809 -
                return "source" if (range_size_source < range_size_target) else "target"
810 -
811 -
        if _is_empty(source_node_ids) or _is_empty(target_node_ids):
812 -
            return
813 -
814 -
        direction = _optimal_direction()
815 -
        if direction == "target":
816 -
            primary_node_ids, secondary_node_ids = target_node_ids, source_node_ids
817 -
            get_connected_node_ids = self.afferent_nodes
818 -
        else:
819 -
            primary_node_ids, secondary_node_ids = source_node_ids, target_node_ids
820 -
            get_connected_node_ids = self.efferent_nodes
821 -
822 -
        primary_node_ids = np.unique(primary_node_ids)
823 -
        if shuffle:
824 -
            np.random.shuffle(primary_node_ids)
825 -
826 -
        if secondary_node_ids is not None:
827 -
            secondary_node_ids = np.unique(secondary_node_ids)
788 +
    @staticmethod
789 +
    def _add_circuit_ids(its, source, target):
790 +
        """Generator comprehension adding the CircuitIds to the iterator.
828 791
829 -
        secondary_node_ids_used = set()
792 +
        Notes:
793 +
            Using closures or lambda functions would result in override functions and so the
794 +
            source and target would be the same for all the populations.
795 +
        """
796 +
        return (
797 +
            (CircuitNodeId(source, source_id), CircuitNodeId(target, target_id), count)
798 +
            for source_id, target_id, count in its
799 +
        )
830 800
831 -
        for key_node_id in primary_node_ids:
832 -
            connected_node_ids = get_connected_node_ids(key_node_id, unique=False)
833 -
            # [[secondary_node_id, count], ...]
834 -
            connected_node_ids_with_count = np.stack(
835 -
                np.unique(connected_node_ids, return_counts=True)
836 -
            ).transpose()
837 -
            # np.stack(uint64, int64) -> float64
838 -
            connected_node_ids_with_count = connected_node_ids_with_count.astype(np.uint32)
839 -
            if secondary_node_ids is not None:
840 -
                mask = np.in1d(
841 -
                    connected_node_ids_with_count[:, 0], secondary_node_ids, assume_unique=True
842 -
                )
843 -
                connected_node_ids_with_count = connected_node_ids_with_count[mask]
844 -
            if shuffle:
845 -
                np.random.shuffle(connected_node_ids_with_count)
801 +
    @staticmethod
802 +
    def _add_edge_ids(its, source, target, pop_name):
803 +
        """Generator comprehension adding the CircuitIds to the iterator."""
804 +
        return (
805 +
            (
806 +
                CircuitNodeId(source, source_id),
807 +
                CircuitNodeId(target, target_id),
808 +
                CircuitEdgeIds.from_dict({pop_name: edge_id}),
809 +
            )
810 +
            for source_id, target_id, edge_id in its
811 +
        )
846 812
847 -
            for conn_node_id, edge_count in connected_node_ids_with_count:
848 -
                if unique_node_ids and (conn_node_id in secondary_node_ids_used):
849 -
                    continue
850 -
                if direction == "target":
851 -
                    yield conn_node_id, key_node_id, edge_count
852 -
                else:
853 -
                    yield key_node_id, conn_node_id, edge_count
854 -
                if unique_node_ids:
855 -
                    secondary_node_ids_used.add(conn_node_id)
856 -
                    break
813 +
    @staticmethod
814 +
    def _omit_edge_count(its, source, target):
815 +
        """Generator comprehension adding the CircuitIds to the iterator."""
816 +
        return (
817 +
            (CircuitNodeId(source, source_id), CircuitNodeId(target, target_id))
818 +
            for source_id, target_id in its
819 +
        )
857 820
858 821
    def iter_connections(
859 -
        self,
860 -
        source=None,
861 -
        target=None,
862 -
        unique_node_ids=False,
863 -
        shuffle=False,
864 -
        return_edge_ids=False,
865 -
        return_edge_count=False,
822 +
        self, source=None, target=None, return_edge_ids=False, return_edge_count=False
866 823
    ):
867 824
        """Iterate through ``source`` -> ``target`` connections.
868 825
869 826
        Args:
870 827
            source (CircuitNodeIds/int/sequence/str/mapping/None): source node group
871 828
            target (CircuitNodeIds/int/sequence/str/mapping/None): target node group
872 -
            unique_node_ids: if True, no node ID will be used more than once as source or
873 -
                target for edges. Careful, this flag does not provide unique (source, target)
874 -
                pairs but unique node IDs.
875 -
            shuffle: if True, result order would be (somewhat) randomized
876 829
            return_edge_count: if True, edge count is added to yield result
877 830
            return_edge_ids: if True, edge ID list is added to yield result
878 831
@@ -887,22 +840,18 @@
Loading
887 840
            raise BluepySnapError(
888 841
                "`return_edge_count` and `return_edge_ids` are mutually exclusive"
889 842
            )
890 -
891 -
        source_node_ids = self._resolve_node_ids(self.source, source)
892 -
        target_node_ids = self._resolve_node_ids(self.target, target)
893 -
894 -
        it = self._iter_connections(source_node_ids, target_node_ids, unique_node_ids, shuffle)
895 -
896 -
        if return_edge_count:
897 -
            return it
898 -
        elif return_edge_ids:
899 -
            add_edge_ids = lambda x: (x[0], x[1], self.pair_edges(x[0], x[1]))
900 -
            return map(add_edge_ids, it)
901 -
        else:
902 -
            omit_edge_count = lambda x: x[:2]
903 -
            return map(omit_edge_count, it)
904 -
905 -
    @property
906 -
    def h5_filepath(self):
907 -
        """Get the H5 edges file associated with population."""
908 -
        return self._edge_storage.h5_filepath
843 +
        for name, pop in self.items():
844 +
            it = pop.iter_connections(
845 +
                source=source,
846 +
                target=target,
847 +
                return_edge_ids=return_edge_ids,
848 +
                return_edge_count=return_edge_count,
849 +
            )
850 +
            source_pop = pop.source.name
851 +
            target_pop = pop.target.name
852 +
            if return_edge_count:
853 +
                yield from self._add_circuit_ids(it, source_pop, target_pop)
854 +
            elif return_edge_ids:
855 +
                yield from self._add_edge_ids(it, source_pop, target_pop, name)
856 +
            else:
857 +
                yield from self._omit_edge_count(it, source_pop, target_pop)

@@ -19,7 +19,7 @@
Loading
19 19
20 20
from cached_property import cached_property
21 21
22 -
from bluepysnap.config import Config
22 +
from bluepysnap.config import CircuitConfig
23 23
from bluepysnap.edges import Edges
24 24
from bluepysnap.node_sets import NodeSets
25 25
from bluepysnap.nodes import Nodes
@@ -32,23 +32,28 @@
Loading
32 32
        """Initializes a circuit object from a SONATA config file.
33 33
34 34
        Args:
35 -
            config (str/dict): Path to a SONATA config file or sonata config dict.
35 +
            config (str): Path to a SONATA config file.
36 36
37 37
        Returns:
38 38
            Circuit: A Circuit object.
39 39
        """
40 -
        self._config = Config(config).resolve()
40 +
        self._config = CircuitConfig.from_config(config)
41 +
42 +
    @property
43 +
    def to_libsonata(self):
44 +
        """Libsonata instance of the circuit."""
45 +
        return self._config.to_libsonata
41 46
42 47
    @property
43 48
    def config(self):
44 49
        """Network config dictionary."""
45 -
        return self._config
50 +
        return self._config.to_dict()
46 51
47 52
    @cached_property
48 53
    def node_sets(self):
49 54
        """Returns the NodeSets object bound to the circuit."""
50 -
        if "node_sets_file" in self._config:
51 -
            return NodeSets(self._config["node_sets_file"])
55 +
        if "node_sets_file" in self.config:
56 +
            return NodeSets(self.config["node_sets_file"])
52 57
        return {}
53 58
54 59
    @cached_property

@@ -13,8 +13,9 @@
Loading
13 13
14 14
from bluepysnap import BluepySnapError
15 15
from bluepysnap.bbp import EDGE_TYPES, NODE_TYPES
16 -
from bluepysnap.config import Config
16 +
from bluepysnap.config import Parser
17 17
from bluepysnap.morph import EXTENSIONS_MAPPING
18 +
from bluepysnap.utils import load_json
18 19
19 20
L = logging.getLogger("brainbuilder")
20 21
MAX_MISSING_FILES_DISPLAY = 10
@@ -41,6 +42,8 @@
Loading
41 42
        """Returns only message by default."""
42 43
        return str(self.message)
43 44
45 +
    __repr__ = __str__
46 +
44 47
    def __eq__(self, other):
45 48
        """Two errors are equal if inherit from Error and their level, message are equal."""
46 49
        if not isinstance(other, Error):
@@ -857,7 +860,7 @@
Loading
857 860
    Returns:
858 861
        list: List of errors, empty if no errors
859 862
    """
860 -
    config = Config(config_file).resolve()
863 +
    config = Parser.parse(load_json(config_file), str(Path(config_file).parent))
861 864
    errors = _check_required_datasets(config)
862 865
863 866
    if not errors:
Files Coverage
bluepysnap 100.00%
Project Totals (23 files) 100.00%
bluepysnap-py39
Build #2838487149 -
pytest

No yaml found.

Create your codecov.yml to customize your Codecov experience

Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading