From 69ae9b63a0672332244258c40586e56c48e3d15b Mon Sep 17 00:00:00 2001 From: Vasudha Jha Date: Fri, 3 Feb 2023 00:17:35 -0800 Subject: [PATCH] use transonic to optimize cost and heuristic functions for A* --- .gitignore | 1 + brightest_path_lib/algorithm/astar.py | 45 +++--- brightest_path_lib/algorithm/nbastar.py | 15 +- brightest_path_lib/cost/__init__.py | 3 +- brightest_path_lib/cost/reciprocal.py | 7 +- .../cost/reciprocal_transonic.py | 86 ++++++++++++ brightest_path_lib/heuristic/__init__.py | 1 + brightest_path_lib/heuristic/euclidean.py | 7 +- .../heuristic/euclidean_transonic.py | 100 +++++++++++++ brightest_path_lib/image/stats.py | 4 +- brightest_path_lib/input/inputs.py | 3 +- brightest_path_lib/pythran_test.ipynb | 132 ++++++++++++++++++ brightest_path_lib/sandbox.py | 30 ++-- brightest_path_lib/sandbox2.py | 19 +-- pyproject.toml | 2 +- setup.py | 52 ++++++- tests/test_reciprocal_cost_func.py | 2 +- 17 files changed, 441 insertions(+), 68 deletions(-) create mode 100644 brightest_path_lib/cost/reciprocal_transonic.py create mode 100644 brightest_path_lib/heuristic/euclidean_transonic.py create mode 100644 brightest_path_lib/pythran_test.ipynb diff --git a/.gitignore b/.gitignore index e8182ec..31c7665 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ venv dist build rr30a_s0_ch2.tif +__pythran__/ diff --git a/brightest_path_lib/algorithm/astar.py b/brightest_path_lib/algorithm/astar.py index b0ce9d5..4337fe7 100644 --- a/brightest_path_lib/algorithm/astar.py +++ b/brightest_path_lib/algorithm/astar.py @@ -3,8 +3,8 @@ import numpy as np from queue import PriorityQueue, Queue from typing import List, Tuple -from brightest_path_lib.cost import Reciprocal -from brightest_path_lib.heuristic import Euclidean +from brightest_path_lib.cost import ReciprocalTransonic +from brightest_path_lib.heuristic import EuclideanTransonic from brightest_path_lib.image import ImageStats from brightest_path_lib.input import CostFunction, HeuristicFunction from brightest_path_lib.node import Node @@ -91,15 +91,16 @@ def __init__( self.open_nodes = open_nodes if cost_function == CostFunction.RECIPROCAL: - self.cost_function = Reciprocal( + self.cost_function = ReciprocalTransonic( min_intensity=self.image_stats.min_intensity, max_intensity=self.image_stats.max_intensity) if heuristic_function == HeuristicFunction.EUCLIDEAN: - self.heuristic_function = Euclidean(scale=self.scale) + self.heuristic_function = EuclideanTransonic(scale=self.scale) self.is_canceled = False self.found_path = False + self.evaluated_nodes = 0 self.result = [] def _validate_inputs( @@ -196,6 +197,7 @@ def search(self) -> List[np.ndarray]: close_set_hash.add(current_coordinates) + self.evaluated_nodes = count return self.result def _default_value(self) -> float: @@ -252,28 +254,25 @@ def _find_2D_neighbors_of(self, node: Node) -> List[Node]: neighbors = [] steps = [-1, 0, 1] for xdiff in steps: + new_x = node.point[1] + xdiff + if new_x < self.image_stats.x_min or new_x > self.image_stats.x_max: + continue + for ydiff in steps: if xdiff == ydiff == 0: continue - new_x = node.point[1] + xdiff - # new_x = node.point[0] + xdiff - if new_x < self.image_stats.x_min or new_x > self.image_stats.x_max: - continue - new_y = node.point[0] + ydiff - # new_y = node.point[1] + ydiff if new_y < self.image_stats.y_min or new_y > self.image_stats.y_max: continue - # new_point = np.array([new_x, new_y]) new_point = np.array([new_y, new_x]) h_for_new_point = self._estimate_cost_to_goal(new_point) intensity_at_new_point = self.image[new_y, new_x] - cost_of_moving_to_new_point = self.cost_function.cost_of_moving_to(intensity_at_new_point) + cost_of_moving_to_new_point = self.cost_function.cost_of_moving_to(float(intensity_at_new_point)) if cost_of_moving_to_new_point < self.cost_function.minimum_step_cost(): cost_of_moving_to_new_point = self.cost_function.minimum_step_cost() @@ -317,7 +316,15 @@ def _find_3D_neighbors_of(self, node: Node) -> List[Node]: steps = [-1, 0, 1] for xdiff in steps: + new_x = node.point[2] + xdiff + if new_x < self.image_stats.x_min or new_x > self.image_stats.x_max: + continue + for ydiff in steps: + new_y = node.point[1] + ydiff + if new_y < self.image_stats.y_min or new_y > self.image_stats.y_max: + continue + for zdiff in steps: if xdiff == ydiff == zdiff == 0: continue @@ -326,24 +333,12 @@ def _find_3D_neighbors_of(self, node: Node) -> List[Node]: if new_z < self.image_stats.z_min or new_z > self.image_stats.z_max: continue - # new_y = node.point[2] + ydiff - new_y = node.point[1] + ydiff - if new_y < self.image_stats.y_min or new_y > self.image_stats.y_max: - continue - - # new_x = node.point[1] + xdiff - new_x = node.point[2] + xdiff - if new_x < self.image_stats.x_min or new_x > self.image_stats.x_max: - continue - - #new_point = np.array([new_z, new_x, new_y]) new_point = np.array([new_z, new_y, new_x]) h_for_new_point = self._estimate_cost_to_goal(new_point) - #intensity_at_new_point = self.image[new_z, new_x, new_y] intensity_at_new_point = self.image[new_z, new_y, new_x] - cost_of_moving_to_new_point = self.cost_function.cost_of_moving_to(intensity_at_new_point) + cost_of_moving_to_new_point = self.cost_function.cost_of_moving_to(float(intensity_at_new_point)) if cost_of_moving_to_new_point < self.cost_function.minimum_step_cost(): cost_of_moving_to_new_point = self.cost_function.minimum_step_cost() diff --git a/brightest_path_lib/algorithm/nbastar.py b/brightest_path_lib/algorithm/nbastar.py index 4642efa..72a450c 100644 --- a/brightest_path_lib/algorithm/nbastar.py +++ b/brightest_path_lib/algorithm/nbastar.py @@ -108,6 +108,7 @@ def __init__( self.touch_node: BidirectionalNode = None self.is_canceled = False self.found_path = False + self.evaluated_nodes = 2 # since we will add the start and goal node to the open queue self.result = [] def _validate_inputs( @@ -264,6 +265,8 @@ def _expand_2D_neighbors_of(self, node: BidirectionalNode, from_start: bool): in these directions - 2D coordinates are of the type (y, x) """ + current_g_score = node.get_g(from_start) # optimization: will be the same for all neighbors + steps = [-1, 0, 1] for xdiff in steps: new_x = node.point[1] + xdiff @@ -280,10 +283,10 @@ def _expand_2D_neighbors_of(self, node: BidirectionalNode, from_start: bool): new_point = np.array([new_y, new_x]) - current_g_score = node.get_g(from_start) + # current_g_score = node.get_g(from_start) intensity_at_new_point = self.image[new_y, new_x] - cost_of_moving_to_new_point = self.cost_function.cost_of_moving_to(intensity_at_new_point) + cost_of_moving_to_new_point = self.cost_function.cost_of_moving_to(float(intensity_at_new_point)) if cost_of_moving_to_new_point < self.cost_function.minimum_step_cost(): cost_of_moving_to_new_point = self.cost_function.minimum_step_cost() @@ -316,6 +319,8 @@ def _expand_3D_neighbors_of(self, node: BidirectionalNode, from_start: bool): or on the edges of the image) - 3D coordinates are of the form (z, x, y) """ + current_g_score = node.get_g(from_start) + steps = [-1, 0, 1] for xdiff in steps: @@ -338,10 +343,10 @@ def _expand_3D_neighbors_of(self, node: BidirectionalNode, from_start: bool): new_point = np.array([new_z, new_y, new_x]) - current_g_score = node.get_g(from_start) + # current_g_score = node.get_g(from_start) intensity_at_new_point = self.image[new_z, new_y, new_x] - cost_of_moving_to_new_point = self.cost_function.cost_of_moving_to(intensity_at_new_point) + cost_of_moving_to_new_point = self.cost_function.cost_of_moving_to(float(intensity_at_new_point)) if cost_of_moving_to_new_point < self.cost_function.minimum_step_cost(): cost_of_moving_to_new_point = self.cost_function.minimum_step_cost() @@ -372,6 +377,7 @@ def is_touch_node( open_queue.put((tentative_f_score, self._get_node_priority(from_start), new_node)) if self.open_nodes: self.open_nodes.put(new_point_coordinates) + self.evaluated_nodes += 1 self.node_at_coordinates[new_point_coordinates] = new_node # elif self._in_closed_set(new_point_coordinates, from_start): # return @@ -383,6 +389,7 @@ def is_touch_node( open_queue.put((tentative_f_score, self._get_node_priority(from_start), already_there)) if self.open_nodes: self.open_nodes.put(new_point_coordinates) + self.evaluated_nodes += 1 path_length = already_there.g_score_from_start + already_there.g_score_from_goal if path_length < self.best_path_length: self.best_path_length = path_length diff --git a/brightest_path_lib/cost/__init__.py b/brightest_path_lib/cost/__init__.py index 66a0706..cc514fb 100644 --- a/brightest_path_lib/cost/__init__.py +++ b/brightest_path_lib/cost/__init__.py @@ -1,2 +1,3 @@ from .cost import Cost -from .reciprocal import Reciprocal \ No newline at end of file +from .reciprocal import Reciprocal +from .reciprocal_transonic import ReciprocalTransonic diff --git a/brightest_path_lib/cost/reciprocal.py b/brightest_path_lib/cost/reciprocal.py index 1c6d39f..ca4ccf6 100644 --- a/brightest_path_lib/cost/reciprocal.py +++ b/brightest_path_lib/cost/reciprocal.py @@ -21,7 +21,6 @@ class Reciprocal(Cost): is between RECIPROCAL MIN and RECIPROCAL_MAX """ - def __init__(self, min_intensity: float, max_intensity: float) -> None: super().__init__() if min_intensity is None or max_intensity is None: @@ -30,10 +29,10 @@ def __init__(self, min_intensity: float, max_intensity: float) -> None: raise ValueError self.min_intensity = min_intensity self.max_intensity = max_intensity - self.RECIPROCAL_MIN = 1E-6 + self.RECIPROCAL_MIN = float(1E-6) self.RECIPROCAL_MAX = 255.0 - self._min_step_cost = 1 / self.RECIPROCAL_MAX - + self._min_step_cost = 1.0 / self.RECIPROCAL_MAX + def cost_of_moving_to(self, intensity_at_new_point: float) -> float: """calculates the cost of moving to a point diff --git a/brightest_path_lib/cost/reciprocal_transonic.py b/brightest_path_lib/cost/reciprocal_transonic.py new file mode 100644 index 0000000..30ab96d --- /dev/null +++ b/brightest_path_lib/cost/reciprocal_transonic.py @@ -0,0 +1,86 @@ +from transonic import boost + +from brightest_path_lib.cost import Cost + +@boost +class ReciprocalTransonic(Cost): + """Uses the reciprocal of pixel/voxel intensity to compute the cost of moving + to a neighboring point + + Parameters + ---------- + min_intensity : float + The minimum intensity a pixel/voxel can have in a given image + max_intensity : float + The maximum intensity a pixel/voxel can have in a given image + + Attributes + ---------- + RECIPROCAL_MIN : float + To cope with zero intensities, RECIPROCAL_MIN is added to the intensities + in the range before reciprocal calculation + RECIPROCAL_MAX : float + We set the maximum intensity <= RECIPROCAL_MAX so that the intensity + is between RECIPROCAL MIN and RECIPROCAL_MAX + + """ + + min_intensity: float + max_intensity: float + RECIPROCAL_MIN: float + RECIPROCAL_MAX: float + _min_step_cost: float + + def __init__(self, min_intensity: float, max_intensity: float) -> None: + super().__init__() + if min_intensity is None or max_intensity is None: + raise TypeError + if min_intensity > max_intensity: + raise ValueError + self.min_intensity = min_intensity + self.max_intensity = max_intensity + self.RECIPROCAL_MIN = float(1E-6) + self.RECIPROCAL_MAX = 255.0 + self._min_step_cost = 1.0 / self.RECIPROCAL_MAX + + + @boost + def cost_of_moving_to(self, intensity_at_new_point: float) -> float: + """calculates the cost of moving to a point + + Parameters + ---------- + intensity_at_new_point : float + The intensity of the new point under consideration + + Returns + ------- + float + the cost of moving to the new point + + Notes + ----- + - To cope with zero intensities, RECIPROCAL_MIN is added to the intensities in the range before reciprocal calculation + - We set the maximum intensity <= RECIPROCAL_MAX so that the intensity is between RECIPROCAL MIN and RECIPROCAL_MAX + + """ + if intensity_at_new_point > self.max_intensity: + raise ValueError + + intensity_at_new_point = self.RECIPROCAL_MAX * (intensity_at_new_point - self.min_intensity) / (self.max_intensity - self.min_intensity) + + if intensity_at_new_point < self.RECIPROCAL_MIN: + intensity_at_new_point = self.RECIPROCAL_MIN + + return 1.0 / intensity_at_new_point + + @boost + def minimum_step_cost(self) -> float: + """calculates the minimum step cost + + Returns + ------- + float + the minimum step cost + """ + return self._min_step_cost diff --git a/brightest_path_lib/heuristic/__init__.py b/brightest_path_lib/heuristic/__init__.py index 48c1086..ecafeab 100644 --- a/brightest_path_lib/heuristic/__init__.py +++ b/brightest_path_lib/heuristic/__init__.py @@ -1,2 +1,3 @@ from .heuristic import Heuristic from .euclidean import Euclidean +from .euclidean_transonic import EuclideanTransonic diff --git a/brightest_path_lib/heuristic/euclidean.py b/brightest_path_lib/heuristic/euclidean.py index a97fe2f..8015fa6 100644 --- a/brightest_path_lib/heuristic/euclidean.py +++ b/brightest_path_lib/heuristic/euclidean.py @@ -23,7 +23,6 @@ class Euclidean(Heuristic): the scale of the image's Z-axis """ - def __init__(self, scale: Tuple): if scale is None: raise TypeError @@ -68,14 +67,10 @@ def estimate_cost_to_goal(self, current_point: np.ndarray, goal_point: np.ndarra if (len(current_point) == 0 or len(goal_point) == 0) or (len(current_point) != len(goal_point)): raise ValueError - # current_x, current_y, current_z = current_point[0], current_point[1], 0 current_x, current_y, current_z = current_point[1], current_point[0], 0 - # goal_x, goal_y, goal_z = goal_point[0], goal_point[1], 0 goal_x, goal_y, goal_z = goal_point[1], goal_point[0], 0 if len(current_point) == len(goal_point) == 3: - # current_z, current_x, current_y = current_point[0], current_point[1], current_point[2] - # goal_z, goal_x, goal_y = goal_point[0], goal_point[1], goal_point[2] current_z, current_y, current_x = current_point[0], current_point[1], current_point[2] goal_z, goal_y, goal_x = goal_point[0], goal_point[1], goal_point[2] @@ -83,4 +78,4 @@ def estimate_cost_to_goal(self, current_point: np.ndarray, goal_point: np.ndarra y_diff = (goal_y - current_y) * self.scale_y z_diff = (goal_z - current_z) * self.scale_z - return math.sqrt((x_diff * x_diff) + (y_diff * y_diff) + (z_diff * z_diff)) \ No newline at end of file + return math.sqrt((x_diff * x_diff) + (y_diff * y_diff) + (z_diff * z_diff)) diff --git a/brightest_path_lib/heuristic/euclidean_transonic.py b/brightest_path_lib/heuristic/euclidean_transonic.py new file mode 100644 index 0000000..848e7a4 --- /dev/null +++ b/brightest_path_lib/heuristic/euclidean_transonic.py @@ -0,0 +1,100 @@ +from brightest_path_lib.heuristic import Heuristic +import math +import numpy as np +from typing import Tuple +from transonic import boost + +@boost +class EuclideanTransonic(Heuristic): + """heuristic cost estimation using Euclidean distance from current point to goal point + + Parameters + ---------- + scale : Tuple + the scale of the image's axes. For example (1.0 1.0) for a 2D image. + - for 2D points, the order of scale is: (x, y) + - for 3D points, the order of scale is: (x, y, z) + + Attributes + ---------- + scale_x : float + the scale of the image's X-axis + scale_y : float + the scale of the image's Y-axis + scale_z : float + the scale of the image's Z-axis + + """ + + scale_x: float + scale_y: float + scale_z: float + + def __init__(self, scale: Tuple): + if scale is None: + raise TypeError + if len(scale) == 0: + raise ValueError + + self.scale_x = scale[0] + self.scale_y = scale[1] + self.scale_z = 1.0 + if len(scale) == 3: + self.scale_z = scale[2] + + @boost + def estimate_cost_to_goal(self, current_point: "int64[:]", goal_point: "int64[:]") -> float: + # def estimate_cost_to_goal(self, current_point: np.ndarray, goal_point: np.ndarray) -> float: + """calculates the estimated cost from current point to the goal + + Parameters + ---------- + current_point : numpy ndarray + the coordinates of the current point + goal_point : numpy ndarray + the coordinates of the current point + + Returns + ------- + float + the estimated cost to goal in the form of Euclidean distance + + Notes + ----- + If the image is zoomed in or out, then the scale of one of more + axes will be more or less than 1.0. For example, if the image is zoomed + in to twice its size then the scale of X and Y axes will be 2.0. + + By including the scale in the calculation of distance to the goal we + can get an accurate cost. + + - for 2D points, the order of coordinates is: (y, x) + - for 3D points, the order of coordinates is: (z, x, y) + """ + if current_point is None or goal_point is None: + raise TypeError + if (len(current_point) == 0 or len(goal_point) == 0) or (len(current_point) != len(goal_point)): + raise ValueError + + current_x: int + current_y: int + current_z: int + goal_x: int + goal_y: int + goal_z: int + x_diff: float + y_diff: float + z_diff: float + + current_x, current_y, current_z = current_point[1], current_point[0], 0 + goal_x, goal_y, goal_z = goal_point[1], goal_point[0], 0 + + if len(current_point) == len(goal_point) == 3: + current_z, current_y, current_x = current_point[0], current_point[1], current_point[2] + goal_z, goal_y, goal_x = goal_point[0], goal_point[1], goal_point[2] + + x_diff = (goal_x - current_x) * self.scale_x + y_diff = (goal_y - current_y) * self.scale_y + z_diff = (goal_z - current_z) * self.scale_z + + return math.sqrt((x_diff * x_diff) + (y_diff * y_diff) + (z_diff * z_diff)) diff --git a/brightest_path_lib/image/stats.py b/brightest_path_lib/image/stats.py index 8632f9f..6055f5e 100644 --- a/brightest_path_lib/image/stats.py +++ b/brightest_path_lib/image/stats.py @@ -35,8 +35,8 @@ def __init__(self, image: np.ndarray): if len(image) == 0: raise ValueError - self.min_intensity = np.min(image) - self.max_intensity = np.max(image) + self.min_intensity = float(np.min(image)) + self.max_intensity = float(np.max(image)) self.x_min = 0 self.y_min = 0 diff --git a/brightest_path_lib/input/inputs.py b/brightest_path_lib/input/inputs.py index 268b148..49a68bb 100644 --- a/brightest_path_lib/input/inputs.py +++ b/brightest_path_lib/input/inputs.py @@ -10,4 +10,5 @@ class HeuristicFunction(Enum): EUCLIDEAN = "euclidean" class SearchFunction(Enum): - ASTAR = "astar" \ No newline at end of file + ASTAR = "astar" + NBASTAR = "nbastar" diff --git a/brightest_path_lib/pythran_test.ipynb b/brightest_path_lib/pythran_test.ipynb new file mode 100644 index 0000000..0a5f099 --- /dev/null +++ b/brightest_path_lib/pythran_test.ipynb @@ -0,0 +1,132 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "sys.path.append(\"/Users/vasudhajha/Documents/mapmanager/brightest-path-lib\")\n", + "from brightest_path_lib.cost import Cost, Reciprocal\n", + "\n", + "from transonic import boost" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [], + "source": [ + "@boost\n", + "class FastReciprocal(Cost):\n", + "\n", + " min_intensity: float\n", + " max_intensity: float\n", + " RECIPROCAL_MIN: float\n", + " RECIPROCAL_MAX: float\n", + " _min_step_cost: float\n", + "\n", + " def __init__(self, min_intensity: float, max_intensity: float) -> None:\n", + " super().__init__()\n", + " if min_intensity is None or max_intensity is None:\n", + " raise TypeError\n", + " if min_intensity > max_intensity:\n", + " raise ValueError\n", + " self.min_intensity = min_intensity\n", + " self.max_intensity = max_intensity\n", + " self.RECIPROCAL_MIN = 1E-6\n", + " self.RECIPROCAL_MAX = 255.0\n", + " self._min_step_cost = 1 / self.RECIPROCAL_MAX\n", + " \n", + " @boost\n", + " def cost_of_moving_to(self, intensity_at_new_point: float) -> float:\n", + " if intensity_at_new_point > self.max_intensity:\n", + " raise ValueError\n", + "\n", + " intensity_at_new_point = self.RECIPROCAL_MAX * (intensity_at_new_point - self.min_intensity) / (self.max_intensity - self.min_intensity)\n", + "\n", + " if intensity_at_new_point < self.RECIPROCAL_MIN:\n", + " intensity_at_new_point = self.RECIPROCAL_MIN\n", + " \n", + " return 1.0 / intensity_at_new_point\n", + " \n", + " @boost\n", + " def minimum_step_cost(self) -> float:\n", + " return self._min_step_cost\n" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.3 µs ± 109 ns per loop (mean ± std. dev. of 7 runs, 45,554 loops each)\n" + ] + } + ], + "source": [ + "%%timeit -n45554\n", + "\n", + "fr = FastReciprocal(0, 10000)\n", + "\n", + "fr.cost_of_moving_to(100)\n", + "fr.minimum_step_cost()" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.35 µs ± 147 ns per loop (mean ± std. dev. of 7 runs, 45,554 loops each)\n" + ] + } + ], + "source": [ + "%%timeit -n45554\n", + "\n", + "r = Reciprocal(0, 10000)\n", + "\n", + "r.cost_of_moving_to(100)\n", + "r.minimum_step_cost()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "cudmorelab3.8", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.12" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "d10ee8d44f5c0a7cb50271f8411c55c4fd1535f2289c64976b55c3ed75b77fcd" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/brightest_path_lib/sandbox.py b/brightest_path_lib/sandbox.py index 4effce5..299eda8 100644 --- a/brightest_path_lib/sandbox.py +++ b/brightest_path_lib/sandbox.py @@ -18,27 +18,29 @@ def test_2D_image(): image = tifffile.imread('rr30a_s0_ch2.tif')[30] start_point = np.array([243, 292]) # (y,x) - goal_point = np.array([247, 1019]) # (y,x) + goal_point = np.array([128, 711]) # (y,x) - # astar_search = AStarSearch( - # image, - # start_point, - # goal_point) - # tic = time.perf_counter() - # result = astar_search.search() - # toc = time.perf_counter() - # print(f"Found brightest path in {toc - tic:0.4f} seconds") - # print(f"path size: {len(result)}") - - nbastar_search = NBAStarSearch( + astar_search = AStarSearch( image, start_point, goal_point) tic = time.perf_counter() - result = nbastar_search.search() + result = astar_search.search() toc = time.perf_counter() print(f"Found brightest path in {toc - tic:0.4f} seconds") print(f"path size: {len(result)}") + print(f"Number of nodes viewed: {astar_search.evaluated_nodes}") + + # nbastar_search = NBAStarSearch( + # image, + # start_point, + # goal_point) + # tic = time.perf_counter() + # result = nbastar_search.search() + # toc = time.perf_counter() + # print(f"Found brightest path in {toc - tic:0.4f} seconds") + # print(f"path size: {len(result)}") + #print(f"Number of nodes viewed: {nbastar_search.evaluated_nodes}") viewer = napari.Viewer() # viewer.add_image(twoDImage[:100, :250], colormap='magma') @@ -82,4 +84,4 @@ def test_3D_image(): napari.run() if __name__ == "__main__": - test_3D_image() + test_2D_image() diff --git a/brightest_path_lib/sandbox2.py b/brightest_path_lib/sandbox2.py index 02cf68c..9e8e23f 100644 --- a/brightest_path_lib/sandbox2.py +++ b/brightest_path_lib/sandbox2.py @@ -37,6 +37,7 @@ def run(self): toc = time.perf_counter() print(f"Found brightest path in {toc - tic:0.4f} seconds") print(f"path size: {len(self.search_algorithm.result)}") + print(f"Number of nodes viewed: {self.search_algorithm.evaluated_nodes}") print("Done") @@ -63,6 +64,7 @@ def run(self): toc = time.perf_counter() print(f"Found brightest path in {toc - tic:0.4f} seconds") print(f"path size: {len(self.search_algorithm.result)}") + print(f"Number of nodes viewed: {self.search_algorithm.evaluated_nodes}") print("Done") @@ -87,20 +89,21 @@ def _plot_points(points: List[np.ndarray], color, size, alpha=1.0): def plot_brightest_path(): - # image = data.cells3d()[30, 0] - # start_point = np.array([0,192]) # [y, x] - # goal_point = np.array([198,9]) + image = data.cells3d()[30, 0] + start_point = np.array([0,192]) # [y, x] + goal_point = np.array([198,9]) - image = tifffile.imread('a-star-image.tif') - start_point = np.array([188, 71]) # (y,x) - goal_point = np.array([126, 701]) + # image = tifffile.imread('/Users/vasudhajha/Documents/mapmanager/brightest-path-lib/brightest_path_lib/a-star-image.tif') + # start_point = np.array([188, 71]) # (y,x) + # # goal_point = np.array([164, 197]) + # goal_point = np.array([128, 628]) _plot_image(image, start_point, goal_point) queue = Queue() - search_thread = AStarThread(image, start_point, goal_point, queue) - #search_thread = NBAStarThread(image, start_point, goal_point, queue) + # search_thread = AStarThread(image, start_point, goal_point, queue) + search_thread = NBAStarThread(image, start_point, goal_point, queue) search_thread.start() # start the thread, internally Python calls tt.run() _updateInterval = 100 # wait for this number of results and update plot diff --git a/pyproject.toml b/pyproject.toml index 07de284..33805ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,3 @@ [build-system] -requires = ["setuptools", "wheel"] +requires = ["setuptools", "wheel", "numpy", "transonic", "psutil"] build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/setup.py b/setup.py index 59aa9fc..1257b5a 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,53 @@ +import os +from pathlib import Path from setuptools import setup, find_packages +import sys +from transonic.dist import ParallelBuildExt, make_backend_files, init_transonic_extensions + +here = Path(__file__).parent.absolute() +sys.path.insert(0, ".") VERSION = "0.1" +TRANSONIC_BACKEND = "pythran" + +build_dependencies_backends = { + "pythran": ["pythran"], + "cython": ["cython"], + "python": [], + "numba": ["numba"], +} + + +setup_requires = [] +setup_requires.extend(build_dependencies_backends[TRANSONIC_BACKEND]) + + +def transonize(): + paths = [ + "brightest_path_lib/cost/reciprocal_transonic.py", + "brightest_path_lib/heuristic/euclidean_transonic.py" + ] + make_backend_files([here / path for path in paths], backend=TRANSONIC_BACKEND) + + +def create_pythran_extensions(): + import numpy as np + + extensions = init_transonic_extensions( + # "brightest-path-lib", + "brightest_path_lib", + backend=TRANSONIC_BACKEND, + include_dirs=np.get_include(), + compile_args=("-O3", f"-march=native", "-DUSE_XSIMD"), + # compile_args=("-O2", "-DUSE_XSIMD"), + ) + return extensions + + +def create_extensions(): + transonize() + return create_pythran_extensions() + setup( name="brightest-path-lib", @@ -22,7 +69,10 @@ "brightest_path_lib.algorithm", "brightest_path_lib.input" ]), - install_requires=["numpy"], + setup_requires=setup_requires, + install_requires=["numpy", "transonic"], extras_require={"test": ["pytest", "pytest-cov", "scikit-image", "pooch"]}, python_requires=">=3.7", + cmdclass={"build_ext": ParallelBuildExt}, + ext_modules=create_extensions(), ) diff --git a/tests/test_reciprocal_cost_func.py b/tests/test_reciprocal_cost_func.py index 49f7b94..156c568 100644 --- a/tests/test_reciprocal_cost_func.py +++ b/tests/test_reciprocal_cost_func.py @@ -33,7 +33,7 @@ def test_init_when_min_intensity_greater_than_max(): ]) def test_cost_of_moving_to(min_intensity, max_intensity, intensity_at_new_point, expected_cost): reciprocal_cost_function = Reciprocal(min_intensity, max_intensity) - cost = reciprocal_cost_function.cost_of_moving_to(intensity_at_new_point) + cost = reciprocal_cost_function.cost_of_moving_to(float(intensity_at_new_point)) assert round(cost, 3) == round(expected_cost, 3) def test_minimum_step_cost():