diff --git a/doc/source/api.rst b/doc/source/api.rst index d608c43..f58a732 100644 --- a/doc/source/api.rst +++ b/doc/source/api.rst @@ -18,6 +18,7 @@ API neuror.utils neuror.axon neuror.main + neuror.exceptions neuror.cut_plane.cut_leaves neuror.cut_plane.detection neuror.cut_plane.legacy_detection diff --git a/doc/source/conf.py b/doc/source/conf.py index 6917e20..c789ad6 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -28,6 +28,7 @@ 'sphinx_autorun', 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', + 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.viewcode', 'sphinx.ext.napoleon', @@ -37,10 +38,21 @@ todo_include_todos = True suppress_warnings = ["ref.python"] autosummary_generate = True -autosummary_imported_members = True +autosummary_imported_members = False autodoc_default_options = { 'members': True, 'show-inheritance': True, + 'private-members': ( + '_downstream_pathlength,' # from axon + '_similar_section,' # from axon + '_tree_distance,' # from axon + '_get_sholl_proba,' # from main + '_sanitize_one,' # from sanitize + '_fix_downstream,' # from zero_diameter_fixer + '_fix_in_between,' # from zero_diameter_fixer + '_fix_upstream,' # from zero_diameter_fixer + '_get_cut_leaves,' # from cut_plane.cut_leaves + ) } # Add any paths that contain templates here, relative to this directory. @@ -65,6 +77,14 @@ "repo_name": "BlueBrain/NeuroR" } +intersphinx_mapping = { + "morphio": ("https://morphio.readthedocs.io/en/latest", None), + "neurom": ("https://neurom.readthedocs.io/en/stable", None), + "numpy": ("https://numpy.org/doc/stable/", None), + "pandas": ("https://pandas.pydata.org/docs", None), + "python": ("https://docs.python.org/3", None), +} + # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". diff --git a/neuror/axon.py b/neuror/axon.py index 95add42..9bfb336 100644 --- a/neuror/axon.py +++ b/neuror/axon.py @@ -14,19 +14,27 @@ def _tree_distance(sec1, sec2): - ''' - Returns the number of sections between the 2 sections + '''Returns the number of sections between the 2 sections. + + Args: + sec1 (~neurom.core.morphology.Section): the first section + sec2 (~neurom.core.morphology.Section): the second section - Reimplementation of: - https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_axon.cpp#L35 + Return: + int: The number of sections - raises: if both sections are not part of the same neurite + Raises: + NeuroRError: if both sections are not part of the same neurite. - Note: - I think the implementation of tree distance is true to the original - but I would expect the tree distance of 2 children with the same parent to be 2 and not 1 - Because in the current case, (root, child1) and (child1, child2) have the - same tree distance and it should probably not be the case + .. note:: + This is a re-implementation of: + https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_axon.cpp#L35 + + .. note:: + I think the implementation of tree distance is ``True`` to the original + but I would expect the tree distance of 2 children with the same parent to be 2 and not 1 + Because in the current case, ``(root, child1)`` and ``(child1, child2)`` have the + same tree distance and it should probably not be the case ''' original_sections = (sec1, sec2) dist = 0 @@ -59,11 +67,11 @@ def _tree_distance(sec1, sec2): def _downstream_pathlength(section): - '''The sum of this section and its descendents's pathlengths - - Reimplementation of the C++ function "children_length": + '''The sum of this section and its descendents's pathlengths. - https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/morphstats.cpp#L112 + .. note:: + This is a re-implementation of the C++ function "children_length": + https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/morphstats.cpp#L112 ''' ret = section_length(section) for child in section.children: @@ -72,12 +80,14 @@ def _downstream_pathlength(section): def _similar_section(intact_axons, section): - '''Use the "mirror" technique of BlueRepairSDK to find out the similar section + '''Use the "mirror" technique of BlueRepairSDK to find out the similar section. - https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_axon.cpp#L83 + .. note:: + This is a re-implementation of: + 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 + .. warning:: + I have **absolutely** no clue why sorting by this metric ''' dists = [] for root in intact_axons: @@ -91,7 +101,7 @@ def _similar_section(intact_axons, section): def _sort_intact_sections_by_score(section, similar_section, axon_branches): - '''Returns an array of sections sorted by their score''' + '''Returns an array of sections sorted by their score.''' reference = _downstream_pathlength(similar_section) - section_length(section) def score(branch): @@ -102,10 +112,7 @@ def score(branch): def repair(morphology, section, intact_sections, axon_branches, used_axon_branches, y_extent): - '''Axonal repair - - Reimplementation of: - https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/repair.cpp#L727 + '''Axonal repair. 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 @@ -115,22 +122,25 @@ def repair(morphology, section, intact_sections, axon_branches, used_axon_branch 5) Mark this section as used and do not re-use it Args: - morphology (neurom.Neuron): the morphology to repair - section (neurom.core._neuron.Section): the section to repair - intact_sections (List[Section]): a list of all sections from this morphology - that are part of an intact subtree. Note: these section won't be grafted - axon_branches (List[Section]): a list a intact sections coming from donor morphologies - These are the sections that will be appended - - ..note:: - The original code used to have more parameters. In the context of the bbp-morphology-workflow - it seems that some of the parameters were always used with the same value. This - reimplementation assumes the following BlueRepairSDK options: - - - --overlap=true - - --incremental=false - - --restrict=true - - --distmethod=mirror' + morphology (~neurom.core.morphology.Morphology): the morphology to repair + section (neurom.core.morphology.Section): the section to repair + intact_sections (List[~neurom.core.morphology.Section]): a list of all sections from this + morphology that are part of an intact subtree. Note: these section won't be grafted. + axon_branches (List[neurom.core.morphology.Section]): a list a intact sections coming from + donor morphologies. These are the sections that will be appended + + .. note:: + This is a re-implementation of: + https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/repair.cpp#L727 + + The original code used to have more parameters. In the context of the + bbp-morphology-workflow it seems that some of the parameters were always used with the + same value. This re-implementation assumes the following BlueRepairSDK options: + + - ``--overlap=true`` + - ``--incremental=false`` + - ``--restrict=true`` + - ``--distmethod=mirror`` ''' if not intact_sections: diff --git a/neuror/cli.py b/neuror/cli.py index a938128..19b465d 100644 --- a/neuror/cli.py +++ b/neuror/cli.py @@ -24,19 +24,19 @@ @click.option('-v', '--verbose', count=True, default=0, help='-v for INFO, -vv for DEBUG') def cli(verbose): - '''The CLI entry point''' + '''The CLI entry point.''' level = (logging.WARNING, logging.INFO, logging.DEBUG)[min(verbose, 2)] L.setLevel(level) @cli.group() def unravel(): - '''CLI utilities related to unravelling''' + '''CLI utilities related to unravelling.''' @cli.group() def cut_plane(): - '''CLI utilities related to cut-plane repair''' + '''CLI utilities related to cut-plane repair.''' @cli.group() @@ -50,17 +50,17 @@ def sanitize(): @cut_plane.group() def compute(): - '''CLI utilities to detect cut planes''' + '''CLI utilities to detect cut planes.''' @cut_plane.group() def repair(): - '''CLI utilities to repair cut planes''' + '''CLI utilities to repair cut planes.''' @cli.group() def error_annotation(): - '''CLI utilities related to error annotations''' + '''CLI utilities related to error annotations.''' @error_annotation.command(short_help='Annotate errors on a morphology') @@ -125,7 +125,7 @@ def folder(input_dir, output_dir, error_summary_file, marker_file): help=('Path to a CSV whose columns represents the X, Y and Z ' 'coordinates of points from which to start the repair')) def file(input_file, output_file, plot_file, axon_donor, cut_file): - '''Repair dendrites of a cut neuron''' + '''Repair dendrites of a cut neuron.''' import pandas from neuror.main import repair # pylint: disable=redefined-outer-name @@ -151,7 +151,7 @@ def file(input_file, output_file, plot_file, axon_donor, cut_file): help=('A dir with the cut points CSV file for each morphology. ' 'See also "neuror cut-plane repair file --help".')) def folder(input_dir, output_dir, plot_dir, axon_donor, cut_file_dir): - '''Repair dendrites of all neurons in a directory''' + '''Repair dendrites of all neurons in a directory.''' from neuror.full import repair_all repair_all(input_dir, output_dir, axons=axon_donor, cut_points_dir=cut_file_dir, plots_dir=plot_dir) @@ -165,7 +165,7 @@ def folder(input_dir, output_dir, plot_dir, axon_donor, cut_file_dir): 'and after unravelling')) @click.option('--window-half-length', default=DEFAULT_WINDOW_HALF_LENGTH) def file(input_file, output_file, mapping_file, window_half_length): - '''Unravel a cell''' + '''Unravel a cell.''' from neuror.unravel import unravel # pylint: disable=redefined-outer-name neuron, mapping = unravel(input_file, window_half_length=window_half_length) neuron.write(output_file) @@ -194,7 +194,7 @@ def folder(input_dir, output_dir, raw_planes_dir, unravelled_planes_dir, window_ @click.argument('folders', nargs=-1) @click.option('--title', '-t', multiple=True) def report(folders, title): - '''Generate a PDF with plots of pre and post repair neurons''' + '''Generate a PDF with plots of pre and post repair neurons.''' from neuror.view import view_all if not folders: print('Need to pass at least one folder') @@ -211,7 +211,7 @@ def report(folders, title): @click.argument('input_file') @click.argument('output_file') def zero_diameters(input_file, output_file): - '''Output a morphology where the zero diameters have been removed''' + '''Output a morphology where the zero diameters have been removed.''' from neuror.zero_diameter_fixer import fix_zero_diameters neuron = Morphology(input_file) fix_zero_diameters(neuron) @@ -241,11 +241,11 @@ def folder(input_folder, output_folder, nprocesses): @cut_plane.group() def compute(): - '''CLI utilities to compute cut planes''' + '''CLI utilities to compute cut planes.''' def _check_results(result): - '''Check the result status''' + '''Check the result status.''' if not result: L.error('Empty results') return -1 @@ -267,7 +267,7 @@ def hint(filename): Example:: - cut-plane compute hint ./tests/data/Neuron_slice.h5 + neuror cut-plane compute hint ./tests/data/Neuron_slice.h5 """ from neuror.cut_plane.viewer import app, set_neuron set_neuron(filename) @@ -352,7 +352,7 @@ def file(filename, output, width, display, plane, position): Example:: - cut-plane compute file -d tests/data/Neuron_slice.h5 -o my-plane.json -w 10 + neuror cut-plane compute file -d tests/data/Neuron_slice.h5 -o my-plane.json -w 10 ''' _export_cut_plane(filename, output, width, display, plane or ('x', 'y', 'z'), position) @@ -391,7 +391,7 @@ def join(out_filename, plane_paths): Example:: - cut-plane join result.json plane1.json plane2.json plane3.json + neuror cut-plane join result.json plane1.json plane2.json plane3.json ''' data = [] for plane in plane_paths: diff --git a/neuror/cut_plane/cut_leaves.py b/neuror/cut_plane/cut_leaves.py index 60967af..dae7afb 100644 --- a/neuror/cut_plane/cut_leaves.py +++ b/neuror/cut_plane/cut_leaves.py @@ -1,5 +1,8 @@ """Detect cut leaves with new algo.""" from itertools import product +from typing import List + +import morphio import numpy as np from neurom.core.dataformat import COLS from neuror.cut_plane.planes import HalfSpace @@ -15,8 +18,8 @@ def _get_cut_leaves(half_space, morphology, bin_width, percentile_threshold): Args: half_space (planes.HalfSpace): half space to search cut points morphology (morphio.Morphology): morphology - bin_width: the bin width - percentile_threshold: the minimum percentile of leaves counts in bins + bin_width (float): the bin width + percentile_threshold (float): the minimum percentile of leaves counts in bins Returns: leaves: ndarray of dim (n, 3) with cut leaves coordinates @@ -46,11 +49,11 @@ def _get_cut_leaves(half_space, morphology, bin_width, percentile_threshold): def find_cut_leaves( - morph, - bin_width=3, - percentile_threshold=70.0, - searched_axes=("Z",), - searched_half_spaces=(-1, 1), + morph: morphio.Morphology, + bin_width: float = 3, + percentile_threshold: float = 70.0, + searched_axes: List[str] = ("Z",), + searched_half_spaces: List[float] = (-1, 1), ): """Find all cut leaves for cuts with strong signal for real cut. @@ -66,7 +69,7 @@ def find_cut_leaves( Note that all cuts can be valid, thus cut leaves can be on both sides. Args: - morph (morphio.Morphology): morphology + morph: morphology bin_width: the bin width percentile_threshold: the minimum percentile of leaves counts in bins searched_axes: x, y or z. Specify the half space for which to search the cut leaves diff --git a/neuror/cut_plane/detection.py b/neuror/cut_plane/detection.py index d01226f..1d2a334 100644 --- a/neuror/cut_plane/detection.py +++ b/neuror/cut_plane/detection.py @@ -21,11 +21,11 @@ class CutPlane(planes.HalfSpace): - '''The cut plane class + '''The cut plane class. It is composed of a HalfSpace and a morphology The morphology is part of the HalfSpace, the cut space is - the complementary HalfSpace + the complementary HalfSpace. ''' def __init__(self, coefs: List[float], @@ -35,9 +35,9 @@ def __init__(self, coefs: List[float], '''Cut plane ctor. Args: - coefs: the [abcd] coefficients of a plane equation: a X + b Y + c Z + d = 0 - upward: if true, the morphology points satisfy: a X + b Y + c Z + d > 0 - else, they satisfy: a X + b Y + c Z + d < 0 + coefs: the [abcd] coefficients of a plane equation: ``a X + b Y + c Z + d = 0`` + upward: if ``True``, the morphology points satisfy: ``a X + b Y + c Z + d > 0`` + else, they satisfy: ``a X + b Y + c Z + d < 0`` morphology: the morphology bin_width: the bin width ''' @@ -48,7 +48,6 @@ def __init__(self, coefs: List[float], elif isinstance(morphology, (str, Path)): self.morphology = nm.load_morphology(morphology) elif morphology is not None: - # pylint: disable=broad-exception-raised raise NeuroRError(f'Unsupported morphology type: {type(morphology)}') self.bin_width = bin_width @@ -62,10 +61,13 @@ def __init__(self, coefs: List[float], @classmethod def from_json(cls, cut_plane_obj, morphology=None): - '''Factory constructor from a JSON file + '''Factory constructor from a JSON file. - cut_plane_obj (dict|str|pathlib.Path): a cut plane - It can be a python dictionary or a path to a json file that contains one + Args: + cut_plane_obj (dict|str|pathlib.Path): a cut plane + It can be a python dictionary or a path to a json file that contains one + morphology (~neurom.core.morphology.Morphology): the morphology passed to the + :class:`CutPlane` object ''' assert isinstance(cut_plane_obj, (str, dict, Path)) @@ -95,8 +97,8 @@ def find(cls, neuron, bin_width=3, searched_axes=('X', 'Y', 'Z'), searched_half_spaces=(-1, 1), fix_position=None): - """ - Find and return the cut plane that is oriented along X, Y or Z. + """Find and return the cut plane that is oriented along X, Y or Z. + 6 potential positions are considered: for each axis, they correspond to the coordinate of the first and last point of the neuron. @@ -114,7 +116,7 @@ def find(cls, neuron, bin_width=3, corresponds to the cut plane Args: - neuron (Neuron|str|pathlib.Path): a Neuron object or path + neuron (~neurom.core.morphology.Morphology|str|pathlib.Path): a morphology bin_width: The size of the binning display: where or not to display the control plots @@ -281,7 +283,6 @@ def _minimize(x0, points, bin_width): method='Nelder-Mead') if result.status: - # pylint: disable=broad-exception-raised raise NeuroRError(result.message) return result.x @@ -305,7 +306,7 @@ def plot(neuron, result, inline=False): '''Plot the neuron, the cut plane and the cut leaves. Args: - neuron (Neuron): the neuron to be plotted + neuron (~neurom.core.morphology.Morphology): the neuron to be plotted result (dict): the cut plane object in dictionary form inline (bool): if True, plot as an interactive plot (for example in a Jupyter notebook) ''' diff --git a/neuror/cut_plane/legacy_detection.py b/neuror/cut_plane/legacy_detection.py index 563add2..49b8345 100644 --- a/neuror/cut_plane/legacy_detection.py +++ b/neuror/cut_plane/legacy_detection.py @@ -19,8 +19,11 @@ def children_ids(section): - ''' - https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#L111 + '''Return the children IDs. + + .. note:: + This is a re-implementation of: + 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. @@ -29,7 +32,7 @@ def children_ids(section): def cut_detect(neuron, cut, offset, axis): - '''Detect the cut leaves the old way + '''Detect the cut leaves the old way. The cut leaves are simply the leaves that live on the half-space (split along the 'axis' coordinate) @@ -47,7 +50,6 @@ def cut_detect(neuron, cut, offset, axis): sum_minus += coord if count_plus == 0 or count_minus == 0: - # pylint: disable=broad-exception-raised raise NeuroRError( "cut detection warning:one of the sides is empty. can't decide on cut side" ) @@ -65,14 +67,14 @@ def cut_detect(neuron, cut, offset, axis): def internal_cut_detection(neuron, axis): - '''As in: + '''Use cut_detect to get the side of the half space the points live in. - 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. -''' + .. note:: + This is a re-implementation of: + https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/repair.cpp#L263 + ''' axis = {'x': COLS.X, 'y': COLS.Y, 'z': COLS.Z}[axis.lower()] cut = defaultdict(lambda key: False) @@ -103,10 +105,11 @@ def internal_cut_detection(neuron, axis): def get_obliques(neuron, extended_types): - ''' - Returns the oblique roots. + '''Returns the oblique roots. - https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#L212 + .. note:: + This is a re-implementation of: + 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 @@ -114,8 +117,11 @@ def get_obliques(neuron, extended_types): def cut_mark(sections, cut, offset, side, axis): - ''' - https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#L654 + '''Find which sections should be cut. + + .. note:: + This is a re-implementation of: + https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#L654 ''' for sec in sections: if sec.children: diff --git a/neuror/cut_plane/planes.py b/neuror/cut_plane/planes.py index 508eba6..f90b78e 100644 --- a/neuror/cut_plane/planes.py +++ b/neuror/cut_plane/planes.py @@ -8,7 +8,7 @@ def _get_displaced_pos(pos, quat, size_multiplier, axis): - """ Compute the shifted position wrt the quaternion and axis """ + """Compute the shifted position wrt the quaternion and axis.""" return pos + size_multiplier * np.array(quat.rotate(axis)) @@ -16,7 +16,8 @@ class PlaneEquation(object): '''This class defines the equation of a plane. It is a mathematical object which is domain agnostic. - a X + b Y + c Z + d = 0 + .. math:: + a X + b Y + c Z + d = 0 ''' def __init__(self, a, b, c, d): @@ -26,18 +27,16 @@ def __init__(self, a, b, c, d): @classmethod def from_dict(cls, data): - '''Instantiate object from dict like: - {"a": 1, "b": 2, "c": 0, "d": 10} - ''' + '''Instantiate object from dict like: ``{"a": 1, "b": 2, "c": 0, "d": 10}``.''' return cls(data['a'], data['b'], data['c'], data['d']) @classmethod def from_rotations_translations(cls, transformations): '''Factory method to build the plane equation from the rotation and translation - provided by the cut_plane.viewer + provided by the :mod:`~neuror.cut_plane.viewer`. Args: - transformations: An array [rot_x, rot_y, rot_z, transl_x, transl_y, transl_z] + transformations: An array ``[rot_x, rot_y, rot_z, transl_x, transl_y, transl_z]`` ''' rot_x, rot_y, rot_z, transl_x, transl_y, transl_z = transformations qx = Quaternion(axis=[1, 0, 0], angle=rot_x / 180. * np.pi).unit @@ -52,7 +51,7 @@ def from_rotations_translations(cls, transformations): -transl.dot(normal_vector)) def distance(self, points): - '''Returns an array containing the distance between the plane and each point''' + '''Returns an array containing the distance between the plane and each point.''' points = np.array(points) u = self.coefs[ABC].copy() return np.abs(points.dot(u) + self.coefs[D]) / np.linalg.norm(u) @@ -65,17 +64,17 @@ def __str__(self): ) def to_json(self): - '''Returns a dict for json serialization''' + '''Returns a dict for json serialization.''' 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): - '''Returns the vector orthogonal to the plane''' + '''Returns the vector orthogonal to the plane.''' return self.coefs[ABC] / np.linalg.norm(self.coefs[ABC]) def project_on_normal(self, points): - '''Returns the points projected on the normal vector''' + '''Returns the points projected on the normal vector.''' if self.coefs[A]: point_in_the_plane = np.array([-self.coefs[D] / self.coefs[A], 0, 0]) elif self.coefs[B]: @@ -86,7 +85,7 @@ def project_on_normal(self, points): return points[:, COLS.XYZ].dot(self.normal) - point_in_the_plane.dot(self.normal) def count_near_plane(self, points, bin_width): - '''Return the number of points in ]-bin_width, 0] and ]0, bin_width]''' + '''Return the number of points in ``]-bin_width, 0]`` and ``]0, bin_width]``.''' points = self.project_on_normal(points) n_left = len(points[(points > -bin_width) & (points <= 0)]) n_right = len(points[(points > 0) & (points <= bin_width)]) @@ -94,11 +93,11 @@ def count_near_plane(self, points, bin_width): class HalfSpace(PlaneEquation): - ''' - A mathematical half-space: https://en.wikipedia.org/wiki/Half-space_(geometry) + '''A mathematical half-space: https://en.wikipedia.org/wiki/Half-space_(geometry). - a X + b Y + c Z + d > 0 if upward == True - a X + b Y + c Z + d < 0 else + .. math:: + a X + b Y + c Z + d > 0 if upward == True + a X + b Y + c Z + d < 0 else ''' def __init__(self, a, b, c, d, upward): @@ -106,14 +105,14 @@ def __init__(self, a, b, c, d, upward): self.upward = upward def to_json(self): - '''Returns a dict for json serialization''' + '''Returns a dict for json serialization.''' inequality = '>' if self.upward else '<' 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''' + '''Project on the normal oriented toward the inside of the half-space.''' points = self.project_on_normal(points) if self.upward: return points diff --git a/neuror/cut_plane/viewer.py b/neuror/cut_plane/viewer.py index 2dec6a7..42b8802 100644 --- a/neuror/cut_plane/viewer.py +++ b/neuror/cut_plane/viewer.py @@ -1,5 +1,5 @@ '''App to find cut planes with arbitrary cut planes orientations -with the help of a manual hint +with the help of a manual hint. Related to https://bbpteam.epfl.ch/project/issues/browse/NGV-85 ''' @@ -34,10 +34,10 @@ class NumpyEncoder(json.JSONEncoder): - '''JSON encoder that handles numpy types + '''JSON encoder that handles numpy types. - In python3, numpy.dtypes don't serialize to correctly, so a custom converter - is needed. + In python3, `numpy types `_ don't + serialize to correctly, so a custom converter is needed. ''' def default(self, o): # pylint: disable=method-hidden @@ -51,16 +51,14 @@ def default(self, o): # pylint: disable=method-hidden def create_plane(pos, quat): - """ Create a 3d plane using a center position and a quaternion for orientation + """Create a 3d plane using a center position and a quaternion for orientation. Args : pos: x,y,z position of the plane's center (array([x,y,z])) quat: quaternion representing the orientations (Quaternion) - size_multiplier: plane size in space coordinates (float) - opacity: set the opacity value (float) Returns : - A square surface to the plotly format + dict: A square surface to the plotly format """ length = np.linalg.norm(BBOX[1] - BBOX[0]) / 2. @@ -162,7 +160,7 @@ def create_plane(pos, quat): def set_neuron(filename): - '''Globally loads the neuron''' + '''Globally loads the neuron.''' global NEURON, FIGURE, BBOX # pylint: disable=global-statement NEURON = load_morphology(filename) FIGURE = NeuronBuilder(NEURON, '3d').get_figure() @@ -184,7 +182,7 @@ def set_neuron(filename): ) def display_click_data(rot_x, rot_y, rot_z, transl_x, transl_y, transl_z, hide, layout): - '''callback that redraw everything when sliders are changed''' + '''Callback that redraw everything when sliders are changed.''' qx = Quaternion(axis=[1, 0, 0], angle=rot_x / 180. * np.pi) qy = Quaternion(axis=[0, 1, 0], angle=rot_y / 180. * np.pi) qz = Quaternion(axis=[0, 0, 1], angle=rot_z / 180. * np.pi) @@ -213,7 +211,7 @@ def display_click_data(rot_x, rot_y, rot_z, transl_x, transl_y, transl_z, hide, Input('translate-z-slider', 'value'), ]) def update_output(rot_x, rot_y, rot_z, transl_x, transl_y, transl_z): - '''Update histo when sliders are changed''' + '''Update histo when sliders are changed.''' transformations = [rot_x, rot_y, rot_z, transl_x, transl_y, transl_z] bin_width = 10 cut_plane = CutPlane.from_rotations_translations(transformations, NEURON, bin_width) @@ -264,7 +262,7 @@ def update_output(rot_x, rot_y, rot_z, transl_x, transl_y, transl_z): ] ) def optimize(n_clicks, rot_x, rot_y, rot_z, transl_x, transl_y, transl_z): - '''Optimize cut plane parameters''' + '''Optimize cut plane parameters.''' if not n_clicks: return rot_x, rot_y, rot_z, transl_x, transl_y, transl_z points = np.array([point @@ -280,7 +278,7 @@ def optimize(n_clicks, rot_x, rot_y, rot_z, transl_x, transl_y, transl_z): Output('rotate-x-slider', 'value'), [Input('optimized', 'data-*')]) def update_post_optim_x_rotate(params): - '''callback''' + '''Dash callback.''' return params[0] @@ -288,7 +286,7 @@ def update_post_optim_x_rotate(params): Output('rotate-y-slider', 'value'), [Input('optimized', 'data-*')]) def update_post_optim_y_rotate(params): - '''callback''' + '''Dash callback.''' return params[1] @@ -296,7 +294,7 @@ def update_post_optim_y_rotate(params): Output('rotate-z-slider', 'value'), [Input('optimized', 'data-*')]) def update_post_optim_z_rotate(params): - '''callback''' + '''Dash callback.''' return params[2] @@ -304,7 +302,7 @@ def update_post_optim_z_rotate(params): Output('translate-x-slider', 'value'), [Input('optimized', 'data-*')]) def update_post_optim_x_translate(params): - '''callback''' + '''Dash callback.''' return params[3] @@ -312,7 +310,7 @@ def update_post_optim_x_translate(params): Output('translate-y-slider', 'value'), [Input('optimized', 'data-*')]) def update_post_optim_y_translate(params): - '''callback''' + '''Dash callback.''' return params[4] @@ -320,7 +318,7 @@ def update_post_optim_y_translate(params): Output('translate-z-slider', 'value'), [Input('optimized', 'data-*')]) def update_post_optim_z_translate(params): - '''callback''' + '''Dash callback.''' return params[5] @@ -335,7 +333,7 @@ def update_post_optim_z_translate(params): State('translate-z-slider', 'value'), State('export-path-input', 'value')]) def export(n_clicks, rot_x, rot_y, rot_z, transl_x, transl_y, transl_z, output_path): - '''Write the final file cut-plane.json to disk''' + '''Write the final file cut-plane.json to disk.''' if not n_clicks: return plane = CutPlane.from_rotations_translations( diff --git a/neuror/full.py b/neuror/full.py index 692d6fe..6d3a95e 100644 --- a/neuror/full.py +++ b/neuror/full.py @@ -1,6 +1,4 @@ -''' -The module to run the full repair -''' +'''The module to run the full repair.''' import logging from pathlib import Path @@ -13,7 +11,7 @@ def repair_all(input_dir, output_dir, seed=0, axons=None, cut_points_dir=None, plots_dir=None): - '''Repair all morphologies in input folder''' + '''Repair all morphologies in input folder.''' for inputfilename in iter_morphology_files(input_dir): outfilename = Path(output_dir, inputfilename.name) if cut_points_dir: diff --git a/neuror/main.py b/neuror/main.py index 8f0d81d..978567b 100644 --- a/neuror/main.py +++ b/neuror/main.py @@ -19,7 +19,7 @@ from morphio import PointLevel, SectionType from neurom import NeuriteType, iter_neurites, iter_sections, load_morphology from neurom.core.dataformat import COLS -from neurom.core import Section +from neurom.core import Neurite, Section from neurom.features.section import branch_order, section_path_length from nptyping import NDArray, Shape, Float from scipy.spatial.distance import cdist @@ -38,6 +38,7 @@ 'children_diameter_ratio': 0.8, # 1: child diam = parent diam, 0: child diam = tip diam 'tip_percentile': 25, # percentile of tip radius distributions to use as tip radius } +"""The default parameters used by :class:`Repair`.""" _PARAM_SCHEMA = { "type": "object", @@ -88,6 +89,7 @@ "tip_percentile", ] } +"""The schema used to validate the parameters passed to :class:`Repair`.""" # Epsilon can not be to small otherwise leaves stored in json files # are not found in the NeuroM neuron @@ -97,50 +99,57 @@ class Action(Enum): - '''To bifurcate or not to bifurcate ?''' + '''To bifurcate or not to bifurcate?''' BIFURCATION = 1 CONTINUATION = 2 TERMINATION = 3 def is_cut_section(section, cut_points): - '''Return true if the section is close from the cut plane''' + '''Return true if the section is close from the cut plane.''' if cut_points.size == 0 or section.points.size == 0: return False return np.min(cdist(section.points[:, COLS.XYZ], cut_points)) < EPSILON def is_branch_intact(branch, cut_points): - '''Does the branch have leaves belonging to the cut plane ?''' + '''Does the branch have leaves belonging to the cut plane?''' return all(not is_cut_section(section, cut_points) for section in branch.ipreorder()) def _get_sholl_layer(section, origin, sholl_layer_size): - '''Returns this section sholl layer''' + '''Returns this section sholl layer.''' return int(np.linalg.norm(section.points[-1, COLS.XYZ] - origin) / sholl_layer_size) -def _get_sholl_proba(sholl_data, section_type, sholl_layer, pseudo_order): +def _get_sholl_proba( + sholl_data: dict, section_type: SectionType, sholl_layer: int, pseudo_order: int +) -> Dict[Action, float]: '''Return the probabilities of bifurcation, termination and bifurcation in a dictionnary for the given sholl layer and branch order. If no data are available for this branch order, the action_counts are averaged on all branch orders for this sholl layer - Args: - sholl_data: nested dict that stores the number of section per SectionType, sholl order - sholl layer and Action type + sholl_data: nested dict that stores the number of section per SectionType, sholl + order, sholl layer and Action type + sholl_data[neurite_type][layer][order][action_type] = counts - section_type (SectionType): section type - sholl_layer (int): sholl layer - pseudo_order (int): pseudo order + section_type: section type + sholl_layer: sholl layer + pseudo_order: pseudo order Returns: - Dict[Action, float]: probability of each action + Probability of each action + + .. note:: + This is based on + https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#n398 - Note: based on https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#n398 # noqa, pylint: disable=line-too-long - Note2: OrderedDict ensures the reproducibility of np.random.choice outcome + .. note:: + :class:`~collections.OrderedDict` ensures the reproducibility of + :func:`numpy.random.choice` outcome. ''' section_type_data = sholl_data[section_type] @@ -155,7 +164,8 @@ def _get_sholl_proba(sholl_data, section_type, sholl_layer, pseudo_order): action_counts = data_layer[pseudo_order] except KeyError: # No data for this order. Average on all orders for this layer - # As done in https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#n426 # noqa, pylint: disable=line-too-long + # As done in + # https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#n426 action_counts = Counter() for data in data_layer.values(): @@ -176,9 +186,11 @@ def _get_sholl_proba(sholl_data, section_type, sholl_layer, pseudo_order): def _grow_until_sholl_sphere( section, origin, sholl_layer, params, taper, tip_radius, current_trunk_radius ): - '''Grow until reaching next sholl layer + '''Grow until reaching next sholl layer. - Note: based on https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#n363 # noqa, pylint: disable=line-too-long + .. note:: + This is based on + https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#n363 ''' backwards_sections = 0 while _get_sholl_layer( @@ -206,9 +218,11 @@ def _last_segment_vector(section, normalized=False): def _branching_angles(section, order_offset=0): '''Return a list of 2-tuples. The first element is the branching order and the second one is - the angles between the direction of the section and its children's ones + the angles between the direction of the section and its children's ones. - Note: based on https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/morphstats.cpp#n194 # noqa, pylint: disable=line-too-long + .. note:: + This is based on + https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/morphstats.cpp#n194 ''' if section_length(section) < EPSILON: return [] @@ -226,9 +240,11 @@ def _branching_angles(section, order_offset=0): def _continuation(sec, origin, params, taper, tip_radius): - '''Continue growing the section + '''Continue growing the section. - Note: based on https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#L241 # noqa, pylint: disable=line-too-long + .. note:: + This is based on + https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#L241 ''' # The following lines is from BlueRepairSDK's code but I'm not # convinced by its relevance @@ -268,36 +284,27 @@ def _continuation(sec, origin, params, taper, tip_radius): def _y_cylindrical_extent(section): - '''Returns the distance from the last section point to the origin in the XZ plane''' + '''Returns the distance from the last section point to the origin in the XZ plane.''' xz_last_point = section.points[-1, [0, 2]] return np.linalg.norm(xz_last_point) def _max_y_dendritic_cylindrical_extent(neuron): - '''Return the maximum distance of dendritic section ends and the origin in the XZ plane''' + '''Return the maximum distance of dendritic section ends and the origin in the XZ plane.''' return max((_y_cylindrical_extent(section) for section in neuron.iter() if section.type in {SectionType.basal_dendrite, SectionType.apical_dendrite}), default=0) class Repair(object): - '''The repair class''' + '''Repair the input morphology. - def __init__(self, # pylint: disable=too-many-arguments - inputfile: Path, - axons: Optional[Path] = None, - seed: Optional[int] = 0, - cut_leaves_coordinates: Optional[NDArray[Shape["3"], Any]] = None, - legacy_detection: bool = False, - repair_flags: Optional[Dict[RepairType, bool]] = None, - apical_point: NDArray[Shape["3"], Float] = None, - params: Dict = None, - validate_params=False): - '''Repair the input morphology + The repair algorithm uses sholl analysis of intact branches to grow new branches from cut + leaves. The algorithm is fairly complex, but can be controled via a few parameters in the + ``params`` dictionary. By default, they are: + + .. code-block:: python - The repair algorithm uses sholl analysis of intact branches to grow new branches from cut - leaves. The algorithm is fairly complex, but can be controled via a few parameters in the - params dictionary. By default, they are: _PARAMS = { 'seg_length': 5.0, # lenghts of new segments 'sholl_layer_size': 10, # resolution of the sholl profile @@ -309,21 +316,35 @@ def __init__(self, # pylint: disable=too-many-arguments 'tip_percentile': 25, # percentile of tip radius distributions to use as tip radius } - Args: - inputfile: the input neuron to repair - axons: donor axons whose section will be used to repair this axon - seed: the numpy seed - cut_leaves_coordinates: List of 3D coordinates from which to start the repair - legacy_detection: if True, use the legacy cut plane detection - (see neuror.legacy_detection) - repair_flags: a dict of flags where key is a RepairType and value is whether - it should be repaired or not. If not provided, all types will be repaired. - apical_point: 3d vector for apical point, else, the automatic apical detection is used - if apical_point == -1, no automatic detection will be tried - params: repair internal parameters (see comments in code for details) - - Note: based on https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/repair.cpp#L469 # noqa, pylint: disable=line-too-long - ''' + Args: + inputfile: the input neuron to repair + axons: donor axons whose section will be used to repair this axon + seed: the numpy seed + cut_leaves_coordinates: List of 3D coordinates from which to start the repair + legacy_detection: if True, use the legacy cut plane detection + (see :mod:`neuror.cut_plane.legacy_detection`) + repair_flags: a dict of flags where key is a :class:`neuror.utils.RepairType` and value is + whether it should be repaired or not. If not provided, all types will be repaired. + apical_point: 3d vector for apical point, else, the automatic apical detection is used + if ``apical_point == -1``, no automatic detection will be tried + params: repair internal parameters (see comments in code for details) + validate_params: if set to ``True``, the given parameters are validated before processing + + .. note:: + This class is based on + https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/repair.cpp#L469 + ''' + + def __init__(self, # pylint: disable=too-many-arguments + inputfile: Path, + axons: Optional[Path] = None, + seed: Optional[int] = 0, + cut_leaves_coordinates: Optional[NDArray[Shape["3"], Any]] = None, + legacy_detection: bool = False, + repair_flags: Optional[Dict[RepairType, bool]] = None, + apical_point: NDArray[Shape["3"], Float] = None, + params: Dict = None, + validate_params=False): np.random.seed(seed) self.legacy_detection = legacy_detection self.inputfile = inputfile @@ -400,7 +421,12 @@ def validate_params(params): def run(self, outputfile: Path, plot_file: Optional[Path] = None): - '''Run''' + '''Run repair and export the result. + + Args: + outputfile: path to the output morphology + plot_file: path to the output figure + ''' if self.cut_leaves.size == 0: L.warning('No cut leaves. Nothing to repair for morphology %s', self.inputfile) self.neuron.write(outputfile) @@ -478,13 +504,13 @@ def run(self, L.debug('Repair successful for %s', self.inputfile) def _find_intact_obliques(self): - ''' - Find root sections of all intact obliques + '''Find root sections of all intact obliques. - Root obliques are obliques with a section parent of type 'trunk' + Root obliques are obliques with a section parent of type 'trunk'. - Note: based on - https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#n193 + .. note:: + This is based on + https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#n193 ''' root_obliques = (section for section in iter_sections(self.neuron) if (self.repair_type_map[section] == RepairType.oblique and @@ -495,7 +521,7 @@ def _find_intact_obliques(self): return intacts def _find_intact_sub_trees(self): - '''Returns intact neurites + '''Returns intact neurites. There is a fallback mechanism in case there are no intact basals: https://bbpcode.epfl.ch/source/xref/platform/BlueRepairSDK/BlueRepairSDK/src/repair.cpp#658 @@ -521,16 +547,18 @@ def _find_intact_sub_trees(self): return basals + obliques + axons + tufts - def _intact_branching_angles(self, branches): - ''' - Returns lists of branching angles stored in a nested dict + def _intact_branching_angles( + self, branches: List[Neurite] + ) -> Dict[SectionType, Dict[int, List[int]]]: + '''Returns lists of branching angles stored in a nested dict. + 1st key: section type, 2nd key: branching order Args: - branches (List[Neurite]) + branches: the input branches Returns: - Dict[SectionType, Dict[int, List[int]]]: Branching angles + Branching angles ''' res = defaultdict(lambda: defaultdict(list)) for branch in branches: @@ -546,7 +574,9 @@ def _best_case_angle_data(self, section_type, branching_order): If no data are available, fallback on aggregate data - ..note:: based on https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#n329 # noqa, pylint: disable=line-too-long + .. note:: + This is based on + https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#n329 ''' angles = self.info['intact_branching_angles'][section_type] return angles[branching_order] or list(chain.from_iterable(angles.values())) @@ -589,10 +619,12 @@ def _compute_sholl_data(self, branches): data[neurite_type][layer][order][action_type] = counts Args: - branches: a collection of Neurite or Section that will be traversed + branches: a collection of :class:`~neurom.core.morphology.Neurite` or + :class:`~neurom.core.morphology.Section` that will be traversed - Note: This is based on - https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/morphstats.cpp#n93 + .. note:: + This is based on + https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/morphstats.cpp#n93 ''' data = defaultdict(lambda: defaultdict(dict)) @@ -636,7 +668,9 @@ def _compute_sholl_data(self, branches): def _bifurcation(self, section, order_offset): '''Create 2 children at the end of the current section - Note: based on https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#n287 # noqa, pylint: disable=line-too-long + .. note:: + This is based on + https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#n287 ''' current_diameter = section.points[-1, COLS.R] * 2 @@ -670,14 +704,16 @@ def shuffle_direction(direction_): L.debug('section appended: %s', child.id) def _grow(self, section, order_offset, origin): - '''grow main method + '''Grow main method. Will either: - continue growing the section - create a bifurcation - terminate the growth - Note: based on https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#n387 # noqa, pylint: disable=line-too-long + .. note:: + This is based on + https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.cpp#n387 ''' if (self.repair_type_map[section] == RepairType.tuft and _y_cylindrical_extent(section) > self.max_y_cylindrical_extent): @@ -730,9 +766,11 @@ def _fill_statistics_for_intact_subtrees(self): } def _fill_repair_type_map(self): - '''Assign a repair section type to each section + '''Assign a repair section type to each section. - Note: based on https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/repair.cpp#n242 # noqa, pylint: disable=line-too-long + .. note:: + This is based on + https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/repair.cpp#n242 ''' self.repair_type_map = repair_type_map(self.neuron, self.apical_section) @@ -746,8 +784,9 @@ def repair(inputfile: Path, # pylint: disable=too-many-arguments plot_file: Optional[Path] = None, repair_flags: Optional[Dict[RepairType, bool]] = None, apical_point: List = None, - params: Dict = None): - '''The repair function + params: Dict = None, + validate_params=False): + '''The repair function. Args: inputfile: the input morph @@ -756,10 +795,11 @@ def repair(inputfile: Path, # pylint: disable=too-many-arguments seed: the numpy seed cut_leaves_coordinates: List of 3D coordinates from which to start the repair plot_file: the filename of the plot - repair_flags: a dict of flags where key is a RepairType and value is whether - it should be repaired or not. If not provided, all types will be repaired. + repair_flags: a dict of flags where key is a :class:`neuror.utils.RepairType` and value is + whether it should be repaired or not. If not provided, all types will be repaired. apical_point: 3d vector for apical point, else, the automatic apical detection is used params: repair internal parameters, None will use defaults + validate_params: if set to ``True``, the given parameters are validated before processing ''' ignored_warnings = ( # We append the section at the wrong place and then we reposition them @@ -781,7 +821,7 @@ def repair(inputfile: Path, # pylint: disable=too-many-arguments obj = Repair(inputfile, axons=axons, seed=seed, cut_leaves_coordinates=cut_leaves_coordinates, legacy_detection=legacy_detection, repair_flags=repair_flags, - apical_point=apical_point, params=params) + apical_point=apical_point, params=params, validate_params=validate_params) obj.run(outputfile, plot_file=plot_file) for warning in ignored_warnings: diff --git a/neuror/sanitize.py b/neuror/sanitize.py index 4b80d77..7097aa5 100755 --- a/neuror/sanitize.py +++ b/neuror/sanitize.py @@ -3,7 +3,7 @@ from functools import partial from multiprocessing import Pool from pathlib import Path -from tqdm import tqdm +from typing import Dict, List, Tuple import numpy as np from morphio import MorphioError, SomaType, set_maximum_warnings @@ -12,6 +12,7 @@ from neurom.check import CheckResult from neurom.apps.annotate import annotate from neurom import load_morphology +from tqdm import tqdm from neuror.exceptions import CorruptedMorphology from neuror.exceptions import ZeroLengthRootSection @@ -67,10 +68,9 @@ def sanitize(input_neuron, output_path): def _sanitize_one(path, input_folder, output_folder): - '''Function to be called by sanitize_all to catch all exceptions - and return path if in error + '''Function to be called by sanitize_all to catch all exceptions and return path if in error. - Since Pool.imap_unordered only supports one argument, the argument + Since :meth:`multiprocessing.pool.Pool.imap_unordered` only supports one argument, the argument is a tuple: (path, input_folder, output_folder). ''' relative_path = path.relative_to(input_folder) @@ -95,7 +95,8 @@ def sanitize_all(input_folder, output_folder, nprocesses=1): input_folder (str|pathlib.Path): input neuron output_folder (str|pathlib.Path): output name - .. note:: the sub-directory structure is maintained. + .. note:: + The sub-directory structure is maintained. ''' set_maximum_warnings(0) @@ -124,7 +125,7 @@ def fix_non_zero_segments(neuron, zero_length=_ZERO_LENGTH): zero_length (float): smallest length of a segment Returns: - a fixed morphio.mut.Morphology + morphio.mut.Morphology: a fixed morphology ''' neuron = Morphology(neuron) to_be_deleted = [] @@ -152,12 +153,12 @@ def fix_non_zero_segments(neuron, zero_length=_ZERO_LENGTH): return neuron -def annotate_neurolucida(morph_path, checkers=None): +def annotate_neurolucida(morph_path: str, checkers: Dict = None): """Annotate errors on a morphology in neurolucida format. Args: - morph_path (str): absolute path to an ascii morphology - checkers (dict): dict of checker functons from neurom with function as keys + morph_path: absolute path to an ascii morphology + checkers: dict of checker functons from neurom with function as keys and marker data in a dict as values, if None, default checkers are used Default checkers include: @@ -168,9 +169,9 @@ def annotate_neurolucida(morph_path, checkers=None): - multifurcation Returns: - annotations to append to .asc file - dict of error summary - dict of error markers + * annotations to append to .asc file + * dict of error summary + * dict of error markers """ if checkers is None: checkers = { @@ -213,16 +214,19 @@ def _try(checker, neuron): return annotate(results, checkers.values()), summary, markers -def annotate_neurolucida_all(morph_paths, nprocesses=1): +def annotate_neurolucida_all( + morph_paths: List[str], nprocesses: int = 1 +) -> Tuple[Dict, Dict, Dict]: """Annotate errors on a list of morphologies in neurolicida format. Args: - morph_paths (list): list of str of paths to morphologies. + morph_paths: list of str of paths to morphologies. + nprocesses: number of processes to use for parallel computation Returns: - dict annotations to append to .asc file (morph_path as keys) - dict of dict of error summary (morph_path as keys) - dict of dict of markers (morph_path as keys) + * dict annotations to append to .asc file (morph_path as keys) + * dict of dict of error summary (morph_path as keys) + * dict of dict of markers (morph_path as keys) """ summaries, annotations, markers = {}, {}, {} with Pool(nprocesses) as pool: @@ -234,7 +238,7 @@ def annotate_neurolucida_all(morph_paths, nprocesses=1): return annotations, summaries, markers -def fix_points_in_soma(morph): +def fix_points_in_soma(morph: Morphology) -> bool: """Ensure section points are not inside the soma. Method: @@ -244,6 +248,12 @@ def fix_points_in_soma(morph): - if there is at least 1 point inside the soma, a new point is defined to replace them. If this new point is too close to the first point outside the soma, the point is not added. + + Args: + morph: the morphology + + Returns: + ``True`` if at least one point was changed, else ``False``. """ changed = False for root_sec in morph.root_sections: diff --git a/neuror/unravel.py b/neuror/unravel.py index c4c3971..3debd6b 100644 --- a/neuror/unravel.py +++ b/neuror/unravel.py @@ -25,8 +25,10 @@ def _get_principal_direction(points): - '''Return the principal direction of a point cloud - It is the eigen vector of the covariance matrix with the highest eigen value''' + '''Return the principal direction of a point cloud. + + It is the eigen vector of the covariance matrix with the highest eigen value. + ''' X = np.copy(np.asarray(points)) X -= np.mean(X, axis=0) @@ -36,7 +38,7 @@ def _get_principal_direction(points): def _unravel_section(section, window_half_length, soma, legacy_behavior, use_path_length): - '''Unravel a section using number of adjacent points as window_half_length''' + '''Unravel a section using number of adjacent points as window_half_length.''' # pylint: disable=too-many-locals points = section.points if legacy_behavior and section.is_root and len(soma.points) > 1: @@ -89,16 +91,17 @@ def _unravel_section(section, window_half_length, soma, legacy_behavior, use_pat def unravel(filename, window_half_length=None, legacy_behavior=False, use_path_length=True): - '''Return an unravelled neuron + '''Return an unravelled neuron. Segment are unravelled iteratively Each segment direction is replaced by the averaged direction in a sliding window around this segment, preserving the original segment length. The start position of the new segment is the end of the latest unravelled segment - Based initially on: - DOI: 10.7551/mitpress/9780262013277.001.0001 - Section: 9.2 Repair of Neuronal Dendrites + .. note:: + Based initially on: DOI: 10.7551/mitpress/9780262013277.001.0001 + + Section: 9.2 Repair of Neuronal Dendrites Args: filename (str): the neuron to unravel @@ -109,9 +112,9 @@ def unravel(filename, window_half_length=None, and correspond to number of points on each side of the window. Returns: - a tuple (morphio.mut.Morphology, dict) where first item is the unravelled - morphology and the second one is the mapping of each point coordinate - before and after unravelling + tuple[morphio.mut.Morphology, pandas.DataFrame]: a tuple where first item is the unravelled + morphology and the second one is the mapping of each point coordinate + before and after unravelling ''' morph = morphio.Morphology(filename, options=morphio.Option.nrn_order) new_morph = morphio.mut.Morphology(morph, options=morphio.Option.nrn_order) # noqa, pylint: disable=no-member @@ -145,8 +148,13 @@ def unravel(filename, window_half_length=None, def unravel_plane(plane, mapping): - '''Return a new CutPlane object where the cut-leaves - position has been updated after unravelling''' + '''Return a new :class:`~neuror.cut_plane.detection.CutPlane` object where the cut-leaves + position has been updated after unravelling. + + Args: + plane (CutPlane): the plane + mapping (pandas.DataFrame): the mapping + ''' leaves = plane.cut_leaves_coordinates if not np.any(leaves): @@ -166,8 +174,7 @@ def unravel_all(raw_dir, unravelled_dir, raw_planes_dir, unravelled_planes_dir, window_half_length=None): - '''Repair all morphologies in input folder - ''' + '''Repair all morphologies in input folder.''' if not os.path.exists(raw_planes_dir): raise NeuroRError(f'{raw_planes_dir} does not exist') diff --git a/neuror/utils.py b/neuror/utils.py index 61eadcd..be7fcb8 100644 --- a/neuror/utils.py +++ b/neuror/utils.py @@ -13,8 +13,9 @@ class RepairType(Enum): '''The types used for the repair. - based on - https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.h#L22 + .. note:: + This class is based on + https://bbpgitlab.epfl.ch/nse/morphologyrepair/BlueRepairSDK/-/blob/main/BlueRepairSDK/src/helper_dendrite.h#L22 ''' trunk = 0 tuft = 1 @@ -24,7 +25,7 @@ class RepairType(Enum): def repair_type_map(neuron, apical_section): - '''Return a dict of extended types''' + '''Return a dict of extended types.''' extended_types = {} for section in iter_sections(neuron): if section.type == SectionType.apical_dendrite: @@ -45,7 +46,7 @@ def repair_type_map(neuron, apical_section): def unit_vector(vector): - """ Returns the unit vector of the vector. """ + """Returns the unit vector of the vector.""" return vector / np.linalg.norm(vector) @@ -66,7 +67,7 @@ def rotation_matrix(axis, theta): # pylint: disable=too-many-locals def angle_between(v1, v2): - """Returns the angle in radians between vectors 'v1' and 'v2':: + """Returns the angle in radians between vectors 'v1' and 'v2'. ..code:: @@ -83,10 +84,10 @@ def angle_between(v1, v2): class RepairJSON(json.JSONEncoder): - '''JSON encoder that handles numpy types + '''JSON encoder that handles numpy types. - In python3, numpy.dtypes don't serialize to correctly, so a custom converter - is needed. + In python3, `numpy types `_ don't + serialize to correctly, so a custom converter is needed. ''' def default(self, o): # pylint: disable=method-hidden @@ -102,7 +103,7 @@ def default(self, o): # pylint: disable=method-hidden def direction(section): - '''Return the direction vector of a section + '''Return the direction vector of a section. Args: section (morphio.mut.Section): section @@ -111,7 +112,7 @@ def direction(section): def section_length(section): - '''Section length + '''Section length. Args: section (morphio.mut.Section): section diff --git a/neuror/view.py b/neuror/view.py index 58da6e6..59e4385 100644 --- a/neuror/view.py +++ b/neuror/view.py @@ -1,4 +1,4 @@ -'''Generate output plots''' +'''Generate output plots.''' import logging import os from datetime import datetime @@ -25,7 +25,7 @@ def get_common_bounding_box(neurons): - '''Returns the bounding box that wraps all neurons''' + '''Returns the bounding box that wraps all neurons.''' common_bbox = geom.bounding_box(neurons[0]) for neuron in neurons[1:]: bbox = geom.bounding_box(neuron) @@ -36,7 +36,7 @@ def get_common_bounding_box(neurons): def plot(neuron, bbox, subplot, title, **kwargs): - '''2D neuron plot''' + '''2D neuron plot.''' ax = plt.subplot(subplot, facecolor='w', aspect='equal') xlim = (bbox[0][0], bbox[1][0]) ylim = (bbox[0][2], bbox[1][2]) @@ -62,7 +62,7 @@ def _neuron_subplot(folders, f, pp, subplot, titles): def view_all(folders, titles, output_pdf=None): - '''Generate PDF report''' + '''Generate PDF report.''' if not output_pdf: path = './plots' output_pdf = os.path.join(path, datetime.now().strftime("%Y_%m_%d_%H_%M_%S") + '.pdf') @@ -87,9 +87,10 @@ def view_all(folders, titles, output_pdf=None): def plot_repaired_neuron(neuron, cut_points, plot_file=None): - ''' Draw a neuron using plotly + ''' Draw a neuron using plotly. - Repaired section are displayed with a different colors''' + Repaired section are displayed with a different colors. + ''' for mode in ['3d', 'xz']: builder = NeuronBuilder(neuron, mode, neuron.name, False) diff --git a/neuror/zero_diameter_fixer.py b/neuror/zero_diameter_fixer.py index e23bef1..ec81a5c 100644 --- a/neuror/zero_diameter_fixer.py +++ b/neuror/zero_diameter_fixer.py @@ -1,8 +1,9 @@ -'''Fix zero diameters +'''Fix zero diameters. -Re-implementation of: -https://bbpgitlab.epfl.ch/nse/morphologyrepair/MUK/-/blob/main/apps/Fix_Zero_Diameter.cpp -with sections recursion instead of point recursion +.. note:: + This module is a re-implementation of: + https://bbpgitlab.epfl.ch/nse/morphologyrepair/MUK/-/blob/main/apps/Fix_Zero_Diameter.cpp + with sections recursion instead of point recursion ''' from collections import namedtuple import numpy as np @@ -12,10 +13,11 @@ Point = namedtuple('Point', ['section', 'point_id']) # index of the point within the given section +"""The class used to store a point as a section and point ID in this section.""" def _next_point_upstream(point): - '''Yield upstream points until reaching the root''' + '''Yield upstream points until reaching the root.''' section, point_id = point while not (section.is_root and point_id == 0): if point_id > 0: @@ -31,10 +33,10 @@ def _get_point_diameter(point): def _set_point_diameter(point, new_diameter): - '''Set a given diameter + '''Set a given diameter. Unfortunately morphio does not support single value assignment - so one has to update the diameters for the whole sections + so one has to update the diameters for the whole sections. ''' diameters = point.section.diameters diameters[point.point_id] = new_diameter @@ -42,10 +44,11 @@ def _set_point_diameter(point, new_diameter): def _connect_average_legacy(from_point): - '''Apply a ramp diameter between the two points + '''Apply a ramp diameter between the two points. - Re-implementation of - https://bbpgitlab.epfl.ch/nse/morphologyrepair/MUK/-/blob/main/muk/Zero_Diameter_Fixer.cpp#L232 + .. note:: + This is a re-implementation of + https://bbpgitlab.epfl.ch/nse/morphologyrepair/MUK/-/blob/main/muk/Zero_Diameter_Fixer.cpp#L232 ''' count = 0 next_point = None @@ -65,12 +68,13 @@ def _connect_average_legacy(from_point): def _connect_average(from_point): - '''Apply a ramp diameter between the two points + '''Apply a ramp diameter between the two points. - Re-implementation of - https://bbpgitlab.epfl.ch/nse/morphologyrepair/MUK/-/blob/main/muk/Zero_Diameter_Fixer.cpp#L232 - Contrary to the previous implementation the diameter the ramp is computed in term of - pathlength and no longer in term of point number + .. note:: + This is a re-implementation of + https://bbpgitlab.epfl.ch/nse/morphologyrepair/MUK/-/blob/main/muk/Zero_Diameter_Fixer.cpp#L232 + Contrary to the previous implementation the diameter the ramp is computed in term of + pathlength and no longer in term of point number. ''' prev_point = from_point pathlengths = [] @@ -94,19 +98,21 @@ def _connect_average(from_point): def _fix_downstream(section): - '''Re-implementation of recursePullFix() available at - https://bbpgitlab.epfl.ch/nse/morphologyrepair/MUK/-/blob/main/muk/Zero_Diameter_Fixer.cpp#L66 + '''Ensure that diameters are decreasing in downstream direction. If the current diameter is below the threshold, change its value to the biggest value - among the 1-degree children downstream diameters + among the 1-degree children downstream diameters. This is recursive only until a diameter above threshold is found so this - fixes zero diameters that are located at the beginning of the neurite + fixes zero diameters that are located at the beginning of the neurite. - Fixes this: - Soma--0--0--0--1--1--1 - But not this: - Soma--1--1--1--0--0--0--1--1 + Fixes this: ``Soma--0--0--0--1--1--1`` + + But not this: ``Soma--1--1--1--0--0--0--1--1`` + + .. note:: + This is a re-implementation of recursePullFix() available at + https://bbpgitlab.epfl.ch/nse/morphologyrepair/MUK/-/blob/main/muk/Zero_Diameter_Fixer.cpp#L66 Args: section(morphio.Section): @@ -126,10 +132,11 @@ def _fix_downstream(section): def _fix_in_between(section, stack, legacy): - '''Re-implementation of - https://bbpgitlab.epfl.ch/nse/morphologyrepair/MUK/-/blob/main/muk/Zero_Diameter_Fixer.cpp#L162 + '''Fix diameters between two points with valid diameters by applying a ramp. - Fix diameters between two points with valid diameters by applying a ramp + .. note:: + This is a re-implementation of + https://bbpgitlab.epfl.ch/nse/morphologyrepair/MUK/-/blob/main/muk/Zero_Diameter_Fixer.cpp#L162 Args: point: the current points @@ -156,11 +163,12 @@ def _fix_in_between(section, stack, legacy): def _fix_upstream(section, upstream_good_diameter): - '''Re-implementation of recursePushFix() available at - https://bbpgitlab.epfl.ch/nse/morphologyrepair/MUK/-/blob/main/muk/Zero_Diameter_Fixer.cpp#L94 + '''Reset the diameter to `upstream_good_diameter` if the current value and all child values + are below threshold. - Reset the diameter to upstream_good_diameter if the current value and all child values - are below threshold + .. note:: + This is a re-implementation of recursePushFix() available at + https://bbpgitlab.epfl.ch/nse/morphologyrepair/MUK/-/blob/main/muk/Zero_Diameter_Fixer.cpp#L94 Args: section: the current section @@ -168,8 +176,8 @@ def _fix_upstream(section, upstream_good_diameter): the current diameter is not suitable Returns: - The current diameter if above threshold, else the smallest child value above threshold - else (if no child value is above threshold) returns 0 + (float): The current diameter if above threshold, else the smallest child value above + threshold else (if no child value is above threshold) returns 0. ''' diameters = section.diameters nonzero_indices = np.asarray(diameters > SMALL).nonzero()[0] @@ -187,13 +195,13 @@ def _fix_upstream(section, upstream_good_diameter): def fix_neurite(root_section, legacy=False): - '''Apply all fixes to a neurite''' + '''Apply all fixes to a neurite.''' _fix_downstream(root_section) _fix_in_between(root_section, [], legacy) _fix_upstream(root_section, 0) def fix_zero_diameters(neuron, legacy=False): - '''Fix zero diameters''' + '''Fix zero diameters.''' for root in neuron.root_sections: fix_neurite(root, legacy)