Skip to content

Commit

Permalink
Fix cyclic-import issues (#238)
Browse files Browse the repository at this point in the history
* import CircuitNodeId, CircuitEdgeId from utils
* move IDS_DTYPE, CircuitNodeId, CircuitEdgeId to circuit_ids_types.py
* move circuit_validation.Error -> exceptions.BluepySnapValidationError
* make warning and fatal classmethods of BluepySnapValidationError
  • Loading branch information
joni-herttuainen committed Oct 19, 2023
1 parent 95fa737 commit 4091868
Show file tree
Hide file tree
Showing 27 changed files with 273 additions and 208 deletions.
2 changes: 1 addition & 1 deletion bluepysnap/_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
import pandas as pd
from more_itertools import roundrobin

from bluepysnap.circuit_ids_types import IDS_DTYPE
from bluepysnap.exceptions import BluepySnapError
from bluepysnap.sonata_constants import Node
from bluepysnap.utils import IDS_DTYPE

L = logging.getLogger(__name__)

Expand Down
5 changes: 1 addition & 4 deletions bluepysnap/circuit_ids.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,15 @@

"""Circuit ids."""
import abc
from collections import namedtuple

import numpy as np
import pandas as pd

from bluepysnap import utils
from bluepysnap._doctools import AbstractDocSubstitutionMeta
from bluepysnap.circuit_ids_types import CircuitEdgeId, CircuitNodeId
from bluepysnap.exceptions import BluepySnapError

CircuitNodeId = namedtuple("CircuitNodeId", ("population", "id"))
CircuitEdgeId = namedtuple("CircuitEdgeId", ("population", "id"))


class CircuitIds(abc.ABC):
"""High performances CircuitID container.
Expand Down
30 changes: 30 additions & 0 deletions bluepysnap/circuit_ids_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Copyright (c) 2023, EPFL/Blue Brain Project

# This file is part of BlueBrain SNAP library <https://github.com/BlueBrain/snap>

# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Lesser General Public License version 3.0 as published
# by the Free Software Foundation.

# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.

# You should have received a copy of the GNU Lesser General Public License
# along with this library; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

"""CircuitIds types."""

from collections import namedtuple

import numpy as np

# dtypes for the different node and edge ids. We are using np.int64 to avoid the infamous
# https://github.com/numpy/numpy/issues/15084 numpy problem. This type needs to be used for
# all returned node or edge ids.
IDS_DTYPE = np.int64

CircuitNodeId = namedtuple("CircuitNodeId", ("population", "id"))
CircuitEdgeId = namedtuple("CircuitEdgeId", ("population", "id"))
133 changes: 58 additions & 75 deletions bluepysnap/circuit_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from bluepysnap import schemas
from bluepysnap.config import Parser
from bluepysnap.exceptions import BluepySnapValidationError
from bluepysnap.morph import EXTENSIONS_MAPPING
from bluepysnap.sonata_constants import DEFAULT_EDGE_TYPE, DEFAULT_NODE_TYPE
from bluepysnap.utils import load_json
Expand All @@ -20,52 +21,6 @@
MAX_MISSING_FILES_DISPLAY = 10


class Error:
"""Error used for reporting of validation errors."""

FATAL = "FATAL"
WARNING = "WARNING"
INFO = "INFO"

def __init__(self, level, message=None):
"""Error.
Args:
level (str): error level
message (str|None): message
"""
self.level = level
self.message = message

def __str__(self):
"""Returns only message by default."""
return str(self.message)

__repr__ = __str__

def __eq__(self, other):
"""Two errors are equal if inherit from Error and their level, message are equal."""
if not isinstance(other, Error):
return False
return self.level == other.level and self.message == other.message

def __hash__(self):
"""Hash. Errors with the same level and message give the same hash."""
return hash(self.level) ^ hash(self.message)


def fatal(message):
"""Shortcut for a fatal error.
Args:
message (str): text message
Returns:
Error: Error with level FATAL
"""
return Error(Error.FATAL, message)


def _check_partial_circuit_config(config):
return config.get("metadata", {}).get("status") == "partial"

Expand All @@ -82,11 +37,11 @@ def _check_components_dir(name, components):
"""
dirpath = components.get(name)
if not dirpath or not Path(dirpath).is_dir():
return [fatal(f'Invalid components "{name}": {dirpath}')]
return [BluepySnapValidationError.fatal(f'Invalid components "{name}": {dirpath}')]
return []


def _check_files(name, files, level):
def _check_files(name, files):
"""Checks for existence of files within an h5 group.
Args:
Expand All @@ -110,18 +65,24 @@ def _check_files(name, files, level):
filenames = "".join(f"\t{m.name}\n" for m in missing)

return [
Error(level, f"missing at least {len(missing)} files in group {name}:\n{filenames}")
BluepySnapValidationError.warning(
f"missing at least {len(missing)} files in group {name}:\n{filenames}"
)
]

return []


def _print_errors(errors):
"""Some fancy errors printing."""
colors = {Error.WARNING: "yellow", Error.FATAL: "red", Error.INFO: "green"}
colors = {
BluepySnapValidationError.WARNING: "yellow",
BluepySnapValidationError.FATAL: "red",
BluepySnapValidationError.INFO: "green",
}

if not errors:
print(click.style("No Error: Success.", fg=colors[Error.INFO]))
print(click.style("No Error: Success.", fg=colors[BluepySnapValidationError.INFO]))

for error in errors:
print(click.style(error.level + ": ", fg=colors[error.level]) + str(error))
Expand All @@ -135,7 +96,9 @@ def _check_duplicate_populations(networks, key):
for population in network.get("populations", {}):
if population in seen:
errors.append(
fatal(f'Already have population "{population}" in config for type "{key}"')
BluepySnapValidationError.fatal(
f'Already have population "{population}" in config for type "{key}"'
)
)
seen.add(population)

Expand Down Expand Up @@ -249,7 +212,7 @@ def _check_bio_nodes_group(group_df, group, population, population_name):

if "morphologies_dir" not in population and "alternate_morphologies" not in population:
errors.append(
fatal(
BluepySnapValidationError.fatal(
"at least one of 'morphologies_dir' or 'alternate_morphologies' "
f"must to be defined for 'biophysical' population '{population_name}'"
)
Expand All @@ -262,7 +225,6 @@ def _check_bio_nodes_group(group_df, group, population, population_name):
errors += _check_files(
f"morphology: {group_name}[{group.file.filename}]",
(Path(morph_path, m + "." + extension) for m in group_df["morphology"].unique()),
Error.WARNING,
)

if "biophysical_neuron_models_dir" in population:
Expand All @@ -276,11 +238,12 @@ def _check_bio_nodes_group(group_df, group, population, population_name):
bio_path / _get_model_template_file(m)
for m in group_df.get("model_template", pd.Series(dtype="object")).unique()
),
Error.WARNING,
)
else:
errors.append(
fatal(f"'biophysical_neuron_models_dir' not defined for population '{population_name}'")
BluepySnapValidationError.fatal(
f"'biophysical_neuron_models_dir' not defined for population '{population_name}'"
)
)
return errors

Expand All @@ -301,12 +264,13 @@ def _check_nodes_group(group_df, group, population, population_name):

errors = []
if "model_type" in group_df and group_df["model_type"][0] != population["type"]:
message = (
f"Population '{population_name}' type mismatch: "
f"'{group_df['model_type'][0]}' (nodes_file), "
f"'{population['type']}' (config)"
errors.append(
BluepySnapValidationError.warning(
f"Population '{population_name}' type mismatch: "
f"'{group_df['model_type'][0]}' (nodes_file), "
f"'{population['type']}' (config)"
)
)
errors.append(Error(Error.WARNING, message))

if population["type"] == "biophysical":
return errors + _check_bio_nodes_group(group_df, group, population, population_name)
Expand All @@ -331,7 +295,9 @@ def validate_node_population(nodes_file, population_dict, name):

# special case in which there are populations but not the one expected
if name not in h5f["nodes"]:
return [fatal(f"population '{name}' not found in {nodes_file}")]
return [
BluepySnapValidationError.fatal(f"population '{name}' not found in {nodes_file}")
]

population = h5f[f"nodes/{name}"]

Expand Down Expand Up @@ -385,7 +351,7 @@ def _check_edges_node_ids(nodes_ds, nodes):

nodes_dict = _find_nodes_population(node_population_name, nodes)
if not nodes_dict:
return [fatal(f'No node population for "{nodes_ds.name}"')]
return [BluepySnapValidationError.fatal(f'No node population for "{nodes_ds.name}"')]

if "nodes_file" not in nodes_dict or not Path(nodes_dict["nodes_file"]).is_file():
return []
Expand All @@ -397,10 +363,16 @@ def _check_edges_node_ids(nodes_ds, nodes):
missing_ids = sorted(set(nodes_ds[:]) - set(node_ids))
if missing_ids:
errors.append(
fatal(f"{nodes_ds.name} misses node ids in its node population: {missing_ids}")
BluepySnapValidationError.fatal(
f"{nodes_ds.name} misses node ids in its node population: {missing_ids}"
)
)
elif f"nodes/{node_population_name}" in h5f:
errors.append(fatal((f"{nodes_ds.name} does not have node ids in its node population")))
errors.append(
BluepySnapValidationError.fatal(
(f"{nodes_ds.name} does not have node ids in its node population")
)
)

return errors

Expand Down Expand Up @@ -430,7 +402,7 @@ def _check(indices, nodes_ds):
edge_node_ids = list(set(nodes_ds[edges_range[0] : edges_range[1]]))
if len(edge_node_ids) > 1 or edge_node_ids[0] != node_id:
errors.append(
fatal(
BluepySnapValidationError.fatal(
f"Population {population.file.filename} edges {edge_node_ids} have "
f"node ids {edges_range} instead of single id {node_id}"
)
Expand All @@ -443,10 +415,18 @@ def _check(indices, nodes_ds):

# These are "optional" (not mentioned in our spec) but better to at least give a warning
if not source_to_target:
errors.append(Error(Error.WARNING, f'No "source_to_target" in {population.file.filename}'))
errors.append(
BluepySnapValidationError.warning(
f'No "source_to_target" in {population.file.filename}'
)
)

if not target_to_source:
errors.append(Error(Error.WARNING, f'No "target_to_source" in {population.file.filename}'))
errors.append(
BluepySnapValidationError.warning(
f'No "target_to_source" in {population.file.filename}'
)
)

if target_to_source and source_to_target:
if "source_node_id" in population:
Expand Down Expand Up @@ -480,7 +460,9 @@ def _check_edge_population_data(population, nodes):
if "indices" in population:
errors += _check_edges_indices(population)
else: # "optional" (not mentioned in our spec) but better to at least give a warning
errors.append(Error(Error.WARNING, f'No "indices" in {population.file.filename}'))
errors.append(
BluepySnapValidationError.warning(f'No "indices" in {population.file.filename}')
)

return errors

Expand All @@ -502,7 +484,9 @@ def validate_edge_population(edges_file, name, nodes):

# special case in which there are populations but not the one expected
if name not in h5f["edges"]:
return [fatal(f"population '{name}' not found in {edges_file}")]
return [
BluepySnapValidationError.fatal(f"population '{name}' not found in {edges_file}")
]

population = h5f[f"edges/{name}"]

Expand Down Expand Up @@ -553,7 +537,7 @@ def _is_source_node_virtual(edges_dict, edge_population, nodes):
if not skip_slow:
errors += validate_edge_population(edges_file, name, nodes)
else:
errors.append(fatal(f'Invalid "edges_file": {edges_file}'))
errors.append(BluepySnapValidationError.fatal(f'Invalid "edges_file": {edges_file}'))

return errors

Expand All @@ -578,7 +562,7 @@ def validate_nodes_dict(nodes_dict, components):
errors = schemas.validate_nodes_schema(nodes_file, population["type"])
errors += validate_node_population(nodes_file, population, pop_name)
else:
errors.append(fatal(f'Invalid "nodes_file": {nodes_file}'))
errors.append(BluepySnapValidationError.fatal(f'Invalid "nodes_file": {nodes_file}'))

return errors

Expand Down Expand Up @@ -629,10 +613,9 @@ def validate(config_file, skip_slow, only_errors=False, print_errors=True):
"for partial configs as it depends on the intended use. "
)
L.warning(message)
errors.append(Error(Error.WARNING, message))

errors.append(BluepySnapValidationError.warning(message))
if only_errors:
errors = [e for e in errors if e.level == Error.FATAL]
errors = [e for e in errors if e.level == BluepySnapValidationError.FATAL]

if print_errors:
_print_errors(errors)
Expand Down
4 changes: 2 additions & 2 deletions bluepysnap/edges/edge_population.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@
from more_itertools import first

from bluepysnap import query, utils
from bluepysnap.circuit_ids import CircuitEdgeId, CircuitEdgeIds
from bluepysnap.circuit_ids import CircuitEdgeIds
from bluepysnap.circuit_ids_types import IDS_DTYPE, CircuitEdgeId
from bluepysnap.exceptions import BluepySnapError
from bluepysnap.sonata_constants import DYNAMICS_PREFIX, ConstContainer, Edge
from bluepysnap.utils import IDS_DTYPE


def _is_empty(xs):
Expand Down
5 changes: 3 additions & 2 deletions bluepysnap/edges/edges.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
import numpy as np

from bluepysnap._doctools import AbstractDocSubstitutionMeta
from bluepysnap.circuit_ids import CircuitEdgeIds, CircuitNodeId, CircuitNodeIds
from bluepysnap.edges import EdgePopulation
from bluepysnap.circuit_ids import CircuitEdgeIds, CircuitNodeIds
from bluepysnap.circuit_ids_types import CircuitNodeId
from bluepysnap.edges.edge_population import EdgePopulation
from bluepysnap.exceptions import BluepySnapError
from bluepysnap.network import NetworkObject

Expand Down

0 comments on commit 4091868

Please sign in to comment.