Skip to content

Commit

Permalink
Add JSON schema validation in Repair (#112)
Browse files Browse the repository at this point in the history
* Add JSON schema validation in Repair

* Fix URLs and add py310 and py311

* Lint and docs

* Use more specific exceptions
  • Loading branch information
adrien-berchet authored Mar 29, 2023
1 parent b151054 commit 71dfd31
Show file tree
Hide file tree
Showing 18 changed files with 182 additions and 82 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/run-tox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7,3.8,3.9]
python-version: ["3.7","3.8","3.9","3.10","3.11"]

steps:
- uses: actions/checkout@v2
Expand Down
4 changes: 2 additions & 2 deletions .pylintrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[MESSAGES CONTROL]
disable=bad-continuation,fixme,invalid-name,len-as-condition,no-else-return,useless-object-inheritance,import-outside-toplevel,unspecified-encoding
disable=fixme,invalid-name,len-as-condition,no-else-return,useless-object-inheritance,import-outside-toplevel,unspecified-encoding


[FORMAT]
Expand All @@ -17,7 +17,7 @@ max-locals=15
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of branch for function / method body
max-branchs=12
max-branches=12
# Maximum number of statements in function / method body
max-statements=50
# Maximum number of parents for a class (see R0901).
Expand Down
11 changes: 6 additions & 5 deletions neuror/axon.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from neurom import COLS
from neurom.features.section import branch_order, strahler_order

from neuror.exceptions import NeuroRError
from neuror.utils import section_length

