From 4dd215eae07bb31336caab7d6e867d2c07fdc363 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Thu, 21 May 2020 04:21:42 +0300 Subject: [PATCH 01/33] update docs and slight refactoring --- docs/api.rst | 94 +++++++++++++++++++++++++++++++ docs/index.rst | 1 + poetry.lock | 8 +-- skmpe/__init__.py | 1 + skmpe/_base.py | 60 +++++++++++++++++++- skmpe/_exceptions.py | 50 ++++++++++++++--- skmpe/_mpe.py | 87 +++++++++++++++++++++++++---- skmpe/_parameters.py | 128 ++++++++++++++++++++++++++++++++++++++++--- 8 files changed, 394 insertions(+), 35 deletions(-) create mode 100644 docs/api.rst diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..2d47cbc --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,94 @@ +.. _api: + +************* +API Reference +************* + +.. currentmodule:: skmpe + +API Summary +=========== + +.. autosummary:: + :nosignatures: + + InitialInfo + PathInfo + ResultPathInfo + + TravelTimeOrder + OdeSolverMethod + Parameters + parameters + default_parameters + + MPEError + ComputeTravelTimeError + PathExtractionError + EndPointNotReachedError + + MinimalPathExtractor + mpe + +| + +Data and Models +=============== + +.. autoclass:: InitialInfo + :members: + +.. autoclass:: PathInfo + :show-inheritance: + +.. autoclass:: ResultPathInfo + :members: + +.. autoclass:: ResultPathInfo + :members: + +Parameters +========== + +.. autoclass:: TravelTimeOrder + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: OdeSolverMethod + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: Parameters + :members: + :undoc-members: + +.. autofunction:: parameters +.. autofunction:: default_parameters + +Exceptions +========== + +.. autoclass:: MPEError + :show-inheritance: + +.. autoclass:: ComputeTravelTimeError + :show-inheritance: + +.. autoclass:: PathExtractionError + :members: + :show-inheritance: + +.. autoclass:: EndPointNotReachedError + :members: + :inherited-members: PathExtractionError + :exclude-members: with_traceback + :show-inheritance: + +Path Extraction +=============== + +.. autoclass:: MinimalPathExtractor + :members: + :special-members: __call__ diff --git a/docs/index.rst b/docs/index.rst index d700685..4568d52 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -11,6 +11,7 @@ using `the fast marching method =3.2)", "rst.linker (>=1.9)"] testing = ["jaraco.itertools"] [extras] -docs = ["sphinx", "numpydoc", "matplotlib", "toml"] +docs = ["sphinx", "numpydoc", "m2r", "matplotlib", "toml"] tests = ["pytest", "pytest-cov", "coveralls", "scikit-image"] [metadata] -content-hash = "2bc05496f1f12aa257cc5a2abd8b99d745068a34a4a122a2e6342fedb45a6737" +content-hash = "727b425dfdaf26d0c296ec88197302c6ac6d8c6ceae7ff3e10bcbe8057057940" python-versions = "^3.6" [metadata.files] diff --git a/skmpe/__init__.py b/skmpe/__init__.py index b090021..c160a9e 100644 --- a/skmpe/__init__.py +++ b/skmpe/__init__.py @@ -9,6 +9,7 @@ from ._parameters import ( TravelTimeOrder, + OdeSolverMethod, Parameters, parameters, default_parameters, diff --git a/skmpe/_base.py b/skmpe/_base.py index e755a96..57b0a2b 100644 --- a/skmpe/_base.py +++ b/skmpe/_base.py @@ -29,6 +29,7 @@ logger.addHandler(logging.NullHandler()) +@set_module(MPE_MODULE) class ImmutableDataObject(BaseModel): """Base immutable data object with validating fields """ @@ -41,7 +42,24 @@ class Config: @set_module(MPE_MODULE) class InitialInfo(ImmutableDataObject): - """Initial info for extracting path + """Initial info data model + + .. py:attribute:: speed_data + + Speed data in numpy ndarray + + .. py:attribute:: start_point + + The starting point + + .. py:attribute:: end_point + + The ending point + + .. py:attribute:: way_points + + The tuple of way points + """ speed_data: np.ndarray @@ -134,16 +152,43 @@ def validate(point, index=None): return v def all_points(self) -> List[PointType]: + """Returns all initial points""" return [self.start_point, *self.way_points, self.end_point] def point_intervals(self) -> List[Tuple[PointType, PointType]]: + """Returns the list of the tuples of initial point intervals""" all_points = self.all_points() return list(zip(all_points[:-1], all_points[1:])) @set_module(MPE_MODULE) class PathInfo(NamedTuple): - """Extracted path info + """The named tuple with info about extracted path or piece of path + + .. py:attribute:: path + + The path in numpy ndarray + + .. py:attribute:: start_point + + The starting point + + .. py:attribute:: end_point + + The ending point + + .. py:attribute:: travel_time + + The travel time numpy ndarray + + .. py:attribute:: path_travel_times + + The travel time values for every path point + + .. py:attribute:: reversed + + The flag is true if the extracted path is reversed + """ path: np.ndarray @@ -156,7 +201,15 @@ class PathInfo(NamedTuple): @set_module(MPE_MODULE) class ResultPathInfo(ImmutableDataObject): - """Result path info + """Result path info model + + .. py:attribute:: path + + Path data in numpy array + + .. py:attribute:: pieces + + The tuple of :class:`PathInfo` for every path piece """ path: np.ndarray @@ -164,6 +217,7 @@ class ResultPathInfo(ImmutableDataObject): @property def point_count(self) -> int: + """Returns the number of path points""" return self.path.shape[0] diff --git a/skmpe/_exceptions.py b/skmpe/_exceptions.py index c9569a6..6c7922e 100644 --- a/skmpe/_exceptions.py +++ b/skmpe/_exceptions.py @@ -10,29 +10,48 @@ @set_module(MPE_MODULE) class MPEError(Exception): - pass + """Base exception class for all MPE errors""" @set_module(MPE_MODULE) class ComputeTravelTimeError(MPEError): - pass + """The exception occurs when computing travel time has failed""" @set_module(MPE_MODULE) class PathExtractionError(MPEError): + """Base exception class for all extracting path errors""" + def __init__(self, *args, travel_time: np.ndarray, start_point: PointType, end_point: PointType) -> None: super().__init__(*args) - self.travel_time = travel_time - self.start_point = start_point - self.end_point = end_point + self._travel_time = travel_time + self._start_point = start_point + self._end_point = end_point + + @property + def travel_time(self) -> np.ndarray: + """Computed travel time data""" + return self._travel_time + + @property + def start_point(self) -> PointType: + """Starting point""" + return self._start_point + + @property + def end_point(self) -> PointType: + """Ending point""" + return self._end_point @set_module(MPE_MODULE) class EndPointNotReachedError(PathExtractionError): + """The exception occurs when the ending point is not reached""" + def __init__(self, *args, travel_time: np.ndarray, start_point: PointType, @@ -42,6 +61,21 @@ def __init__(self, *args, reason: str) -> None: super().__init__(*args, travel_time=travel_time, start_point=start_point, end_point=end_point) - self.extracted_points = extracted_points - self.last_distance = last_distance - self.reason = reason + self._extracted_points = extracted_points + self._last_distance = last_distance + self._reason = reason + + @property + def extracted_points(self) -> List[FloatPointType]: + """The list of extracted path points""" + return self._extracted_points + + @property + def last_distance(self) -> float: + """The last distance to the ending point from the last path point""" + return self._last_distance + + @property + def reason(self) -> str: + """The reason of extracting path termination""" + return self._reason diff --git a/skmpe/_mpe.py b/skmpe/_mpe.py index 74649e8..1dd4a15 100644 --- a/skmpe/_mpe.py +++ b/skmpe/_mpe.py @@ -38,6 +38,35 @@ class MinimalPathExtractor: Minimal path extractor based on the fast marching method and ODE solver. + Parameters + ---------- + + speed_data : np.ndarray + The speed data (n-d numpy array) + + source_point : Sequence[int] + The source point (aka "ending point") + + parameters : class:`Parameters` + The parameters + + Examples + -------- + + .. code-block:: python + + from skmpe import MinimalPathExtractor + + # some function for computing speed data + speed_data_2d = compute_speed_data_2d() + + mpe = MinimalPathExtractor(speed_data_2d, (10, 25)) + path = mpe((123, 34)) + + Raises + ------ + ComputeTravelTimeError : Computing travel time has failed + """ def __init__(self, speed_data: np.ndarray, source_point: PointType, @@ -51,17 +80,16 @@ def __init__(self, speed_data: np.ndarray, source_point: PointType, grad_interpolants, tt_interpolant, phi_interpolant = self._compute_interpolants(gradients, travel_time, phi) - self.speed_data = speed_data - self.travel_time = travel_time - self.phi = phi + self._travel_time = travel_time + self._phi = phi - self.source_point = source_point + self._source_point = source_point - self.travel_time_interpolant = tt_interpolant - self.phi_interpolant = phi_interpolant - self.gradient_interpolants = grad_interpolants + self._travel_time_interpolant = tt_interpolant + self._phi_interpolant = phi_interpolant + self._gradient_interpolants = grad_interpolants - self.parameters = parameters + self._parameters = parameters # output after compute ODE solution self.integrate_times = [] @@ -70,6 +98,24 @@ def __init__(self, speed_data: np.ndarray, source_point: PointType, self.steps = 0 self.func_eval_count = 0 + @property + def travel_time(self) -> np.ndarray: + """Returns the computed travel time for given speed data + """ + return self._travel_time + + @property + def phi(self) -> np.ndarray: + """Returns the computed phi (zero contour) for given source point + """ + return self._phi + + @property + def parameters(self) -> Parameters: + """Returns the parameters + """ + return self._parameters + @staticmethod def _compute_travel_time(speed_data: np.ndarray, source_point: PointType, @@ -102,8 +148,27 @@ def _compute_interpolants(gradients, travel_time, phi): return gradient_interpolants, tt_interpolant, phi_interpolant def __call__(self, start_point: PointType) -> np.ndarray: - gradient_interpolants = self.gradient_interpolants - travel_time_interpolant = self.travel_time_interpolant + """Extract path from start point to source point (ending point) + + Parameters + ---------- + start_point : Sequence[int] + The starting point + + Returns + ------- + path : np.ndarray + The extracted path + + Raises + ------ + PathExtractionError : Extracting path has failed + EndPointNotReachedError : The extracted path is not reached the ending point + + """ + + gradient_interpolants = self._gradient_interpolants + travel_time_interpolant = self._travel_time_interpolant def right_hand_func(time: float, point: np.ndarray) -> np.ndarray: # noqa velocity = np.array([gi(point).item() for gi in gradient_interpolants]) @@ -136,7 +201,7 @@ def right_hand_func(time: float, point: np.ndarray) -> np.ndarray: # noqa self.path_points = [] self.steps = 0 - end_point = self.source_point + end_point = self._source_point dist_tol = self.parameters.dist_tol y_old = start_point diff --git a/skmpe/_parameters.py b/skmpe/_parameters.py index f8ba9e7..93ff9d1 100644 --- a/skmpe/_parameters.py +++ b/skmpe/_parameters.py @@ -11,12 +11,24 @@ @set_module(MPE_MODULE) class TravelTimeOrder(enum.IntEnum): + """The enumeration of travel time computation orders + + Orders: + + - **first** -- the first ordered travel time computation + - **second** -- the second ordered travel time computation + + """ + first = 1 second = 2 @set_module(MPE_MODULE) class OdeSolverMethod(str, enum.Enum): + """The enumeration of ODE solver methods + """ + RK23 = 'RK23' RK45 = 'RK45' DOP853 = 'DOP853' @@ -27,7 +39,62 @@ class OdeSolverMethod(str, enum.Enum): @set_module(MPE_MODULE) class Parameters(ImmutableDataObject): - """MPE algorithm parameters + """MPE algorithm parameters model + + .. py:attribute:: travel_time_spacing + + The travel time computation spacing + + | default: 1.0 + + .. py:attribute:: travel_time_order + + The travel time computation order + + | default: ``TravelTimeOrder.first`` + + .. py:attribute:: travel_time_cache + + Use or not travel time computation cache for extracting paths with way points + + | default: True + + .. py:attribute:: ode_solver_method + + ODE solver method + + | default: 'RK45' + + .. py:attribute:: integrate_time_bound + + Integration time bound + + | default: 10000 + + .. py:attribute:: integrate_min_step + + Integration minimum step + + | default: 0.0 + + .. py:attribute:: integrate_max_step + + Integration maximum step + + | default: 4.0 + + .. py:attribute:: dist_tol + + Distance tolerance for control path evolution + + | default: 1e-03 + + .. py:attribute:: max_small_dist_steps + + The max number of small distance steps while path evolution + + | default: 100 + """ travel_time_spacing: confloat(gt=0.0) = 1.0 @@ -66,14 +133,35 @@ def parameters(**kwargs): kwargs : mapping The parameters - - **travel_time_spacing** -- - - **travel_time_order** -- - - **travel_time_cache** -- - - **ode_solver_method** -- - - **integrate_time_bound** -- - - **integrate_min_step** -- - - **dist_tol** -- - - **max_small_dist_steps** -- + Examples + -------- + + .. code-block:: python + + >>> from skmpe import parameters + >>> with parameters(integrate_time_bound=200000) as params: + >>> print(params.__repr__()) + + Parameters( + travel_time_spacing=1.0, + travel_time_order=, + travel_time_cache=False, + ode_solver_method=, + integrate_time_bound=200000.0, + integrate_min_step=0.0, + integrate_max_step=4.0, + dist_tol=0.001, + max_small_dist_steps=100 + ) + + .. code-block:: python + + from skmpe import parameters, mpe + + ... + + with parameters(integrate_time_bound=200000): + path_result = mpe(start_point, end_point) """ @@ -96,6 +184,28 @@ def default_parameters() -> Parameters: parameters : Parameters Default parameters + Examples + -------- + + .. code-block:: python + + >>> from skmpe import default_parameters + >>> print(default_parameters().__repr__()) + + Parameters( + travel_time_spacing=1.0, + travel_time_order=, + travel_time_cache=False, + ode_solver_method=, + integrate_time_bound=10000.0, + integrate_min_step=0.0, + integrate_max_step=4.0, + dist_tol=0.001, + max_small_dist_steps=100 + ) + + + """ return _default_parameters From bcb58f9e3601fce1b20a8faa33e2ddd2c11ebc69 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Thu, 21 May 2020 13:43:33 +0300 Subject: [PATCH 02/33] wip: refactoring --- skmpe/__init__.py | 5 +- skmpe/_mpe.py | 114 +++++++++++++++++++++++++++++++--------------- 2 files changed, 81 insertions(+), 38 deletions(-) diff --git a/skmpe/__init__.py b/skmpe/__init__.py index c160a9e..e47cfd0 100644 --- a/skmpe/__init__.py +++ b/skmpe/__init__.py @@ -22,7 +22,10 @@ EndPointNotReachedError, ) -from ._mpe import MinimalPathExtractor +from ._mpe import ( + ExtractedPathResult, + MinimalPathExtractor, +) # register dispatchered API import skmpe._api as _api # noqa diff --git a/skmpe/_mpe.py b/skmpe/_mpe.py index 1dd4a15..f080543 100644 --- a/skmpe/_mpe.py +++ b/skmpe/_mpe.py @@ -2,7 +2,7 @@ import itertools import warnings -from typing import List, Optional +from typing import List, Optional, NamedTuple import numpy as np import skfmm as fmm @@ -32,6 +32,40 @@ def make_interpolator(coords, values, fill_value: float = 0.0): } +class ExtractedPathResult(NamedTuple): + """The named tuple with info about extracted path + + The instance of the class is returned from :class:`MinimalPathExtractor`. + + .. py:attribute:: path_points + + The extracted path points in the list + + .. py:attribute:: path_integrate_times + + The list of integrate times for every path point + + .. py:attribute:: path_travel_times + + The list of travel time values for every path point + + .. py:attribute:: step_count + + The number of integration steps + + .. py:attribute:: func_eval_count + + The number of evaluations of the right hand function + + """ + + path_points: List[PointType] + path_integrate_times: List[float] + path_travel_times: List[float] + step_count: int + func_eval_count: int + + @set_module(MPE_MODULE) class MinimalPathExtractor: """Minimal path extractor @@ -91,12 +125,12 @@ def __init__(self, speed_data: np.ndarray, source_point: PointType, self._parameters = parameters - # output after compute ODE solution - self.integrate_times = [] - self.path_points = [] - self.path_travel_times = [] - self.steps = 0 - self.func_eval_count = 0 + # the output when computing ODE solution is finished + self._path_points = [] + self._path_integrate_times = [] + self._path_travel_times = [] + self._step_count = 0 + self._func_eval_count = 0 @property def travel_time(self) -> np.ndarray: @@ -147,7 +181,7 @@ def _compute_interpolants(gradients, travel_time, phi): return gradient_interpolants, tt_interpolant, phi_interpolant - def __call__(self, start_point: PointType) -> np.ndarray: + def __call__(self, start_point: PointType) -> ExtractedPathResult: """Extract path from start point to source point (ending point) Parameters @@ -157,8 +191,8 @@ def __call__(self, start_point: PointType) -> np.ndarray: Returns ------- - path : np.ndarray - The extracted path + extracted_path_result : ExtractedPathResult + The extracted path result in :class:`ExtractedPathResult` Raises ------ @@ -196,10 +230,10 @@ def right_hand_func(time: float, point: np.ndarray) -> np.ndarray: # noqa first_step=None, ) - self.integrate_times = [] - self.path_travel_times = [] - self.path_points = [] - self.steps = 0 + self._path_points = [] + self._path_integrate_times = [] + self._path_travel_times = [] + self._step_count = 0 end_point = self._source_point dist_tol = self.parameters.dist_tol @@ -208,7 +242,7 @@ def right_hand_func(time: float, point: np.ndarray) -> np.ndarray: # noqa small_dist_steps_left = self.parameters.max_small_dist_steps while True: - self.steps += 1 + self._step_count += 1 message = solver.step() if solver.status == 'failed': # pragma: no cover @@ -228,41 +262,41 @@ def right_hand_func(time: float, point: np.ndarray) -> np.ndarray: # noqa if y_dist < dist_tol: logger.warning('step: %d, the distance between old and current extracted point (%f) is ' - 'too small (less than dist_tol=%f)', self.steps, y_dist, dist_tol) + 'too small (less than dist_tol=%f)', self._step_count, y_dist, dist_tol) add_point = False small_dist_steps_left -= 1 if add_point: small_dist_steps_left = self.parameters.max_small_dist_steps - self.integrate_times.append(t) - self.path_points.append(y) - self.path_travel_times.append(tt) + self._path_points.append(y) + self._path_integrate_times.append(t) + self._path_travel_times.append(tt) - self.func_eval_count = solver.nfev + self._func_eval_count = solver.nfev step_size = solver.step_size dist_to_end = euclidean(y, end_point) logger.debug('step: %d, time: %.2f, point: %s, step_size: %.2f, nfev: %d, dist: %.2f, message: "%s"', - self.steps, t, y, step_size, solver.nfev, dist_to_end, message) + self._step_count, t, y, step_size, solver.nfev, dist_to_end, message) if dist_to_end < step_size: logger.debug( - 'The minimal path has been extracted (time: %.2f, steps: %d, nfev: %d, dist_to_end: %.2f)', - t, self.steps, solver.nfev, dist_to_end) + 'The minimal path has been extracted (time: %.2f, _step_count: %d, nfev: %d, dist_to_end: %.2f)', + t, self._step_count, solver.nfev, dist_to_end) break if solver.status == 'finished' or small_dist_steps_left == 0: if small_dist_steps_left == 0: reason = f'the distance between old and current point stay too small ' \ - f'for {self.parameters.max_small_dist_steps} steps' + f'for {self.parameters.max_small_dist_steps} _step_count' else: reason = f'time bound {self.parameters.integrate_time_bound} is reached, solver was finished.' err_msg = ( f'The extracted path from the start point {start_point} ' - f'did not reach the end point {end_point} in {t} time and {self.steps} steps ' + f'did not reach the end point {end_point} in {t} time and {self._step_count} _step_count ' f'with distance {dist_to_end:.2f} to the end point. Reason: {reason}' ) @@ -271,29 +305,35 @@ def right_hand_func(time: float, point: np.ndarray) -> np.ndarray: # noqa travel_time=self.travel_time, start_point=start_point, end_point=end_point, - extracted_points=self.path_points, + extracted_points=self._path_points, last_distance=dist_to_end, reason=reason, ) - return np.array(self.path_points) + return ExtractedPathResult( + path_points=self._path_points, + path_integrate_times=self._path_integrate_times, + path_travel_times=self._path_travel_times, + step_count=self._step_count, + func_eval_count=self._func_eval_count, + ) def extract_path_without_way_points(init_info: InitialInfo, parameters: Parameters) -> ResultPathInfo: extractor = MinimalPathExtractor(init_info.speed_data, init_info.end_point, parameters) - path = extractor(init_info.start_point) + result = extractor(init_info.start_point) path_info = PathInfo( - path=path, + path=np.asarray(result.path_points), start_point=init_info.start_point, end_point=init_info.end_point, travel_time=extractor.travel_time, - path_travel_times=np.asarray(extractor.path_travel_times), + path_travel_times=np.asarray(result.path_travel_times), reversed=False, ) - return ResultPathInfo(path=path, pieces=[path_info]) + return ResultPathInfo(path=path_info.path, pieces=[path_info]) def make_whole_path_from_pieces(path_pieces_info: List[PathInfo]) -> ResultPathInfo: @@ -331,27 +371,27 @@ def extract_path_with_way_points(init_info: InitialInfo, start_point, end_point = end_point, start_point is_reversed = True - path = extractor(start_point) + result = extractor(start_point) path_pieces_info.append(PathInfo( - path=path, + path=np.asarray(result.path_points), start_point=start_point, end_point=end_point, travel_time=extractor.travel_time, - path_travel_times=np.asarray(extractor.path_travel_times), + path_travel_times=np.asarray(result.path_travel_times), reversed=is_reversed )) else: for start_point, end_point in init_info.point_intervals(): extractor = MinimalPathExtractor(speed_data, end_point, parameters) - path = extractor(start_point) + result = extractor(start_point) path_pieces_info.append(PathInfo( - path=path, + path=np.asarray(result.path_points), start_point=start_point, end_point=end_point, travel_time=extractor.travel_time, - path_travel_times=np.asarray(extractor.path_travel_times), + path_travel_times=np.asarray(result.path_travel_times), reversed=False )) From bbb33ca4d6a9c7b058fcbc81767868a464f584e1 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Thu, 21 May 2020 14:57:18 +0300 Subject: [PATCH 03/33] wip: refactoring --- skmpe/__init__.py | 2 +- skmpe/_mpe.py | 22 ++++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/skmpe/__init__.py b/skmpe/__init__.py index e47cfd0..18b1d9c 100644 --- a/skmpe/__init__.py +++ b/skmpe/__init__.py @@ -23,7 +23,7 @@ ) from ._mpe import ( - ExtractedPathResult, + PathExtractionResult, MinimalPathExtractor, ) diff --git a/skmpe/_mpe.py b/skmpe/_mpe.py index f080543..a129424 100644 --- a/skmpe/_mpe.py +++ b/skmpe/_mpe.py @@ -32,10 +32,14 @@ def make_interpolator(coords, values, fill_value: float = 0.0): } -class ExtractedPathResult(NamedTuple): +@set_module(MPE_MODULE) +class PathExtractionResult(NamedTuple): """The named tuple with info about extracted path - The instance of the class is returned from :class:`MinimalPathExtractor`. + Notes + ----- + + The instance of the class is returned from :func:`MinimalPathExtractor.__call__`. .. py:attribute:: path_points @@ -181,7 +185,7 @@ def _compute_interpolants(gradients, travel_time, phi): return gradient_interpolants, tt_interpolant, phi_interpolant - def __call__(self, start_point: PointType) -> ExtractedPathResult: + def __call__(self, start_point: PointType) -> PathExtractionResult: """Extract path from start point to source point (ending point) Parameters @@ -191,8 +195,8 @@ def __call__(self, start_point: PointType) -> ExtractedPathResult: Returns ------- - extracted_path_result : ExtractedPathResult - The extracted path result in :class:`ExtractedPathResult` + path_extraction_result : PathExtractionResult + The path extraction result in :class:`ExtractedPathResult` instance Raises ------ @@ -310,7 +314,7 @@ def right_hand_func(time: float, point: np.ndarray) -> np.ndarray: # noqa reason=reason, ) - return ExtractedPathResult( + return PathExtractionResult( path_points=self._path_points, path_integrate_times=self._path_integrate_times, path_travel_times=self._path_travel_times, @@ -386,14 +390,16 @@ def extract_path_with_way_points(init_info: InitialInfo, extractor = MinimalPathExtractor(speed_data, end_point, parameters) result = extractor(start_point) - path_pieces_info.append(PathInfo( + path_piece_info = PathInfo( path=np.asarray(result.path_points), start_point=start_point, end_point=end_point, travel_time=extractor.travel_time, path_travel_times=np.asarray(result.path_travel_times), reversed=False - )) + ) + + path_pieces_info.append(path_piece_info) return make_whole_path_from_pieces(path_pieces_info) From b9bd0f7cf7eead8bbe4448a7c048769dbaebfa3a Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Thu, 21 May 2020 15:23:27 +0300 Subject: [PATCH 04/33] wip: refactoring --- docs/api.rst | 13 +++++++------ skmpe/__init__.py | 4 ++-- skmpe/_api.py | 6 +++--- skmpe/_base.py | 19 ++++++++++--------- skmpe/_mpe.py | 20 ++++++++++---------- 5 files changed, 32 insertions(+), 30 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 2d47cbc..83f13ac 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -14,7 +14,7 @@ API Summary InitialInfo PathInfo - ResultPathInfo + PathInfoResult TravelTimeOrder OdeSolverMethod @@ -27,6 +27,7 @@ API Summary PathExtractionError EndPointNotReachedError + PathExtractionResult MinimalPathExtractor mpe @@ -41,11 +42,8 @@ Data and Models .. autoclass:: PathInfo :show-inheritance: -.. autoclass:: ResultPathInfo - :members: - -.. autoclass:: ResultPathInfo - :members: +.. autoclass:: PathInfoResult + :show-inheritance: Parameters ========== @@ -89,6 +87,9 @@ Exceptions Path Extraction =============== +.. autoclass:: PathExtractionResult + :show-inheritance: + .. autoclass:: MinimalPathExtractor :members: :special-members: __call__ diff --git a/skmpe/__init__.py b/skmpe/__init__.py index 18b1d9c..f6c913d 100644 --- a/skmpe/__init__.py +++ b/skmpe/__init__.py @@ -3,7 +3,7 @@ from ._base import ( InitialInfo, PathInfo, - ResultPathInfo, + PathInfoResult, mpe ) @@ -36,7 +36,7 @@ __all__ = [ 'InitialInfo', 'PathInfo', - 'ResultPathInfo', + 'PathInfoResult', 'TravelTimeOrder', 'Parameters', diff --git a/skmpe/_api.py b/skmpe/_api.py index 7b52547..f159a02 100644 --- a/skmpe/_api.py +++ b/skmpe/_api.py @@ -5,7 +5,7 @@ import numpy as np from ._base import mpe as api_dispatch -from ._base import PointType, PointSequenceType, InitialInfo, ResultPathInfo +from ._base import PointType, PointSequenceType, InitialInfo, PathInfoResult from ._parameters import Parameters from ._mpe import extract_path @@ -16,7 +16,7 @@ def mpe(speed_data: np.ndarray, end_point: Union[PointType, np.ndarray], way_points: Union[PointSequenceType, np.ndarray] = (), *, - parameters: Optional[Parameters] = None) -> ResultPathInfo: + parameters: Optional[Parameters] = None) -> PathInfoResult: init_info = InitialInfo( speed_data=speed_data, @@ -31,5 +31,5 @@ def mpe(speed_data: np.ndarray, @api_dispatch.register(InitialInfo) # noqa def mpe(init_info: InitialInfo, *, - parameters: Optional[Parameters] = None) -> ResultPathInfo: + parameters: Optional[Parameters] = None) -> PathInfoResult: return extract_path(init_info, parameters) diff --git a/skmpe/_base.py b/skmpe/_base.py index 57b0a2b..cc9ebe4 100644 --- a/skmpe/_base.py +++ b/skmpe/_base.py @@ -11,6 +11,7 @@ if TYPE_CHECKING: from ._parameters import Parameters # noqa + from ._mpe import PathExtractionResult # noqa PointType = Sequence[int] @@ -181,9 +182,9 @@ class PathInfo(NamedTuple): The travel time numpy ndarray - .. py:attribute:: path_travel_times + .. py:attribute:: extraction_result - The travel time values for every path point + The path extraction result in :class:`PathExtractionResult` that is returned by :class:`MinimalPathExtractor` .. py:attribute:: reversed @@ -195,13 +196,13 @@ class PathInfo(NamedTuple): start_point: PointType end_point: PointType travel_time: np.ndarray - path_travel_times: np.ndarray + extraction_result: 'PathExtractionResult' reversed: bool @set_module(MPE_MODULE) -class ResultPathInfo(ImmutableDataObject): - """Result path info model +class PathInfoResult(NamedTuple): + """The named tuple with path info result .. py:attribute:: path @@ -227,20 +228,20 @@ def mpe(speed_data: np.ndarray, end_point: Union[PointType, np.ndarray], way_points: Union[PointSequenceType, np.ndarray] = (), *, - parameters: Optional['Parameters'] = None) -> ResultPathInfo: + parameters: Optional['Parameters'] = None) -> PathInfoResult: pass # pragma: no cover @overload def mpe(init_info: InitialInfo, *, - parameters: Optional['Parameters'] = None) -> ResultPathInfo: + parameters: Optional['Parameters'] = None) -> PathInfoResult: pass # pragma: no cover @set_module(MPE_MODULE) @singledispatch -def mpe(*args, **kwargs) -> ResultPathInfo: # noqa +def mpe(*args, **kwargs) -> PathInfoResult: # noqa """Extracts a minimal path Usage @@ -259,7 +260,7 @@ def mpe(*args, **kwargs) -> ResultPathInfo: # noqa Returns ------- - path_info : ResultPathInfo + path_info : PathInfoResult Extracted path info """ diff --git a/skmpe/_mpe.py b/skmpe/_mpe.py index a129424..598bafb 100644 --- a/skmpe/_mpe.py +++ b/skmpe/_mpe.py @@ -11,7 +11,7 @@ from scipy.integrate import RK23, RK45, DOP853, Radau, BDF, LSODA from scipy.spatial.distance import euclidean -from ._base import PointType, InitialInfo, PathInfo, ResultPathInfo, logger, MPE_MODULE +from ._base import PointType, InitialInfo, PathInfo, PathInfoResult, logger, MPE_MODULE from ._helpers import set_module from ._parameters import Parameters, OdeSolverMethod, default_parameters from ._exceptions import ComputeTravelTimeError, PathExtractionError, EndPointNotReachedError @@ -324,7 +324,7 @@ def right_hand_func(time: float, point: np.ndarray) -> np.ndarray: # noqa def extract_path_without_way_points(init_info: InitialInfo, - parameters: Parameters) -> ResultPathInfo: + parameters: Parameters) -> PathInfoResult: extractor = MinimalPathExtractor(init_info.speed_data, init_info.end_point, parameters) result = extractor(init_info.start_point) @@ -333,14 +333,14 @@ def extract_path_without_way_points(init_info: InitialInfo, start_point=init_info.start_point, end_point=init_info.end_point, travel_time=extractor.travel_time, - path_travel_times=np.asarray(result.path_travel_times), + extraction_result=result, reversed=False, ) - return ResultPathInfo(path=path_info.path, pieces=[path_info]) + return PathInfoResult(path=path_info.path, pieces=[path_info]) -def make_whole_path_from_pieces(path_pieces_info: List[PathInfo]) -> ResultPathInfo: +def make_whole_path_from_pieces(path_pieces_info: List[PathInfo]) -> PathInfoResult: path_pieces = [path_pieces_info[0].path] for path_info in path_pieces_info[1:]: @@ -349,14 +349,14 @@ def make_whole_path_from_pieces(path_pieces_info: List[PathInfo]) -> ResultPathI path = np.flipud(path) path_pieces.append(path) - return ResultPathInfo( + return PathInfoResult( path=np.vstack(path_pieces), pieces=path_pieces_info, ) def extract_path_with_way_points(init_info: InitialInfo, - parameters: Parameters) -> ResultPathInfo: + parameters: Parameters) -> PathInfoResult: speed_data = init_info.speed_data path_pieces_info = [] @@ -382,7 +382,7 @@ def extract_path_with_way_points(init_info: InitialInfo, start_point=start_point, end_point=end_point, travel_time=extractor.travel_time, - path_travel_times=np.asarray(result.path_travel_times), + extraction_result=result, reversed=is_reversed )) else: @@ -395,7 +395,7 @@ def extract_path_with_way_points(init_info: InitialInfo, start_point=start_point, end_point=end_point, travel_time=extractor.travel_time, - path_travel_times=np.asarray(result.path_travel_times), + extraction_result=result, reversed=False ) @@ -405,7 +405,7 @@ def extract_path_with_way_points(init_info: InitialInfo, def extract_path(init_info: InitialInfo, - parameters: Optional[Parameters] = None) -> ResultPathInfo: + parameters: Optional[Parameters] = None) -> PathInfoResult: if parameters is None: parameters = default_parameters() From 908bd0273651bb5a86c10b2448fce5f653458c17 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Thu, 21 May 2020 15:53:10 +0300 Subject: [PATCH 05/33] fix type --- skmpe/_mpe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skmpe/_mpe.py b/skmpe/_mpe.py index 598bafb..c702316 100644 --- a/skmpe/_mpe.py +++ b/skmpe/_mpe.py @@ -337,7 +337,7 @@ def extract_path_without_way_points(init_info: InitialInfo, reversed=False, ) - return PathInfoResult(path=path_info.path, pieces=[path_info]) + return PathInfoResult(path=path_info.path, pieces=(path_info,)) def make_whole_path_from_pieces(path_pieces_info: List[PathInfo]) -> PathInfoResult: From 187bf1f61a0c0cc89ccb32b23ce91fec0211efa1 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Thu, 21 May 2020 18:55:51 +0300 Subject: [PATCH 06/33] fix docstring --- skmpe/_mpe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skmpe/_mpe.py b/skmpe/_mpe.py index c702316..ed02ec7 100644 --- a/skmpe/_mpe.py +++ b/skmpe/_mpe.py @@ -195,8 +195,8 @@ def __call__(self, start_point: PointType) -> PathExtractionResult: Returns ------- - path_extraction_result : PathExtractionResult - The path extraction result in :class:`ExtractedPathResult` instance + path_extraction_result : :class:`PathExtractionResult` + The path extraction result Raises ------ From 63aeddf04be400dc504b4d172cc8ca1e4d154d8c Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Thu, 21 May 2020 19:18:23 +0300 Subject: [PATCH 07/33] fix using y_old value --- skmpe/_mpe.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/skmpe/_mpe.py b/skmpe/_mpe.py index ed02ec7..09ecefb 100644 --- a/skmpe/_mpe.py +++ b/skmpe/_mpe.py @@ -242,6 +242,7 @@ def right_hand_func(time: float, point: np.ndarray) -> np.ndarray: # noqa end_point = self._source_point dist_tol = self.parameters.dist_tol + y = None y_old = start_point small_dist_steps_left = self.parameters.max_small_dist_steps @@ -254,14 +255,14 @@ def right_hand_func(time: float, point: np.ndarray) -> np.ndarray: # noqa f"ODE solver '{solver_cls.__name__}' has failed: {message}", travel_time=self.travel_time, start_point=start_point, end_point=end_point) - t = solver.t + if y is not None: + y_old = y + y = solver.y + t = solver.t tt = travel_time_interpolant(y).item() add_point = True - - if solver.y_old is not None: - y_old = solver.y_old y_dist = euclidean(y, y_old) if y_dist < dist_tol: From 1ed2aae71ead93b56a6762ebd105074bf9f0c1fd Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Thu, 21 May 2020 19:18:49 +0300 Subject: [PATCH 08/33] add tests parameters for all ODE solvers --- tests/test_mpe.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/tests/test_mpe.py b/tests/test_mpe.py index 0c70d37..8dda1d9 100644 --- a/tests/test_mpe.py +++ b/tests/test_mpe.py @@ -5,7 +5,7 @@ import pytest import numpy as np -from skmpe import mpe, parameters, EndPointNotReachedError +from skmpe import mpe, parameters, OdeSolverMethod, EndPointNotReachedError travel_time_order_param = pytest.mark.parametrize('travel_time_order', [ @@ -14,13 +14,19 @@ ]) -@pytest.mark.parametrize('start_point, end_point, point_count', [ - ((37, 255), (172, 112), 79), - ((37, 255), (484, 300), 189), +@pytest.mark.parametrize('ode_method, start_point, end_point, point_count', [ + (OdeSolverMethod.RK23, (37, 255), (172, 112), 82), + (OdeSolverMethod.RK45, (37, 255), (172, 112), 79), + (OdeSolverMethod.DOP853, (37, 255), (172, 112), 79), + (OdeSolverMethod.Radau, (37, 255), (172, 112), 80), + (OdeSolverMethod.BDF, (37, 255), (172, 112), 94), + (OdeSolverMethod.LSODA, (37, 255), (172, 112), 153), + + (OdeSolverMethod.RK45, (37, 255), (484, 300), 189), ]) @travel_time_order_param -def test_extract_without_waypoints(retina_speed_image, travel_time_order, start_point, end_point, point_count): - with parameters(travel_time_order=travel_time_order): +def test_extract_path_without_waypoints(retina_speed_image, travel_time_order, ode_method, start_point, end_point, point_count): + with parameters(ode_solver_method=ode_method, travel_time_order=travel_time_order): path_info = mpe(retina_speed_image, start_point, end_point) assert path_info.point_count == point_count @@ -31,9 +37,9 @@ def test_extract_without_waypoints(retina_speed_image, travel_time_order, start_ ((37, 255), (484, 300), ((172, 112), (236, 98), (420, 153)), False, 199, 4, 0), ]) @travel_time_order_param -def test_extract_with_waypoints(retina_speed_image, travel_time_order, - start_point, end_point, way_points, ttime_cache, - point_count, ttime_count, reversed_count): +def test_extract_path_with_waypoints(retina_speed_image, travel_time_order, + start_point, end_point, way_points, ttime_cache, + point_count, ttime_count, reversed_count): with parameters(travel_time_order=travel_time_order, travel_time_cache=ttime_cache): path_info = mpe(retina_speed_image, start_point, end_point, way_points) From 1d04ad5b87d4950bbe9e7d5bc341ee1f8f54f4f8 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Thu, 21 May 2020 19:42:10 +0300 Subject: [PATCH 09/33] update docs --- docs/index.rst | 30 +++++++++++++++++++++++++++++- docs/tutorial.rst | 5 +++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 docs/tutorial.rst diff --git a/docs/index.rst b/docs/index.rst index 4568d52..6e14a04 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,14 +3,42 @@ scikit-mpe ========== +Overview +-------- + **scikit-mpe** is a package for extracting a minimal path in n-dimensional Euclidean space (on regular Cartesian grids) using `the fast marching method `_. +A Simple Example +~~~~~~~~~~~~~~~~ + +Here is the simple example: how to extract 2-d minimal path using some speed data. + +.. code-block:: python + :linenos: + + from skmpe import mpe + + # Somehow speed data is calculating + speed_data = get_speed_data() + + # Extracting minimal path from the starting point to the ending point + path_info = mpe(speed_data, start_point=(10, 20), end_point=(120, 45)) + + # Getting the path data in numpy ndarray + path = path_info.path + + +Installing +---------- + +Contents +-------- .. toctree:: :maxdepth: 2 - :caption: Contents: + tutorial api changelog diff --git a/docs/tutorial.rst b/docs/tutorial.rst new file mode 100644 index 0000000..308431c --- /dev/null +++ b/docs/tutorial.rst @@ -0,0 +1,5 @@ +.. _tutorial: + +******** +Tutorial +******** From 3c34d0fb5a0ddb031ee65fe6f3d2a40322739ca7 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Thu, 21 May 2020 23:44:15 +0300 Subject: [PATCH 10/33] update docs --- docs/index.rst | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 6e14a04..d176346 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,8 +9,20 @@ Overview **scikit-mpe** is a package for extracting a minimal path in n-dimensional Euclidean space (on regular Cartesian grids) using `the fast marching method `_. +The package can be used in various engineering and image processing tasks. +For example, it can be used for extracting paths through tubular structures on 2-d and 3-d images, +or shortest paths on terrain maps. + +Installing +---------- + +Python 3.6 or above is supported. You can install the package using pip:: + + pip install -U scikit-mpe + + A Simple Example -~~~~~~~~~~~~~~~~ +---------------- Here is the simple example: how to extract 2-d minimal path using some speed data. @@ -29,9 +41,6 @@ Here is the simple example: how to extract 2-d minimal path using some speed dat path = path_info.path -Installing ----------- - Contents -------- From 3c7d3780ea3c166301f0d449f63a98848df7fc5a Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Fri, 22 May 2020 16:14:37 +0300 Subject: [PATCH 11/33] fix tests: do not check the number of points --- tests/test_mpe.py | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/tests/test_mpe.py b/tests/test_mpe.py index 8dda1d9..feb8a5e 100644 --- a/tests/test_mpe.py +++ b/tests/test_mpe.py @@ -14,36 +14,44 @@ ]) -@pytest.mark.parametrize('ode_method, start_point, end_point, point_count', [ - (OdeSolverMethod.RK23, (37, 255), (172, 112), 82), - (OdeSolverMethod.RK45, (37, 255), (172, 112), 79), - (OdeSolverMethod.DOP853, (37, 255), (172, 112), 79), - (OdeSolverMethod.Radau, (37, 255), (172, 112), 80), - (OdeSolverMethod.BDF, (37, 255), (172, 112), 94), - (OdeSolverMethod.LSODA, (37, 255), (172, 112), 153), - - (OdeSolverMethod.RK45, (37, 255), (484, 300), 189), +@pytest.mark.parametrize('ode_method, start_point, end_point', [ + (OdeSolverMethod.RK23, (37, 255), (172, 112)), + (OdeSolverMethod.RK45, (37, 255), (172, 112)), + (OdeSolverMethod.DOP853, (37, 255), (172, 112)), + (OdeSolverMethod.Radau, (37, 255), (172, 112)), + (OdeSolverMethod.BDF, (37, 255), (172, 112)), + (OdeSolverMethod.LSODA, (37, 255), (172, 112)), + + (OdeSolverMethod.RK45, (37, 255), (484, 300)), ]) @travel_time_order_param -def test_extract_path_without_waypoints(retina_speed_image, travel_time_order, ode_method, start_point, end_point, point_count): +def test_extract_path_without_waypoints(retina_speed_image, travel_time_order, ode_method, start_point, end_point): with parameters(ode_solver_method=ode_method, travel_time_order=travel_time_order): path_info = mpe(retina_speed_image, start_point, end_point) - assert path_info.point_count == point_count + path_piece_info = path_info.pieces[0] + start_travel_time = path_piece_info.travel_time[start_point[0], start_point[1]] + path_start_travel_time = path_piece_info.extraction_result.path_travel_times[0] + assert path_start_travel_time == pytest.approx(start_travel_time, abs=50) -@pytest.mark.parametrize('start_point, end_point, way_points, ttime_cache, point_count, ttime_count, reversed_count', [ - ((37, 255), (484, 300), ((172, 112), (236, 98), (420, 153)), True, 200, 2, 2), - ((37, 255), (484, 300), ((172, 112), (236, 98), (420, 153)), False, 199, 4, 0), + +@pytest.mark.parametrize('start_point, end_point, way_points, ttime_cache, ttime_count, reversed_count', [ + ((37, 255), (484, 300), ((172, 112), (236, 98), (420, 153)), True, 2, 2), + ((37, 255), (484, 300), ((172, 112), (236, 98), (420, 153)), False, 4, 0), ]) @travel_time_order_param def test_extract_path_with_waypoints(retina_speed_image, travel_time_order, start_point, end_point, way_points, ttime_cache, - point_count, ttime_count, reversed_count): + ttime_count, reversed_count): with parameters(travel_time_order=travel_time_order, travel_time_cache=ttime_cache): path_info = mpe(retina_speed_image, start_point, end_point, way_points) - assert path_info.point_count == point_count + path_piece_info = path_info.pieces[0] + start_travel_time = path_piece_info.travel_time[start_point[0], start_point[1]] + path_start_travel_time = path_piece_info.extraction_result.path_travel_times[0] + + assert path_start_travel_time == pytest.approx(start_travel_time, abs=50) ttime_counter = collections.Counter(id(piece.travel_time) for piece in path_info.pieces) assert len(ttime_counter) == ttime_count From d01d9007e7ed4b4521168bdb7355d745e535b37d Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Fri, 22 May 2020 17:16:20 +0300 Subject: [PATCH 12/33] fix import modules in __init__ and __all__ --- skmpe/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skmpe/__init__.py b/skmpe/__init__.py index f6c913d..9b2f2d9 100644 --- a/skmpe/__init__.py +++ b/skmpe/__init__.py @@ -39,6 +39,7 @@ 'PathInfoResult', 'TravelTimeOrder', + 'OdeSolverMethod', 'Parameters', 'parameters', 'default_parameters', @@ -48,6 +49,7 @@ 'PathExtractionError', 'EndPointNotReachedError', + 'PathExtractionResult', 'MinimalPathExtractor', 'mpe', ] From 6dd3358d9b1f06c81e3f80a9b77a3b3746ce1314 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Fri, 22 May 2020 17:43:47 +0300 Subject: [PATCH 13/33] improve tests: check path travel times near start/end points --- tests/test_mpe.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/tests/test_mpe.py b/tests/test_mpe.py index feb8a5e..905d4ca 100644 --- a/tests/test_mpe.py +++ b/tests/test_mpe.py @@ -8,6 +8,8 @@ from skmpe import mpe, parameters, OdeSolverMethod, EndPointNotReachedError +TRAVEL_TIME_ABS_TOL = 100 + travel_time_order_param = pytest.mark.parametrize('travel_time_order', [ pytest.param(1), pytest.param(2, marks=pytest.mark.skip('https://github.com/scikit-fmm/scikit-fmm/issues/28')), @@ -31,9 +33,12 @@ def test_extract_path_without_waypoints(retina_speed_image, travel_time_order, o path_piece_info = path_info.pieces[0] start_travel_time = path_piece_info.travel_time[start_point[0], start_point[1]] + end_travel_time = path_piece_info.travel_time[end_point[0], end_point[1]] path_start_travel_time = path_piece_info.extraction_result.path_travel_times[0] + path_end_travel_time = path_piece_info.extraction_result.path_travel_times[-1] - assert path_start_travel_time == pytest.approx(start_travel_time, abs=50) + assert path_start_travel_time == pytest.approx(start_travel_time, abs=TRAVEL_TIME_ABS_TOL) + assert path_end_travel_time == pytest.approx(end_travel_time, abs=TRAVEL_TIME_ABS_TOL) @pytest.mark.parametrize('start_point, end_point, way_points, ttime_cache, ttime_count, reversed_count', [ @@ -47,11 +52,17 @@ def test_extract_path_with_waypoints(retina_speed_image, travel_time_order, with parameters(travel_time_order=travel_time_order, travel_time_cache=ttime_cache): path_info = mpe(retina_speed_image, start_point, end_point, way_points) - path_piece_info = path_info.pieces[0] - start_travel_time = path_piece_info.travel_time[start_point[0], start_point[1]] - path_start_travel_time = path_piece_info.extraction_result.path_travel_times[0] + for path_piece_info in path_info.pieces: + start_pt = path_piece_info.start_point + end_pt = path_piece_info.end_point + + start_travel_time = path_piece_info.travel_time[start_pt[0], start_pt[1]] + end_travel_time = path_piece_info.travel_time[end_pt[0], end_pt[1]] + path_start_travel_time = path_piece_info.extraction_result.path_travel_times[0] + path_end_travel_time = path_piece_info.extraction_result.path_travel_times[-1] - assert path_start_travel_time == pytest.approx(start_travel_time, abs=50) + assert path_start_travel_time == pytest.approx(start_travel_time, abs=TRAVEL_TIME_ABS_TOL) + assert path_end_travel_time == pytest.approx(end_travel_time, abs=TRAVEL_TIME_ABS_TOL) ttime_counter = collections.Counter(id(piece.travel_time) for piece in path_info.pieces) assert len(ttime_counter) == ttime_count From 5b241ef3b7a082d5e8daaf1d35ac546248c939fd Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Sat, 23 May 2020 01:31:29 +0300 Subject: [PATCH 14/33] refactoring --- skmpe/_mpe.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/skmpe/_mpe.py b/skmpe/_mpe.py index 09ecefb..6e040fa 100644 --- a/skmpe/_mpe.py +++ b/skmpe/_mpe.py @@ -82,8 +82,8 @@ class MinimalPathExtractor: speed_data : np.ndarray The speed data (n-d numpy array) - source_point : Sequence[int] - The source point (aka "ending point") + end_point : Sequence[int] + The ending point (a.k.a. "source point") parameters : class:`Parameters` The parameters @@ -98,7 +98,7 @@ class MinimalPathExtractor: # some function for computing speed data speed_data_2d = compute_speed_data_2d() - mpe = MinimalPathExtractor(speed_data_2d, (10, 25)) + mpe = MinimalPathExtractor(speed_data_2d, end_point=(10, 25)) path = mpe((123, 34)) Raises @@ -107,13 +107,13 @@ class MinimalPathExtractor: """ - def __init__(self, speed_data: np.ndarray, source_point: PointType, + def __init__(self, speed_data: np.ndarray, end_point: PointType, parameters: Optional[Parameters] = None) -> None: if parameters is None: # pragma: no cover parameters = default_parameters() - travel_time, phi = self._compute_travel_time(speed_data, source_point, parameters) + travel_time, phi = self._compute_travel_time(speed_data, end_point, parameters) gradients = np.gradient(travel_time, parameters.travel_time_spacing) grad_interpolants, tt_interpolant, phi_interpolant = self._compute_interpolants(gradients, travel_time, phi) @@ -121,7 +121,7 @@ def __init__(self, speed_data: np.ndarray, source_point: PointType, self._travel_time = travel_time self._phi = phi - self._source_point = source_point + self._end_point = end_point self._travel_time_interpolant = tt_interpolant self._phi_interpolant = phi_interpolant @@ -239,7 +239,7 @@ def right_hand_func(time: float, point: np.ndarray) -> np.ndarray: # noqa self._path_travel_times = [] self._step_count = 0 - end_point = self._source_point + end_point = self._end_point dist_tol = self.parameters.dist_tol y = None @@ -352,7 +352,7 @@ def make_whole_path_from_pieces(path_pieces_info: List[PathInfo]) -> PathInfoRes return PathInfoResult( path=np.vstack(path_pieces), - pieces=path_pieces_info, + pieces=tuple(path_pieces_info), ) From b833285239f255c3dda0fea22accfd7eeef53e38 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Sat, 23 May 2020 02:19:26 +0300 Subject: [PATCH 15/33] refactoring --- docs/api.rst | 2 + skmpe/__init__.py | 4 +- skmpe/_api.py | 88 +++++++++++++++++++++++++++++++++++---- skmpe/_base.py | 98 +++++++++++++++++++++----------------------- skmpe/_exceptions.py | 11 +++-- skmpe/_helpers.py | 51 ----------------------- skmpe/_mpe.py | 7 ++-- skmpe/_parameters.py | 11 ++--- 8 files changed, 143 insertions(+), 129 deletions(-) delete mode 100644 skmpe/_helpers.py diff --git a/docs/api.rst b/docs/api.rst index 83f13ac..76b3605 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -93,3 +93,5 @@ Path Extraction .. autoclass:: MinimalPathExtractor :members: :special-members: __call__ + +.. autofunction:: mpe diff --git a/skmpe/__init__.py b/skmpe/__init__.py index 9b2f2d9..ed8ce12 100644 --- a/skmpe/__init__.py +++ b/skmpe/__init__.py @@ -4,7 +4,6 @@ InitialInfo, PathInfo, PathInfoResult, - mpe ) from ._parameters import ( @@ -27,8 +26,7 @@ MinimalPathExtractor, ) -# register dispatchered API -import skmpe._api as _api # noqa +from skmpe._api import mpe __version__ = '0.1.1' diff --git a/skmpe/_api.py b/skmpe/_api.py index f159a02..a0627ce 100644 --- a/skmpe/_api.py +++ b/skmpe/_api.py @@ -1,22 +1,92 @@ # -*- coding: utf-8 -*- -from typing import Optional, Union +from typing import Optional, Union, overload import numpy as np -from ._base import mpe as api_dispatch -from ._base import PointType, PointSequenceType, InitialInfo, PathInfoResult +from ._base import mpe_module, singledispatch, PointType, PointSequenceType, InitialInfo, PathInfoResult from ._parameters import Parameters from ._mpe import extract_path -@api_dispatch.register(np.ndarray) # noqa +@overload def mpe(speed_data: np.ndarray, start_point: Union[PointType, np.ndarray], end_point: Union[PointType, np.ndarray], way_points: Union[PointSequenceType, np.ndarray] = (), *, - parameters: Optional[Parameters] = None) -> PathInfoResult: + parameters: Optional['Parameters'] = None) -> PathInfoResult: + pass # pragma: no cover + + +@overload +def mpe(init_info: InitialInfo, + *, + parameters: Optional['Parameters'] = None) -> PathInfoResult: + pass # pragma: no cover + + +@mpe_module +@singledispatch +def mpe(*args, **kwargs) -> PathInfoResult: # noqa + """Extracts a minimal path by start/end and optionally way points + + The function is high level API for extracting paths. + + Parameters + ---------- + init_info : :class:`InitialInfo` + (sign 1) The initial info + start_point : Sequence[int] + (sign 2) The starting point + end_point : Sequence[int] + (sign 2) The ending point + way_points : Sequence[Sequence[int]] + (sign 2) The way points + parameters : :class:`Parameters` + The parameters + + Notes + ----- + + There are two signatures of `mpe` function. + + Use :class:`InitialInfo` for init data: + + .. code-block:: python + + mpe(init_info: InitialInfo, *, + parameters: Optional[Parameters] = None) -> ResultPathInfo + + Set init data directly: + + .. code-block:: python + + mpe(speed_data: np.ndarray, *, + start_point: Sequence[int], + end_point: Sequence[int], + way_points: Sequence[Sequence[int]] = (), + parameters: Optional[Parameters] = None) -> ResultPathInfo + + Returns + ------- + path_info : :class:`PathInfoResult` + Extracted path info + + See Also + -------- + InitialInfo, Parameters, MinimalPathExtractor + + """ + + +@mpe.register(np.ndarray) # noqa +def mpe_1(speed_data: np.ndarray, + start_point: Union[PointType, np.ndarray], + end_point: Union[PointType, np.ndarray], + way_points: Union[PointSequenceType, np.ndarray] = (), + *, + parameters: Optional[Parameters] = None) -> PathInfoResult: init_info = InitialInfo( speed_data=speed_data, @@ -28,8 +98,8 @@ def mpe(speed_data: np.ndarray, return mpe(init_info, parameters=parameters) -@api_dispatch.register(InitialInfo) # noqa -def mpe(init_info: InitialInfo, - *, - parameters: Optional[Parameters] = None) -> PathInfoResult: +@mpe.register(InitialInfo) # noqa +def mpe_2(init_info: InitialInfo, + *, + parameters: Optional[Parameters] = None) -> PathInfoResult: return extract_path(init_info, parameters) diff --git a/skmpe/_base.py b/skmpe/_base.py index cc9ebe4..a96d12a 100644 --- a/skmpe/_base.py +++ b/skmpe/_base.py @@ -1,14 +1,14 @@ # -*- coding: utf-8 -*- import collections +import functools +import inspect import logging -from typing import Optional, Union, Tuple, Sequence, List, NamedTuple, overload, TYPE_CHECKING +from typing import Tuple, Sequence, List, NamedTuple, TYPE_CHECKING from pydantic import BaseModel, Extra, validator, root_validator import numpy as np -from ._helpers import set_module, singledispatch - if TYPE_CHECKING: from ._parameters import Parameters # noqa from ._mpe import PathExtractionResult # noqa @@ -30,7 +30,47 @@ logger.addHandler(logging.NullHandler()) -@set_module(MPE_MODULE) +def mpe_module(obj): + """Replace __module__ for decorated object + + Returns + ------- + obj : Any + The object with replaced __module__ + """ + + obj.__module__ = MPE_MODULE + return obj + + +def singledispatch(func_stub): + """Single dispatch wrapper with default implementation that raises the exception for invalid signatures + """ + + @functools.wraps(func_stub) + @functools.singledispatch + def _dispatch(*args, **kwargs): + sign_args = ', '.join(f'{type(arg).__name__}' for arg in args) + sign_kwargs = ', '.join(f'{kwname}: {type(kwvalue).__name__}' for kwname, kwvalue in kwargs.items()) + allowed_signs = '' + i = 0 + + for arg_type, func in _dispatch.registry.items(): + if arg_type is object: + continue + i += 1 + sign = inspect.signature(func) + allowed_signs += f' [{i}] => {sign}\n' + + raise TypeError( + f"call '{func_stub.__name__}' with invalid signature:\n" + f" => ({sign_args}, {sign_kwargs})\n\n" + f"allowed signatures:\n{allowed_signs}") + + return _dispatch + + +@mpe_module class ImmutableDataObject(BaseModel): """Base immutable data object with validating fields """ @@ -41,7 +81,7 @@ class Config: allow_mutation = False -@set_module(MPE_MODULE) +@mpe_module class InitialInfo(ImmutableDataObject): """Initial info data model @@ -162,7 +202,7 @@ def point_intervals(self) -> List[Tuple[PointType, PointType]]: return list(zip(all_points[:-1], all_points[1:])) -@set_module(MPE_MODULE) +@mpe_module class PathInfo(NamedTuple): """The named tuple with info about extracted path or piece of path @@ -200,7 +240,7 @@ class PathInfo(NamedTuple): reversed: bool -@set_module(MPE_MODULE) +@mpe_module class PathInfoResult(NamedTuple): """The named tuple with path info result @@ -220,47 +260,3 @@ class PathInfoResult(NamedTuple): def point_count(self) -> int: """Returns the number of path points""" return self.path.shape[0] - - -@overload -def mpe(speed_data: np.ndarray, - start_point: Union[PointType, np.ndarray], - end_point: Union[PointType, np.ndarray], - way_points: Union[PointSequenceType, np.ndarray] = (), - *, - parameters: Optional['Parameters'] = None) -> PathInfoResult: - pass # pragma: no cover - - -@overload -def mpe(init_info: InitialInfo, - *, - parameters: Optional['Parameters'] = None) -> PathInfoResult: - pass # pragma: no cover - - -@set_module(MPE_MODULE) -@singledispatch -def mpe(*args, **kwargs) -> PathInfoResult: # noqa - """Extracts a minimal path - - Usage - ----- - - .. code-block:: python - - mpe(init_info: InitialInfo, *, - parameters: Optional[Parameters] = None) -> ResultPathInfo - - mpe(speed_data: np.ndarray, *, - start_point: Sequence[int], - end_point: Sequence[int], - way_points: Sequence[Sequence[int]] = (), - parameters: Optional[Parameters] = None) -> ResultPathInfo - - Returns - ------- - path_info : PathInfoResult - Extracted path info - - """ diff --git a/skmpe/_exceptions.py b/skmpe/_exceptions.py index 6c7922e..a617011 100644 --- a/skmpe/_exceptions.py +++ b/skmpe/_exceptions.py @@ -4,21 +4,20 @@ import numpy as np -from ._base import PointType, FloatPointType, MPE_MODULE -from ._helpers import set_module +from ._base import mpe_module, PointType, FloatPointType -@set_module(MPE_MODULE) +@mpe_module class MPEError(Exception): """Base exception class for all MPE errors""" -@set_module(MPE_MODULE) +@mpe_module class ComputeTravelTimeError(MPEError): """The exception occurs when computing travel time has failed""" -@set_module(MPE_MODULE) +@mpe_module class PathExtractionError(MPEError): """Base exception class for all extracting path errors""" @@ -48,7 +47,7 @@ def end_point(self) -> PointType: return self._end_point -@set_module(MPE_MODULE) +@mpe_module class EndPointNotReachedError(PathExtractionError): """The exception occurs when the ending point is not reached""" diff --git a/skmpe/_helpers.py b/skmpe/_helpers.py deleted file mode 100644 index ac96c6f..0000000 --- a/skmpe/_helpers.py +++ /dev/null @@ -1,51 +0,0 @@ -# -*- coding: utf-8 -*- - -import functools -import inspect - - -def set_module(name: str): - """Replace __module__ for decorated object - - Parameters - ---------- - name : str - Module name - - Returns - ------- - obj : Any - The object with replaced __module__ - """ - - def decorator(obj): - obj.__module__ = name - return obj - return decorator - - -def singledispatch(func_stub): - """Single dispatch wrapper with default implementation that raises the exception for invalid signatures - """ - - @functools.wraps(func_stub) - @functools.singledispatch - def _dispatch(*args, **kwargs): - sign_args = ', '.join(f'{type(arg).__name__}' for arg in args) - sign_kwargs = ', '.join(f'{kwname}: {type(kwvalue).__name__}' for kwname, kwvalue in kwargs.items()) - allowed_signs = '' - i = 0 - - for arg_type, func in _dispatch.registry.items(): - if arg_type is object: - continue - i += 1 - sign = inspect.signature(func) - allowed_signs += f' [{i}] => {sign}\n' - - raise TypeError( - f"call '{func_stub.__name__}' with invalid signature:\n" - f" => ({sign_args}, {sign_kwargs})\n\n" - f"allowed signatures:\n{allowed_signs}") - - return _dispatch diff --git a/skmpe/_mpe.py b/skmpe/_mpe.py index 6e040fa..89d9261 100644 --- a/skmpe/_mpe.py +++ b/skmpe/_mpe.py @@ -11,8 +11,7 @@ from scipy.integrate import RK23, RK45, DOP853, Radau, BDF, LSODA from scipy.spatial.distance import euclidean -from ._base import PointType, InitialInfo, PathInfo, PathInfoResult, logger, MPE_MODULE -from ._helpers import set_module +from ._base import mpe_module, PointType, InitialInfo, PathInfo, PathInfoResult, logger from ._parameters import Parameters, OdeSolverMethod, default_parameters from ._exceptions import ComputeTravelTimeError, PathExtractionError, EndPointNotReachedError @@ -32,7 +31,7 @@ def make_interpolator(coords, values, fill_value: float = 0.0): } -@set_module(MPE_MODULE) +@mpe_module class PathExtractionResult(NamedTuple): """The named tuple with info about extracted path @@ -70,7 +69,7 @@ class PathExtractionResult(NamedTuple): func_eval_count: int -@set_module(MPE_MODULE) +@mpe_module class MinimalPathExtractor: """Minimal path extractor diff --git a/skmpe/_parameters.py b/skmpe/_parameters.py index 93ff9d1..12b7e9f 100644 --- a/skmpe/_parameters.py +++ b/skmpe/_parameters.py @@ -5,11 +5,10 @@ from pydantic import confloat, conint, validator -from ._base import MPE_MODULE, ImmutableDataObject -from ._helpers import set_module +from ._base import mpe_module, ImmutableDataObject -@set_module(MPE_MODULE) +@mpe_module class TravelTimeOrder(enum.IntEnum): """The enumeration of travel time computation orders @@ -24,7 +23,7 @@ class TravelTimeOrder(enum.IntEnum): second = 2 -@set_module(MPE_MODULE) +@mpe_module class OdeSolverMethod(str, enum.Enum): """The enumeration of ODE solver methods """ @@ -37,7 +36,7 @@ class OdeSolverMethod(str, enum.Enum): LSODA = 'LSODA' -@set_module(MPE_MODULE) +@mpe_module class Parameters(ImmutableDataObject): """MPE algorithm parameters model @@ -123,6 +122,7 @@ def _check_travel_time_order(cls, v): _default_parameters = Parameters() +@mpe_module @contextlib.contextmanager def parameters(**kwargs): """Context manager for using specified parameters @@ -176,6 +176,7 @@ def parameters(**kwargs): _default_parameters = prev_default_parameters +@mpe_module def default_parameters() -> Parameters: """Returns the default parameters From 4d293f2f0a023642da5f5bccf188e348c5d67e98 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Sat, 23 May 2020 02:34:04 +0300 Subject: [PATCH 16/33] refactoring: add solver cls to OdeSolverMethod enum --- skmpe/_mpe.py | 13 +------------ skmpe/_parameters.py | 26 +++++++++++++++++++------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/skmpe/_mpe.py b/skmpe/_mpe.py index 89d9261..09caa36 100644 --- a/skmpe/_mpe.py +++ b/skmpe/_mpe.py @@ -8,7 +8,6 @@ import skfmm as fmm from scipy.interpolate import RegularGridInterpolator -from scipy.integrate import RK23, RK45, DOP853, Radau, BDF, LSODA from scipy.spatial.distance import euclidean from ._base import mpe_module, PointType, InitialInfo, PathInfo, PathInfoResult, logger @@ -21,16 +20,6 @@ def make_interpolator(coords, values, fill_value: float = 0.0): coords, values, method='linear', bounds_error=False, fill_value=fill_value) -ODE_SOLVER_METHODS = { - OdeSolverMethod.RK23: RK23, - OdeSolverMethod.RK45: RK45, - OdeSolverMethod.DOP853: DOP853, - OdeSolverMethod.Radau: Radau, - OdeSolverMethod.BDF: BDF, - OdeSolverMethod.LSODA: LSODA, -} - - @mpe_module class PathExtractionResult(NamedTuple): """The named tuple with info about extracted path @@ -216,7 +205,7 @@ def right_hand_func(time: float, point: np.ndarray) -> np.ndarray: # noqa return -velocity / np.linalg.norm(velocity) - solver_cls = ODE_SOLVER_METHODS[self.parameters.ode_solver_method] + solver_cls = self.parameters.ode_solver_method.solver logger.debug("ODE solver '%s' will be used.", solver_cls.__name__) with warnings.catch_warnings(): diff --git a/skmpe/_parameters.py b/skmpe/_parameters.py index 12b7e9f..5e72c3f 100644 --- a/skmpe/_parameters.py +++ b/skmpe/_parameters.py @@ -2,8 +2,10 @@ import contextlib import enum +from typing import Type, Union from pydantic import confloat, conint, validator +from scipy.integrate import RK23, RK45, DOP853, Radau, BDF, LSODA from ._base import mpe_module, ImmutableDataObject @@ -23,17 +25,27 @@ class TravelTimeOrder(enum.IntEnum): second = 2 +SupportedOdeSolvers = Union[RK23, RK45, DOP853, Radau, BDF, LSODA] + + @mpe_module class OdeSolverMethod(str, enum.Enum): - """The enumeration of ODE solver methods + """The enumeration of supported ODE solver methods """ - RK23 = 'RK23' - RK45 = 'RK45' - DOP853 = 'DOP853' - Radau = 'Radau' - BDF = 'BDF' - LSODA = 'LSODA' + RK23 = 'RK23', RK23 + RK45 = 'RK45', RK45 + DOP853 = 'DOP853', DOP853 + Radau = 'Radau', Radau + BDF = 'BDF', BDF + LSODA = 'LSODA', LSODA + + def __new__(cls, value: str, solver: Type[SupportedOdeSolvers]): + obj = str.__new__(cls, value) # noqa + obj._value_ = value + + obj.solver = solver + return obj @mpe_module From 1a384ab13f307e3e68c18adc9741d66c0f9970f5 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Sat, 23 May 2020 02:41:43 +0300 Subject: [PATCH 17/33] add noqa --- skmpe/_parameters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skmpe/_parameters.py b/skmpe/_parameters.py index 5e72c3f..655e62f 100644 --- a/skmpe/_parameters.py +++ b/skmpe/_parameters.py @@ -122,7 +122,7 @@ class Parameters(ImmutableDataObject): max_small_dist_steps: conint(strict=True, gt=1) = 100 @validator('travel_time_order') - def _check_travel_time_order(cls, v): + def _check_travel_time_order(cls, v): # noqa if v == TravelTimeOrder.second: raise ValueError( 'Currently the second order for computing travel time does not work properly.' From 4b761fe54403601d2616b07d90cac14240d53c8b Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Sat, 23 May 2020 02:42:47 +0300 Subject: [PATCH 18/33] add noqa --- skmpe/_base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skmpe/_base.py b/skmpe/_base.py index a96d12a..997cb2a 100644 --- a/skmpe/_base.py +++ b/skmpe/_base.py @@ -110,11 +110,11 @@ class InitialInfo(ImmutableDataObject): way_points: InitialWayPointsType = () @validator('start_point', 'end_point', 'way_points', pre=True) - def _to_canonical(cls, v): + def _to_canonical(cls, v): # noqa return np.asarray(v).tolist() @root_validator - def _check_ndim(cls, values): + def _check_ndim(cls, values): # noqa speed_data = values.get('speed_data') start_point = values.get('start_point') @@ -142,7 +142,7 @@ def _check_ndim(cls, values): return values @root_validator - def _check_point_duplicates(cls, values): + def _check_point_duplicates(cls, values): # noqa start_point = values.get('start_point') end_point = values.get('end_point') way_points = values.get('way_points') @@ -160,7 +160,7 @@ def _check_point_duplicates(cls, values): return values @validator('start_point', 'end_point', 'way_points') - def _check_points(cls, v, field, values): + def _check_points(cls, v, field, values): # noqa if v is None: return v # pragma: no cover From 32d5d00895db31bed60a80e169a81b6d30f20dd5 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Sat, 23 May 2020 05:38:56 +0300 Subject: [PATCH 19/33] remove extra deps --- pyproject.toml | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 635c96d..cf34e69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,23 +39,6 @@ scipy = "^1.4.1" scikit-fmm = "^2019.1.30" pydantic = "^1.4" -# tests -pytest = { version = "^5.3.5", optional = true } -pytest-cov = { version = "^2.8.1", optional = true } -coveralls = { version = "^1.10.0", optional = true } -scikit-image = { version = "^0.16.2", optional = true } - -# docs -sphinx = { version = "^2.3.1", optional = true } -matplotlib = { version = "^3.1.3", optional = true } -numpydoc = { version = "^0.9.2", optional = true } -m2r = { version = "^0.2.1", optional = true } -toml = { version = "^0.10.0", optional = true } - -[tool.poetry.extras] -tests = ["pytest", "pytest-cov", "coveralls", "scikit-image"] -docs = ["sphinx", "numpydoc", "m2r", "matplotlib", "toml"] - [tool.poetry.dev-dependencies] flake8 = "^3.7.9" flake8-colors = "^0.1.6" From 906a9e5254543b44bcde898a38497368c77d2434 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Sat, 23 May 2020 05:39:05 +0300 Subject: [PATCH 20/33] update docs --- docs/conf.py | 1 - docs/index.rst | 3 -- docs/tutorial.rst | 109 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index a689a2c..4a12bcb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -65,7 +65,6 @@ def get_author(): plot_apply_rcparams = True plot_rcparams = { 'figure.autolayout': 'True', - 'figure.figsize': '5, 3.5', 'savefig.bbox': 'tight', 'savefig.facecolor': "None", } diff --git a/docs/index.rst b/docs/index.rst index d176346..f586b88 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -3,9 +3,6 @@ scikit-mpe ========== -Overview --------- - **scikit-mpe** is a package for extracting a minimal path in n-dimensional Euclidean space (on regular Cartesian grids) using `the fast marching method `_. diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 308431c..c988f5f 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -3,3 +3,112 @@ ******** Tutorial ******** + +Overview +======== + +**scikit-mpe** package allows you to extract N-dimensional minimal paths +using existing speed data and starting/ending and optionally way points initial data. + +.. note:: + + The package does not compute any speed data (a.k.a speed function). It is expected that the + speed data was previously obtained/computed in some way. + +The package can be useful for various engineering and image processing tasks. +For example, the package can be used for extracting paths through tubular structures +on 2-d and 3-d images, or shortest paths on a terrain map. + +The package uses `the fast marching method `_ and +`ODE solver `_ for extracting minimal paths. + +Algorithm +========= + +The algorithm contains two main steps: + + - First, the travel time is computing from the given ending point (zero contour) to every speed data point + using the `fast marching method `_. + - Second, the minimal path (travel time is minimizing) is extracting from the starting point to the ending point + using ODE solver (`Runge-Kutta `_ for example) + for solving the differential equation :math:`x_t = - \nabla_t / | \nabla_t` + +If we have way points we need to perform these two steps for every interval between the starting point, the set of the +way points and the ending point and concatenate the path pieces to the full path. + +Quickstart +========== + +Let's look at a simple example of how the algorithm works. + +.. note:: + + We will use `retina test image `_ from + `scikit-image `_ package as the test data for all examples. + +First, we need a speed data (speed function). We can use one of the tubeness filters for computing speed data for +our test data, `sato filter `_ for example. + +.. plot:: + :context: + + from skimage.data import retina + from skimage.color import rgb2gray + from skimage.transform import rescale + from skimage.filters import sato + + image_data = rescale(rgb2gray(retina()), 0.5) + speed_data = sato(image_data) + 0.05 + speed_data[speed_data > 1.0] = 1.0 + + _, (ax1, ax2) = plt.subplots(1, 2) + ax1.imshow(image_data, cmap='gray') + ax1.set_title('source data') + ax1.axis('off') + ax2.imshow(speed_data, cmap='gray') + ax2.set_title('speed data') + ax2.axis('off') + +The speed data values must be in range [0.0, 1.0] and can be `masked `_ also. + +where: + + - 0.0 -- zero speed (impassable) + - 1.0 -- max speed + - masked -- impassable + +Second, let's try to extract the minimal path for some starting and ending points +using **scikit-mpe** package and plot it. Also we can plot travel time contours. + +.. plot:: + :context: close-figs + + from skmpe import mpe + + # define starting and ending points + start_point = (165, 280) + end_point = (611, 442) + + path_info = mpe(speed_data, start_point, end_point) + + # get computed travel time for given ending point and extracted path + travel_time = path_info.pieces[0].travel_time + path = path_info.path + + nrows, ncols = speed_data.shape + xx, yy = np.meshgrid(np.arange(ncols), np.arange(nrows)) + + fig, ax = plt.subplots(1, 1) + ax.imshow(speed_data, cmap='gray', alpha=0.9) + ax.plot(path[:,1], path[:,0], '-', color=[0, 1, 0], linewidth=2) + ax.plot(start_point[1], start_point[0], 'or') + ax.plot(end_point[1], end_point[0], 'o', color=[1, 1, 0]) + tt_c = ax.contour(xx, yy, travel_time, 20, cmap='plasma', linewidths=1.5) + ax.clabel(tt_c, inline=1, fontsize=9, fmt='%d') + ax.set_title('minimal path and travel time contours') + ax.axis('off') + cb = fig.colorbar(tt_c) + cb.ax.set_ylabel('travel time') + +Advanced Usage +============== From 18b74321b12ec0bad0f86d54e77410947ac002ae Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Sat, 23 May 2020 23:43:35 +0300 Subject: [PATCH 21/33] update docs --- docs/tutorial.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index c988f5f..7cd9b03 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -31,7 +31,7 @@ The algorithm contains two main steps: using the `fast marching method `_. - Second, the minimal path (travel time is minimizing) is extracting from the starting point to the ending point using ODE solver (`Runge-Kutta `_ for example) - for solving the differential equation :math:`x_t = - \nabla_t / | \nabla_t` + for solving the differential equation :math:`x_t = - \nabla (t) / | \nabla (t)` If we have way points we need to perform these two steps for every interval between the starting point, the set of the way points and the ending point and concatenate the path pieces to the full path. @@ -105,7 +105,7 @@ using **scikit-mpe** package and plot it. Also we can plot travel time contours. ax.plot(end_point[1], end_point[0], 'o', color=[1, 1, 0]) tt_c = ax.contour(xx, yy, travel_time, 20, cmap='plasma', linewidths=1.5) ax.clabel(tt_c, inline=1, fontsize=9, fmt='%d') - ax.set_title('minimal path and travel time contours') + ax.set_title('travel time contours and minimal path') ax.axis('off') cb = fig.colorbar(tt_c) cb.ax.set_ylabel('travel time') From 2289d235fd02901a6f39a1ff6e87747901a9860c Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Sun, 24 May 2020 00:45:19 +0300 Subject: [PATCH 22/33] try to fix tests --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index cdebbf7..8a56d1e 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ whitelist_externals = poetry skip_install = true commands = - poetry install --no-dev --extras tests + poetry install poetry run pytest tests/ --color=yes {posargs} -v [testenv:py37-pytest-coverage] @@ -21,7 +21,7 @@ skip_install = true passenv = TRAVIS TRAVIS_* commands = - poetry install --no-dev --extras tests + poetry install poetry run pytest tests/ --cov=skmpe --color=yes {posargs} -v coveralls From a9dd0435dc3fa481d6354a8c7f8a1e12955c18c1 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Sun, 24 May 2020 01:43:30 +0300 Subject: [PATCH 23/33] remove unused import --- skmpe/_mpe.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skmpe/_mpe.py b/skmpe/_mpe.py index 09caa36..13032ba 100644 --- a/skmpe/_mpe.py +++ b/skmpe/_mpe.py @@ -11,7 +11,7 @@ from scipy.spatial.distance import euclidean from ._base import mpe_module, PointType, InitialInfo, PathInfo, PathInfoResult, logger -from ._parameters import Parameters, OdeSolverMethod, default_parameters +from ._parameters import Parameters, default_parameters from ._exceptions import ComputeTravelTimeError, PathExtractionError, EndPointNotReachedError From 10be662c2c0613f4992e12e78999954ed2b52053 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Tue, 26 May 2020 12:35:12 +0300 Subject: [PATCH 24/33] change __str__ formatting in Parameters class --- skmpe/_parameters.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/skmpe/_parameters.py b/skmpe/_parameters.py index 655e62f..9271038 100644 --- a/skmpe/_parameters.py +++ b/skmpe/_parameters.py @@ -130,6 +130,9 @@ def _check_travel_time_order(cls, v): # noqa ) return v + def __str__(self): + return self.__repr_str__('\n') + _default_parameters = Parameters() From 8c3bb1481a5be54c00f20929d6f3d976b324597e Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Tue, 26 May 2020 15:27:25 +0300 Subject: [PATCH 25/33] update docs tutorial --- docs/tutorial.rst | 155 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 154 insertions(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 7cd9b03..18a8aa1 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -4,6 +4,8 @@ Tutorial ******** +.. currentmodule:: skmpe + Overview ======== @@ -23,7 +25,7 @@ The package uses `the fast marching method `_ for extracting minimal paths. Algorithm -========= +--------- The algorithm contains two main steps: @@ -112,3 +114,154 @@ using **scikit-mpe** package and plot it. Also we can plot travel time contours. Advanced Usage ============== + +Initial Data +------------ + +The initial data is storing and validating in :class:`InitialInfo` class which inherited from +`Pydantic BaseModel `_. The class checks speed data and points dimensions, boundaries and values. + +Therefore, we cannot set an invalid data: + +.. code-block:: python + + import numpy as np + from skmpe import InitialInfo + + speed_data = np.zeros((100, 200)) + start_point = (10, 300) # out of bounds + end_point = (50, 60) + + init_data = InitialInfo( + speed_data=speed_data, + start_point=start_point, + end_point=end_point, + ) + +The code above is raising an exception:: + + Traceback (most recent call last): + ... + raise validation_error + pydantic.error_wrappers.ValidationError: 1 validation error for InitialInfo + start_point + 'start_point' (10, 300) coordinate 1 is out of 'speed_data' bounds [0, 200). (type=value_error) + + +We can use :class:`InitialInfo` explicity in :func:`mpe` function: + +.. code-block:: python + + from skmpe import InitialInfo, mpe + + init_data = InitialInfo(...) + result = mpe(init_data) + + +Also in most cases we can use the second :func:`mpe` function signature without using `InitialInfo` explicity: + +.. code-block:: python + + from skmpe import mpe + + ... + + result = mpe(speed_data, start_point, end_point) + + +Parameters +---------- + +The algorithm parameters are storing and validating in :class:`Parameters` class. +We can use this class directly, or we can use :func:`parameters` context manager for manage parameters. + +Also :func:`default_parameters` function returns the instance with default parameters: + +.. code-block:: python + + >>> from skmpe import default_parameters + >>> print(default_parameters()) + + travel_time_spacing=1.0 + travel_time_order= + travel_time_cache=False + ode_solver_method= + integrate_time_bound=10000.0 + integrate_min_step=0.0 + integrate_max_step=4.0 + dist_tol=0.001 + max_small_dist_steps=100 + + +Important Parameters +~~~~~~~~~~~~~~~~~~~~ + +The following parameters may be important in some cases: + + - **travel_time_order** -- the order of the fast-marching computation method. + 2 is more accurate, but it is slower. By default it is 1. Use :class:`TravelTimeOrder` enum for + this parameter + - **travel_time_cache** -- if we set way points we can use cached travel time. + For example if we set one way point we can compute travel time once for this way point as + source point. By default it is False. + - **ode_solver_method** -- we can use some ODE methods for extracting path. + Some methods may be work faster or more accurate on some speed data. + Use :class:`OdeSolverMethod` enum for this parameter. By default it is Runge-Kutta 4/5 (`RK45`) + - **integrate_time_bound** -- if we want to extract a long path we need to set a greater + value for time bound. By default it is 10000 + - **dist_tol** -- distance tolerance between steps for control path evolution. + By default it is 0.001 + - **max_small_dist_steps** -- the maximum number of small distance steps while path evolution. + Too small steps will be ignore N-times by this parameter. + +Using Parameters +~~~~~~~~~~~~~~~~ + +We can set the custom parameter values by :class:`Parameters` class or :func:`parameters` context manager. + +Using class: + +.. code-block:: python + + from skmpe import Parameters, mpe + + my_parameters = Parameters(travel_time_cache=True, travel_time_order=1) + result = mpe(..., parameters=my_parameters) + + +Using context manager: + +.. code-block:: python + + from skmpe import parameters, mpe + + with parameters(travel_time_cache=True, travel_time_order=1): + # the custom parameters will be used automatically + result = mpe(...) + + +Results +------- + +The whole extracted path results are storing in :class:`PathInfoResult` class (named tuple). +The instance of this class is returning from :func:`mpe` function. The pieces of the path +(in the case with way points) are storing in :class:`PathInfo` class. + +:class:`PathInfoResult` object contains: + + - **path** -- the whole extracted path in numpy array MxN where M is the number of points and N is dimension + - **pieces** -- the list of extracted path pieces between start/end or way points in `PathInfo` instances. + If we do not use way points, **pieces** list will be contain one piece. + +:class:`PathInfo` object contains: + + - **path** -- the extracted path piece in numpy array MxN where M is the number of points and N is dimension + - **start_point** -- the starting point + - **end_point** -- the ending point + - **travel_time** -- the computed travel time data for given speed data + - **extraction_result** -- the raw extraction result in :class:`PathExtractionResult` instance. + This data is returning from :class:`MinimalPathExtractor` class (low-level API). The data + contains additional info about extracted path and info about extracting process. + This data may be useful for debugging. + - **reversed** -- The flag indicates that the path piece is reversed. + This is relevant when using ``travel_time_cache == True`` parameter. From ee9fc949e1333a2c0255e57664ade90194e80465 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Wed, 27 May 2020 11:20:42 +0300 Subject: [PATCH 26/33] add examples to docs --- docs/examples.rst | 79 +++++++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 1 + docs/tutorial.rst | 2 ++ 3 files changed, 82 insertions(+) create mode 100644 docs/examples.rst diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 0000000..a9d73ac --- /dev/null +++ b/docs/examples.rst @@ -0,0 +1,79 @@ +.. _examples: + +******** +Examples +******** + +Retina Vessels +============== + +Extracting the minimal path through the retina vessels with additional way points. + +.. plot:: + + from skimage.data import retina + from skimage.color import rgb2gray + from skimage.transform import rescale + from skimage.filters import sato + + from skmpe import mpe + + image = rescale(rgb2gray(retina()), 0.5) + speed_image = sato(image) + + start_point = (76, 388) + end_point = (611, 442) + way_points = [(330, 98), (554, 203)] + + path_info = mpe(speed_image, start_point, end_point, way_points) + + px, py = path_info.path[:, 1], path_info.path[:, 0] + + plt.imshow(image, cmap='gray') + plt.plot(px, py, '-r') + plt.plot(*start_point[::-1], 'oy') + plt.plot(*end_point[::-1], 'og') + for p in way_points: + plt.plot(*p[::-1], 'ob') + plt.axis('off') + + +Bricks +====== + +Extracting the shortest paths through "briks" image. + +.. plot:: + + from matplotlib import pyplot as plt + + from skimage.data import brick + from skimage.transform import rescale + from skimage.exposure import rescale_intensity, adjust_sigmoid + + from skmpe import parameters, mpe + + image = rescale(brick(), 0.5) + speed_image = rescale_intensity( + adjust_sigmoid(image, cutoff=0.5, gain=10).astype(np.float_), out_range=(0., 1.)) + + start_point = (44, 13) + end_point = (233, 230) + way_points = [(211, 59), (17, 164)] + + with parameters(integrate_max_step=1.0): + path_info1 = mpe(speed_image, start_point, end_point) + path_info2 = mpe(speed_image, start_point, end_point, way_points) + + px1, py1 = path_info1.path[:, 1], path_info1.path[:, 0] + px2, py2 = path_info2.path[:, 1], path_info2.path[:, 0] + + plt.imshow(image, cmap='gray') + plt.plot(px1, py1, '-r', linewidth=2) + plt.plot(px2, py2, '--r', linewidth=2) + + plt.plot(*start_point[::-1], 'oy') + plt.plot(*end_point[::-1], 'og') + for p in way_points: + plt.plot(*p[::-1], 'ob') + plt.axis('off') diff --git a/docs/index.rst b/docs/index.rst index f586b88..0322078 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -45,6 +45,7 @@ Contents :maxdepth: 2 tutorial + examples api changelog diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 18a8aa1..3e4a87e 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -209,6 +209,8 @@ The following parameters may be important in some cases: Use :class:`OdeSolverMethod` enum for this parameter. By default it is Runge-Kutta 4/5 (`RK45`) - **integrate_time_bound** -- if we want to extract a long path we need to set a greater value for time bound. By default it is 10000 + - **integrate_min_step**, **integrate_max_step** -- these options can be used to control of ODE solver steps. + For example, lower value of ``integrate_max_step`` leads to lower the performance, but higher the accuracy. - **dist_tol** -- distance tolerance between steps for control path evolution. By default it is 0.001 - **max_small_dist_steps** -- the maximum number of small distance steps while path evolution. From feab1edfe6d1c84d7b2fb37c54cabd3620b80b32 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Wed, 27 May 2020 11:21:16 +0300 Subject: [PATCH 27/33] fix typo --- docs/examples.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/examples.rst b/docs/examples.rst index a9d73ac..33f5562 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -41,7 +41,7 @@ Extracting the minimal path through the retina vessels with additional way point Bricks ====== -Extracting the shortest paths through "briks" image. +Extracting the shortest paths through "bricks" image. .. plot:: From b33a18c5c3c55348b6e4316d944669ef1af25e7f Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Wed, 27 May 2020 11:22:07 +0300 Subject: [PATCH 28/33] remove extra code for example --- docs/examples.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/examples.rst b/docs/examples.rst index 33f5562..9f692ab 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -45,8 +45,6 @@ Extracting the shortest paths through "bricks" image. .. plot:: - from matplotlib import pyplot as plt - from skimage.data import brick from skimage.transform import rescale from skimage.exposure import rescale_intensity, adjust_sigmoid From 49dafc5611f730627d2941275dbe9e39e60367f6 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Wed, 27 May 2020 16:51:18 +0300 Subject: [PATCH 29/33] update readme --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 1b0c03b..dd6f554 100644 --- a/README.md +++ b/README.md @@ -61,8 +61,6 @@ plt.show() The full documentation can be found at [scikit-mpe.readthedocs.io](https://scikit-mpe.readthedocs.io/en/latest) -(The documentation is being written) - ## References - [Fast Marching Methods: A boundary value formulation](https://math.berkeley.edu/~sethian/2006/Explanations/fast_marching_explain.html) From 51df1faa54900757b8299e1beba01378e313e2a0 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Wed, 27 May 2020 17:31:37 +0300 Subject: [PATCH 30/33] bump version to 0.2.0 --- CHANGELOG.md | 5 ++ poetry.lock | 114 ++++++++++++++++++++++------------------------ pyproject.toml | 3 +- skmpe/__init__.py | 8 +++- 4 files changed, 68 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef12ef1..547ac44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v0.2.0 (27.05.2020) + +- Refactoring the package with changing some low-level API +- Add documentation + ## v0.1.1 - Fix links diff --git a/poetry.lock b/poetry.lock index ef50649..4f2ef7b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,5 @@ [[package]] -category = "main" +category = "dev" description = "A configurable sidebar-enabled Sphinx theme" name = "alabaster" optional = false @@ -7,7 +7,7 @@ python-versions = "*" version = "0.7.12" [[package]] -category = "main" +category = "dev" description = "Atomic file writes." marker = "sys_platform == \"win32\"" name = "atomicwrites" @@ -16,7 +16,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "1.3.0" [[package]] -category = "main" +category = "dev" description = "Classes Without Boilerplate" name = "attrs" optional = false @@ -30,7 +30,7 @@ docs = ["sphinx", "zope.interface"] tests = ["coverage", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "zope.interface"] [[package]] -category = "main" +category = "dev" description = "Internationalization utilities" name = "babel" optional = false @@ -41,7 +41,7 @@ version = "2.8.0" pytz = ">=2015.7" [[package]] -category = "main" +category = "dev" description = "Python package for providing Mozilla's CA Bundle." name = "certifi" optional = false @@ -49,7 +49,7 @@ python-versions = "*" version = "2019.11.28" [[package]] -category = "main" +category = "dev" description = "Universal encoding detector for Python 2 and 3" name = "chardet" optional = false @@ -57,7 +57,7 @@ python-versions = "*" version = "3.0.4" [[package]] -category = "main" +category = "dev" description = "Cross-platform colored terminal text." marker = "sys_platform == \"win32\"" name = "colorama" @@ -66,7 +66,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" version = "0.4.3" [[package]] -category = "main" +category = "dev" description = "Code coverage measurement for Python" name = "coverage" optional = false @@ -77,7 +77,7 @@ version = "5.0.3" toml = ["toml"] [[package]] -category = "main" +category = "dev" description = "Show coverage stats online via coveralls.io" name = "coveralls" optional = false @@ -93,7 +93,7 @@ requests = ">=1.0.0" yaml = ["PyYAML (>=3.10)"] [[package]] -category = "main" +category = "dev" description = "Composable style cycles" name = "cycler" optional = false @@ -113,7 +113,7 @@ python-versions = "*" version = "0.6" [[package]] -category = "main" +category = "dev" description = "Decorators for Humans" name = "decorator" optional = false @@ -121,7 +121,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*" version = "4.4.1" [[package]] -category = "main" +category = "dev" description = "Pythonic argument parser, that will make you smile" name = "docopt" optional = false @@ -129,7 +129,7 @@ python-versions = "*" version = "0.6.2" [[package]] -category = "main" +category = "dev" description = "Docutils -- Python Documentation Utilities" name = "docutils" optional = false @@ -170,7 +170,7 @@ version = "0.1.6" flake8 = ">3.0.0" [[package]] -category = "main" +category = "dev" description = "Internationalized Domain Names in Applications (IDNA)" name = "idna" optional = false @@ -178,7 +178,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "2.8" [[package]] -category = "main" +category = "dev" description = "Library for reading and writing a wide range of image, video, scientific, and volumetric data formats." name = "imageio" optional = false @@ -197,7 +197,7 @@ gdal = ["gdal"] itk = ["itk"] [[package]] -category = "main" +category = "dev" description = "Getting image size from png/jpeg/jpeg2000/gif file" name = "imagesize" optional = false @@ -207,11 +207,10 @@ version = "1.2.0" [[package]] category = "main" description = "Read metadata from Python packages" -marker = "python_version < \"3.8\"" name = "importlib-metadata" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.5.0" +version = "1.6.0" [package.dependencies] zipp = ">=0.5" @@ -221,7 +220,7 @@ docs = ["sphinx", "rst.linker"] testing = ["packaging", "importlib-resources"] [[package]] -category = "main" +category = "dev" description = "A very fast and expressive template engine." name = "jinja2" optional = false @@ -235,7 +234,7 @@ MarkupSafe = ">=0.23" i18n = ["Babel (>=0.8)"] [[package]] -category = "main" +category = "dev" description = "A fast implementation of the Cassowary constraint solver" name = "kiwisolver" optional = false @@ -246,7 +245,7 @@ version = "1.1.0" setuptools = "*" [[package]] -category = "main" +category = "dev" description = "Markdown and reStructuredText in a single file." name = "m2r" optional = false @@ -258,7 +257,7 @@ docutils = "*" mistune = "*" [[package]] -category = "main" +category = "dev" description = "Safely add untrusted strings to HTML/XML markup." name = "markupsafe" optional = false @@ -266,7 +265,7 @@ python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" version = "1.1.1" [[package]] -category = "main" +category = "dev" description = "Python plotting package" name = "matplotlib" optional = false @@ -289,7 +288,7 @@ python-versions = "*" version = "0.6.1" [[package]] -category = "main" +category = "dev" description = "The fastest markdown parser in pure Python" name = "mistune" optional = false @@ -297,7 +296,7 @@ python-versions = "*" version = "0.8.4" [[package]] -category = "main" +category = "dev" description = "More routines for operating on iterables, beyond itertools" name = "more-itertools" optional = false @@ -305,7 +304,7 @@ python-versions = ">=3.5" version = "8.2.0" [[package]] -category = "main" +category = "dev" description = "Python package for creating and manipulating graphs and networks" name = "networkx" optional = false @@ -337,7 +336,7 @@ python-versions = ">=3.5" version = "1.18.1" [[package]] -category = "main" +category = "dev" description = "Sphinx extension to support docstrings in Numpy format" name = "numpydoc" optional = false @@ -349,7 +348,7 @@ Jinja2 = ">=2.3" sphinx = ">=1.6.5" [[package]] -category = "main" +category = "dev" description = "Core utilities for Python packages" name = "packaging" optional = false @@ -361,7 +360,7 @@ pyparsing = ">=2.0.2" six = "*" [[package]] -category = "main" +category = "dev" description = "Python Imaging Library (Fork)" name = "pillow" optional = false @@ -369,7 +368,7 @@ python-versions = ">=3.5" version = "7.0.0" [[package]] -category = "main" +category = "dev" description = "plugin and hook calling mechanisms for python" name = "pluggy" optional = false @@ -385,7 +384,7 @@ version = ">=0.12" dev = ["pre-commit", "tox"] [[package]] -category = "main" +category = "dev" description = "library with cross-python path, ini-parsing, io, code, log facilities" name = "py" optional = false @@ -435,7 +434,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "2.1.1" [[package]] -category = "main" +category = "dev" description = "Pygments is a syntax highlighting package written in Python." name = "pygments" optional = false @@ -443,7 +442,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" version = "2.5.2" [[package]] -category = "main" +category = "dev" description = "Python parsing module" name = "pyparsing" optional = false @@ -451,7 +450,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" version = "2.4.6" [[package]] -category = "main" +category = "dev" description = "pytest: simple powerful testing with Python" name = "pytest" optional = false @@ -494,7 +493,7 @@ elasticsearch = ["elasticsearch"] histogram = ["pygal", "pygaljs"] [[package]] -category = "main" +category = "dev" description = "Pytest plugin for measuring coverage." name = "pytest-cov" optional = false @@ -509,7 +508,7 @@ pytest = ">=3.6" testing = ["fields", "hunter", "process-tests (2.0.2)", "six", "virtualenv"] [[package]] -category = "main" +category = "dev" description = "Extensions to the standard Python datetime module" name = "python-dateutil" optional = false @@ -520,7 +519,7 @@ version = "2.8.1" six = ">=1.5" [[package]] -category = "main" +category = "dev" description = "World timezone definitions, modern and historical" name = "pytz" optional = false @@ -528,7 +527,7 @@ python-versions = "*" version = "2019.3" [[package]] -category = "main" +category = "dev" description = "PyWavelets, wavelet transform module" name = "pywavelets" optional = false @@ -539,7 +538,7 @@ version = "1.1.1" numpy = ">=1.13.3" [[package]] -category = "main" +category = "dev" description = "Python HTTP for Humans." name = "requests" optional = false @@ -568,7 +567,7 @@ version = "2019.1.30" numpy = ">=1.0.2" [[package]] -category = "main" +category = "dev" description = "Image processing routines for SciPy" name = "scikit-image" optional = false @@ -600,7 +599,7 @@ version = "1.4.1" numpy = ">=1.13.3" [[package]] -category = "main" +category = "dev" description = "Python 2 and 3 compatibility utilities" name = "six" optional = false @@ -608,7 +607,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" version = "1.14.0" [[package]] -category = "main" +category = "dev" description = "This package provides 26 stemmers for 25 languages generated from Snowball algorithms." name = "snowballstemmer" optional = false @@ -616,7 +615,7 @@ python-versions = "*" version = "2.0.0" [[package]] -category = "main" +category = "dev" description = "Python documentation generator" name = "sphinx" optional = false @@ -647,7 +646,7 @@ docs = ["sphinxcontrib-websupport"] test = ["pytest", "pytest-cov", "html5lib", "flake8 (>=3.5.0)", "flake8-import-order", "mypy (>=0.761)", "docutils-stubs"] [[package]] -category = "main" +category = "dev" description = "" name = "sphinxcontrib-applehelp" optional = false @@ -658,7 +657,7 @@ version = "1.0.1" test = ["pytest", "flake8", "mypy"] [[package]] -category = "main" +category = "dev" description = "" name = "sphinxcontrib-devhelp" optional = false @@ -669,7 +668,7 @@ version = "1.0.1" test = ["pytest", "flake8", "mypy"] [[package]] -category = "main" +category = "dev" description = "" name = "sphinxcontrib-htmlhelp" optional = false @@ -680,7 +679,7 @@ version = "1.0.2" test = ["pytest", "flake8", "mypy", "html5lib"] [[package]] -category = "main" +category = "dev" description = "A sphinx extension which renders display math in HTML via JavaScript" name = "sphinxcontrib-jsmath" optional = false @@ -691,7 +690,7 @@ version = "1.0.1" test = ["pytest", "flake8", "mypy"] [[package]] -category = "main" +category = "dev" description = "" name = "sphinxcontrib-qthelp" optional = false @@ -702,7 +701,7 @@ version = "1.0.2" test = ["pytest", "flake8", "mypy"] [[package]] -category = "main" +category = "dev" description = "" name = "sphinxcontrib-serializinghtml" optional = false @@ -713,7 +712,7 @@ version = "1.1.3" test = ["pytest", "flake8", "mypy"] [[package]] -category = "main" +category = "dev" description = "Python Library for Tom's Obvious, Minimal Language" name = "toml" optional = false @@ -721,7 +720,7 @@ python-versions = "*" version = "0.10.0" [[package]] -category = "main" +category = "dev" description = "HTTP library with thread-safe connection pooling, file post, and more." name = "urllib3" optional = false @@ -734,7 +733,7 @@ secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "cer socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7,<2.0)"] [[package]] -category = "main" +category = "dev" description = "Measures number of Terminal column cells of wide-character codes" name = "wcwidth" optional = false @@ -744,7 +743,6 @@ version = "0.1.8" [[package]] category = "main" description = "Backport of pathlib-compatible object wrapper for zip files" -marker = "python_version < \"3.8\"" name = "zipp" optional = false python-versions = ">=3.6" @@ -754,12 +752,8 @@ version = "2.1.0" docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["jaraco.itertools"] -[extras] -docs = ["sphinx", "numpydoc", "m2r", "matplotlib", "toml"] -tests = ["pytest", "pytest-cov", "coveralls", "scikit-image"] - [metadata] -content-hash = "727b425dfdaf26d0c296ec88197302c6ac6d8c6ceae7ff3e10bcbe8057057940" +content-hash = "1e783913dab16e2b18765fa9af2b7cbd6686aed5acf72283cbd8eef21bc0ecee" python-versions = "^3.6" [metadata.files] @@ -871,8 +865,8 @@ imagesize = [ {file = "imagesize-1.2.0.tar.gz", hash = "sha256:b1f6b5a4eab1f73479a50fb79fcf729514a900c341d8503d62a62dbc4127a2b1"}, ] importlib-metadata = [ - {file = "importlib_metadata-1.5.0-py2.py3-none-any.whl", hash = "sha256:b97607a1a18a5100839aec1dc26a1ea17ee0d93b20b0f008d80a5a050afb200b"}, - {file = "importlib_metadata-1.5.0.tar.gz", hash = "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302"}, + {file = "importlib_metadata-1.6.0-py2.py3-none-any.whl", hash = "sha256:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f"}, + {file = "importlib_metadata-1.6.0.tar.gz", hash = "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"}, ] jinja2 = [ {file = "Jinja2-2.11.1-py2.py3-none-any.whl", hash = "sha256:b0eaf100007721b5c16c1fc1eecb87409464edc10469ddc9a22a27a99123be49"}, diff --git a/pyproject.toml b/pyproject.toml index cf34e69..51f6270 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "scikit-mpe" -version = "0.1.1" +version = "0.2.0" description = "Minimal path extraction using the fast marching method" authors = ["Eugene Prilepin "] license = "MIT" @@ -38,6 +38,7 @@ numpy = "^1.18.1" scipy = "^1.4.1" scikit-fmm = "^2019.1.30" pydantic = "^1.4" +importlib-metadata = "^1.6.0" [tool.poetry.dev-dependencies] flake8 = "^3.7.9" diff --git a/skmpe/__init__.py b/skmpe/__init__.py index ed8ce12..87ef42d 100644 --- a/skmpe/__init__.py +++ b/skmpe/__init__.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +from importlib_metadata import metadata, PackageNotFoundError + from ._base import ( InitialInfo, PathInfo, @@ -29,7 +31,11 @@ from skmpe._api import mpe -__version__ = '0.1.1' +try: + __version__ = metadata('scikit-mpe')['version'] +except PackageNotFoundError: + __version__ = '0.0.0.dev' + __all__ = [ 'InitialInfo', From 1699a4424e7dce3abf153fe4971ef976f860cc7b Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Wed, 27 May 2020 17:48:13 +0300 Subject: [PATCH 31/33] add build docs on CI --- .travis.yml | 3 ++- tox.ini | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0a1ce4f..acf5fe1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,8 @@ matrix: env: TOXENV=py37-pytest-coverage - python: "3.8" env: TOXENV=py38-pytest - + - python: "3.7" + env: TOXENV=docs - python: "3.7" env: TOXENV=flake8 diff --git a/tox.ini b/tox.ini index 8a56d1e..05f4520 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py{36,37,38}-pytest-coverage, flake8 +envlist = py{36,37,38}-pytest-coverage, docs, flake8 isolated_build = True [tox:.package] @@ -25,6 +25,12 @@ commands = poetry run pytest tests/ --cov=skmpe --color=yes {posargs} -v coveralls +[testenv:docs] +whitelist_externals = make +commands = + poetry install + make -C docs/ html + [testenv:flake8] deps = flake8 From 03702af7b9b3312e92cca37da3f9e3349e63c9d0 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Wed, 27 May 2020 18:18:24 +0300 Subject: [PATCH 32/33] add additional asserts --- tests/test_mpe.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_mpe.py b/tests/test_mpe.py index 905d4ca..ee5b842 100644 --- a/tests/test_mpe.py +++ b/tests/test_mpe.py @@ -31,6 +31,8 @@ def test_extract_path_without_waypoints(retina_speed_image, travel_time_order, o with parameters(ode_solver_method=ode_method, travel_time_order=travel_time_order): path_info = mpe(retina_speed_image, start_point, end_point) + assert path_info.point_count > 0 + path_piece_info = path_info.pieces[0] start_travel_time = path_piece_info.travel_time[start_point[0], start_point[1]] end_travel_time = path_piece_info.travel_time[end_point[0], end_point[1]] @@ -52,6 +54,8 @@ def test_extract_path_with_waypoints(retina_speed_image, travel_time_order, with parameters(travel_time_order=travel_time_order, travel_time_cache=ttime_cache): path_info = mpe(retina_speed_image, start_point, end_point, way_points) + assert path_info.point_count > 0 + for path_piece_info in path_info.pieces: start_pt = path_piece_info.start_point end_pt = path_piece_info.end_point From 6604fd17855f228439422681df8d5737bbbfb2dc Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Wed, 27 May 2020 18:28:32 +0300 Subject: [PATCH 33/33] add no covers for not important code --- skmpe/__init__.py | 2 +- skmpe/_base.py | 2 +- skmpe/_exceptions.py | 12 ++++++------ skmpe/_mpe.py | 2 +- skmpe/_parameters.py | 4 ++-- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/skmpe/__init__.py b/skmpe/__init__.py index 87ef42d..fae45be 100644 --- a/skmpe/__init__.py +++ b/skmpe/__init__.py @@ -33,7 +33,7 @@ try: __version__ = metadata('scikit-mpe')['version'] -except PackageNotFoundError: +except PackageNotFoundError: # pragma: no cover __version__ = '0.0.0.dev' diff --git a/skmpe/_base.py b/skmpe/_base.py index 997cb2a..c186d66 100644 --- a/skmpe/_base.py +++ b/skmpe/_base.py @@ -9,7 +9,7 @@ from pydantic import BaseModel, Extra, validator, root_validator import numpy as np -if TYPE_CHECKING: +if TYPE_CHECKING: # pragma: no cover from ._parameters import Parameters # noqa from ._mpe import PathExtractionResult # noqa diff --git a/skmpe/_exceptions.py b/skmpe/_exceptions.py index a617011..cd89934 100644 --- a/skmpe/_exceptions.py +++ b/skmpe/_exceptions.py @@ -32,17 +32,17 @@ def __init__(self, *args, self._end_point = end_point @property - def travel_time(self) -> np.ndarray: + def travel_time(self) -> np.ndarray: # pragma: no cover """Computed travel time data""" return self._travel_time @property - def start_point(self) -> PointType: + def start_point(self) -> PointType: # pragma: no cover """Starting point""" return self._start_point @property - def end_point(self) -> PointType: + def end_point(self) -> PointType: # pragma: no cover """Ending point""" return self._end_point @@ -65,16 +65,16 @@ def __init__(self, *args, self._reason = reason @property - def extracted_points(self) -> List[FloatPointType]: + def extracted_points(self) -> List[FloatPointType]: # pragma: no cover """The list of extracted path points""" return self._extracted_points @property - def last_distance(self) -> float: + def last_distance(self) -> float: # pragma: no cover """The last distance to the ending point from the last path point""" return self._last_distance @property - def reason(self) -> str: + def reason(self) -> str: # pragma: no cover """The reason of extracting path termination""" return self._reason diff --git a/skmpe/_mpe.py b/skmpe/_mpe.py index 13032ba..608cfb6 100644 --- a/skmpe/_mpe.py +++ b/skmpe/_mpe.py @@ -131,7 +131,7 @@ def travel_time(self) -> np.ndarray: return self._travel_time @property - def phi(self) -> np.ndarray: + def phi(self) -> np.ndarray: # pragma: no cover """Returns the computed phi (zero contour) for given source point """ return self._phi diff --git a/skmpe/_parameters.py b/skmpe/_parameters.py index 9271038..aa6b7ea 100644 --- a/skmpe/_parameters.py +++ b/skmpe/_parameters.py @@ -123,14 +123,14 @@ class Parameters(ImmutableDataObject): @validator('travel_time_order') def _check_travel_time_order(cls, v): # noqa - if v == TravelTimeOrder.second: + if v == TravelTimeOrder.second: # pragma: no cover raise ValueError( 'Currently the second order for computing travel time does not work properly.' '\nSee the following issue for details: https://github.com/scikit-fmm/scikit-fmm/issues/28' ) return v - def __str__(self): + def __str__(self): # pragma: no cover return self.__repr_str__('\n')