L = logging.getLogger('neuror')
Expand All @@ -17,7 +18,7 @@ def _tree_distance(sec1, sec2):
Returns the number of sections between the 2 sections
Reimplementation of:
https://bbpcode.epfl.ch/browse/code/platform/BlueRepairSDK/tree/BlueRepairSDK/src/helper_axon.cpp#n35
https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_axon.cpp#L35
raises: if both sections are not part of the same neurite
Expand Down Expand Up @@ -50,7 +51,7 @@ def _tree_distance(sec1, sec2):
sec2 = sec2.parent
dist += 2
if None in {sec1, sec2}:
raise Exception(
raise NeuroRError(
f'Sections {original_sections[0]} and {original_sections[1]} '
'are not part of the same neurite')

Expand All @@ -62,7 +63,7 @@ def _downstream_pathlength(section):
Reimplementation of the C++ function "children_length":
https://bbpcode.epfl.ch/browse/code/platform/BlueRepairSDK/tree/BlueRepairSDK/src/morphstats.cpp#n112
https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/morphstats.cpp#L112
'''
ret = section_length(section)
for child in section.children:
Expand All @@ -73,7 +74,7 @@ def _downstream_pathlength(section):
def _similar_section(intact_axons, section):
'''Use the "mirror" technique of BlueRepairSDK to find out the similar section
https://bbpcode.epfl.ch/browse/code/platform/BlueRepairSDK/tree/BlueRepairSDK/src/helper_axon.cpp#n83
https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_axon.cpp#L83
Note:
I have *absolutely* no clue why sorting by this metric
Expand Down Expand Up @@ -104,7 +105,7 @@ def repair(morphology, section, intact_sections, axon_branches, used_axon_branch
'''Axonal repair
Reimplementation of:
https://bbpcode.epfl.ch/browse/code/platform/BlueRepairSDK/tree/BlueRepairSDK/src/repair.cpp#n727
https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/repair.cpp#L727
1) Find the most similar section in INTACT_SECTIONS list to SECTION
2) Sort AXON_BRANCHES according to a similarity score to the section found at step 1
Expand Down
9 changes: 5 additions & 4 deletions neuror/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from neurom.utils import NeuromJSON

from neuror.cut_plane.detection import CutPlane
from neuror.exceptions import NeuroRError
from neuror.unravel import DEFAULT_WINDOW_HALF_LENGTH

logging.basicConfig()
Expand Down Expand Up @@ -74,7 +75,7 @@ def file(input_file, output_file, error_summary_file, marker_file):
from neuror.sanitize import annotate_neurolucida

if Path(input_file).suffix not in ['.asc', '.ASC']:
raise Exception('Only .asc/.ASC files are allowed, please convert with morph-tool.')
raise NeuroRError('Only .asc/.ASC files are allowed, please convert with morph-tool.')

annotations, summary, markers = annotate_neurolucida(input_file)
shutil.copy(input_file, output_file)
Expand Down Expand Up @@ -170,7 +171,7 @@ def file(input_file, output_file, mapping_file, window_half_length):
neuron.write(output_file)
if mapping_file is not None:
if not mapping_file.lower().endswith('csv'):
raise Exception('the mapping file must end with .csv')
raise NeuroRError('the mapping file must end with .csv')
mapping.to_csv(mapping_file)


Expand Down Expand Up @@ -280,7 +281,7 @@ def _export_cut_plane(filename, output, width, display, searched_axes, fix_posit
It returns the cut plane and the positions of all cut terminations.
'''
if os.path.isdir(filename):
raise Exception(f'filename ({filename}) should not be a directory')
raise NeuroRError(f'filename ({filename}) should not be a directory')

result = CutPlane.find(filename,
width,
Expand All @@ -303,7 +304,7 @@ def _export_cut_plane(filename, output, width, display, searched_axes, fix_posit
@compute.command(short_help='Compute a cut plane for morphology FILENAME')
@click.argument('filename', type=str, required=True)
@click.option('-o', '--output',
help=('Output name for the JSON file (default=STDOUT)'))
help='Output name for the JSON file (default=STDOUT)')
@click.option('-w', '--width', type=float, default=3,
help='The bin width (in um) of the 1D distributions')
@click.option('-d', '--display', is_flag=True, default=False,
Expand Down
2 changes: 1 addition & 1 deletion neuror/cut_plane/cut_leaves.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ def find_cut_leaves(
# create half spaces
searched_axes = [axis.upper() for axis in searched_axes]
half_spaces = [
HalfSpace(int(axis == "X"), int(axis == "Y"), int(axis == "Z"), 0, upward=(side > 0))
HalfSpace(int(axis == "X"), int(axis == "Y"), int(axis == "Z"), 0, upward=side > 0)
for axis, side in product(searched_axes, searched_half_spaces)
]

Expand Down
9 changes: 6 additions & 3 deletions neuror/cut_plane/detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from scipy import optimize, special

from neuror.cut_plane import legacy_detection, planes
from neuror.exceptions import NeuroRError

L = logging.getLogger(__name__)

Expand Down Expand Up @@ -47,7 +48,8 @@ def __init__(self, coefs: List[float],
elif isinstance(morphology, (str, Path)):
self.morphology = nm.load_morphology(morphology)
elif morphology is not None:
raise Exception(f'Unsupported morphology type: {type(morphology)}')
# pylint: disable=broad-exception-raised
raise NeuroRError(f'Unsupported morphology type: {type(morphology)}')

self.bin_width = bin_width
self.cut_leaves_coordinates = None
Expand Down Expand Up @@ -165,7 +167,7 @@ def find_legacy(cls, neuron, axis):
'''Find the cut points according to the legacy algorithm
As implemented in:
https://bbpcode.epfl.ch/source/xref/platform/BlueRepairSDK/BlueRepairSDK/src/repair.cpp#263
https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/repair.cpp#L263
'''
if not isinstance(neuron, Morphology):
neuron = nm.load_morphology(neuron)
Expand Down Expand Up @@ -279,7 +281,8 @@ def _minimize(x0, points, bin_width):
method='Nelder-Mead')

if result.status:
raise Exception(result.message)
# pylint: disable=broad-exception-raised
raise NeuroRError(result.message)
return result.x


Expand Down
16 changes: 10 additions & 6 deletions neuror/cut_plane/legacy_detection.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'''Module for the legacy cut plane detection.
As implemented in:
https://bbpcode.epfl.ch/source/xref/platform/BlueRepairSDK/BlueRepairSDK/src/repair.cpp#263
https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/repair.cpp#L263
'''
import logging
from collections import defaultdict
Expand All @@ -12,14 +12,15 @@
from neurom.core import Section
from neurom.core.dataformat import COLS

from neuror.exceptions import NeuroRError
from neuror.utils import RepairType, repair_type_map

L = logging.getLogger(__name__)


def children_ids(section):
'''
https://bbpcode.epfl.ch/source/xref/platform/BlueRepairSDK/BlueRepairSDK/src/helper_dendrite.cpp#111
https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#L111
The original code returns the ids of the descendant sections
but this implementation return the Section objects instead.
Expand All @@ -46,7 +47,10 @@ def cut_detect(neuron, cut, offset, axis):
sum_minus += coord

if count_plus == 0 or count_minus == 0:
raise Exception("cut detection warning:one of the sides is empty. can't decide on cut side")
# pylint: disable=broad-exception-raised
raise NeuroRError(
"cut detection warning:one of the sides is empty. can't decide on cut side"
)

if -sum_minus / count_minus > sum_plus / count_plus:
sign = 1
Expand All @@ -63,7 +67,7 @@ def cut_detect(neuron, cut, offset, axis):
def internal_cut_detection(neuron, axis):
'''As in:
https://bbpcode.epfl.ch/source/xref/platform/BlueRepairSDK/BlueRepairSDK/src/repair.cpp#263
https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/repair.cpp#L263
Use cut_detect to get the side of the half space the points live in.
Then mark points which are children of the apical section.
Expand Down Expand Up @@ -102,7 +106,7 @@ def get_obliques(neuron, extended_types):
'''
Returns the oblique roots.
https://bbpcode.epfl.ch/source/xref/platform/BlueRepairSDK/BlueRepairSDK/src/helper_dendrite.cpp#212
https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#L212
'''
return [section for section in iter_sections(neuron)
if (extended_types[section] == RepairType.oblique and
Expand All @@ -111,7 +115,7 @@ def get_obliques(neuron, extended_types):

def cut_mark(sections, cut, offset, side, axis):
'''
https://bbpcode.epfl.ch/source/xref/platform/BlueRepairSDK/BlueRepairSDK/src/helper_dendrite.cpp#654
https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#L654
'''
for sec in sections:
if sec.children:
Expand Down
10 changes: 5 additions & 5 deletions neuror/cut_plane/planes.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ def __str__(self):

def to_json(self):
'''Returns a dict for json serialization'''
return dict(a=self.coefs[A], b=self.coefs[B], c=self.coefs[C], d=self.coefs[D],
comment="Equation: a*X + b*Y + c*Z + d = 0")
return {"a": self.coefs[A], "b": self.coefs[B], "c": self.coefs[C], "d": self.coefs[D],
"comment": "Equation: a*X + b*Y + c*Z + d = 0"}

@property
def normal(self):
Expand Down Expand Up @@ -108,9 +108,9 @@ def __init__(self, a, b, c, d, upward):
def to_json(self):
'''Returns a dict for json serialization'''
inequality = '>' if self.upward else '<'
return dict(a=self.coefs[A], b=self.coefs[B], c=self.coefs[C], d=self.coefs[D],
upward=self.upward,
comment=f"Equation: a*X + b*Y + c*Z + d {inequality} 0")
return {"a": self.coefs[A], "b": self.coefs[B], "c": self.coefs[C], "d": self.coefs[D],
"upward": self.upward,
"comment": f"Equation: a*X + b*Y + c*Z + d {inequality} 0"}

def project_on_directed_normal(self, points):
'''Project on the normal oriented toward the inside of the half-space'''
Expand Down
17 changes: 9 additions & 8 deletions neuror/cut_plane/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,15 @@ def create_plane(pos, quat):
y = [[positif_x[1], positif_y[1]], [negatif_y[1], negatif_x[1]]]
z = [[positif_x[2], positif_y[2]], [negatif_y[2], negatif_x[2]]]

return dict(
z=z,
x=x,
y=y,
showscale=False,
type='surface',
surfacecolor=['green', 'green'], opacity=1
)
return {
"z": z,
"x": x,
"y": y,
"showscale": False,
"type": "surface",
"surfacecolor": ["green", "green"],
"opacity": 1,
}


ROT_X, ROT_Y, ROT_Z = 4, 45, -21
Expand Down
13 changes: 13 additions & 0 deletions neuror/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Define exceptions specific to NeuroR."""


class NeuroRError(Exception):
"""Exceptions raised by NeuroR."""


class CorruptedMorphology(NeuroRError):
"""Exception for morphologies that should not be used."""


class ZeroLengthRootSection(NeuroRError):
"""Exception for morphologies that have zero length root sections."""
Loading

0 comments on commit 71dfd31

Please sign in to comment.