From 7522f768b77f1c9cb6847033cf2a31f8c0860ac9 Mon Sep 17 00:00:00 2001 From: jrb20008 Date: Thu, 20 Mar 2025 23:54:57 -0400 Subject: [PATCH 1/5] Added Custom Types Everything runs abut half as fast but it is much less error prone and more readable --- AUVSim/auv.py | 31 ++++--- CustomTypes/.gitignore | 1 + CustomTypes/__init__.py | 11 +++ CustomTypes/custom_arrays.py | 90 ++++++++++++++++++ CustomTypes/custom_types.py | 92 +++++++++++++++++++ Raytrace/BVHMesh.py | 69 +++++++------- Raytrace/CompositeMesh.py | 5 +- .../ExampleMultipleSonarReadings.ipynb | 17 ++-- .../Examples/ExampleSingleSonarReading.ipynb | 13 +-- Raytrace/PlotRays.py | 6 +- Raytrace/SideScan.py | 18 ++-- Raytrace/TriangleMesh.py | 54 ++++++----- 12 files changed, 306 insertions(+), 101 deletions(-) create mode 100644 CustomTypes/.gitignore create mode 100644 CustomTypes/__init__.py create mode 100644 CustomTypes/custom_arrays.py create mode 100644 CustomTypes/custom_types.py diff --git a/AUVSim/auv.py b/AUVSim/auv.py index 76819f7..9215f86 100644 --- a/AUVSim/auv.py +++ b/AUVSim/auv.py @@ -1,23 +1,26 @@ -from Raytrace.TriangleMesh import TriangleMesh, Ray +from Raytrace.TriangleMesh import TriangleMesh from Raytrace.SideScan import SideScan import numpy as np from typing import Any +from CustomTypes import Ray, Vector3, Vector3Array, RayArray + class AUV: ideal_distance:float # Currently unused - start_pos:np.ndarray - start_facing:np.ndarray - def __init__(self, position:np.ndarray, facing:np.ndarray): + start_pos:Vector3 + start_facing:Vector3 + def __init__(self, position:Vector3, facing:Vector3): self.start_pos = position self.start_facing = facing class AUVPath: - positions:np.ndarray[float, Any] - facings:np.ndarray[float, Any] + positions:Vector3Array + facings:Vector3Array - def __init__(self, positions:np.ndarray, facings:np.ndarray): + def __init__(self, positions:Vector3Array, facings:Vector3Array): self.positions = positions self.facings = facings - def as_rays(self): - return [Ray(i, j) for i, j in zip(self.facings, self.positions)] + @property + def rays(self) -> RayArray: + return RayArray(map(Ray, self.facings, self.positions)) class AUVPathGen: mesh:TriangleMesh @@ -27,8 +30,8 @@ def __init__(self, mesh:TriangleMesh, auv:AUV): self.auv = auv def get_path(self, travel_distance:float, samples:int) -> AUVPath: travel_step = travel_distance / (samples - 1) - positions = [self.auv.start_pos] - facings = [self.auv.start_facing] + positions = Vector3Array([self.auv.start_pos]) + facings = Vector3Array([self.auv.start_facing]) # Utility functions # HACK: There should be a function to generate one side ray @@ -50,10 +53,8 @@ def get_path(self, travel_distance:float, samples:int) -> AUVPath: # point in the direction that the hull is sloping rise = (ray_dist - prev_dist) * side_ray.direction if not (np.isfinite(ray_dist) and np.isfinite(prev_dist)): - rise = np.zeros(3) + rise = Vector3(0, 0, 0) new_facing = normalize(facings[-1] + rise) facings.append(new_facing) - np_positions = np.array(positions) - np_facings = np.array(facings) - return AUVPath(np_positions, np_facings) \ No newline at end of file + return AUVPath(positions, facings) \ No newline at end of file diff --git a/CustomTypes/.gitignore b/CustomTypes/.gitignore new file mode 100644 index 0000000..ba0430d --- /dev/null +++ b/CustomTypes/.gitignore @@ -0,0 +1 @@ +__pycache__/ \ No newline at end of file diff --git a/CustomTypes/__init__.py b/CustomTypes/__init__.py new file mode 100644 index 0000000..0b11c6b --- /dev/null +++ b/CustomTypes/__init__.py @@ -0,0 +1,11 @@ +from .custom_types import ( + Vector3, + Triangle, + Ray, +) +from .custom_arrays import ( + TriangleArray, + Vector3Array, + RayArray, +) + \ No newline at end of file diff --git a/CustomTypes/custom_arrays.py b/CustomTypes/custom_arrays.py new file mode 100644 index 0000000..02ed5d8 --- /dev/null +++ b/CustomTypes/custom_arrays.py @@ -0,0 +1,90 @@ +from . import Triangle, Vector3, Ray +from functools import cached_property +from typing import Generic, Iterable, Iterator, Self, overload, TypeVar +import numpy as np +import numpy.typing as npt + +_T = TypeVar('_T') +class CustomArray(Generic[_T]): + _array:list[_T] + def __init__(self, content:Iterable[_T]|None = None) -> None: + if content is None: content = [] + self._array = list(content) + @overload + def __getitem__(self, index:int) -> _T: ... + @overload + def __getitem__(self, index:slice) -> Self: ... + def __getitem__(self, index): + if isinstance(index, slice): + return type(self)(self._array.__getitem__(index)) + return self._array.__getitem__(index) + @overload + def __setitem__(self, key:int, value:_T) -> None: ... + @overload + def __setitem__(self, key:slice, value:Iterable[_T]) -> None: ... + def __setitem__(self, key, value): + self._array.__setitem__(key, value) + def append(self, object: _T) -> None: + self._array.append(object) + + def __len__(self) -> int: + return len(self._array) + def __iter__(self) -> Iterator[_T]: + return self._array.__iter__() + + def __str__(self): + return self._array.__str__() + def __repr__(self): + return self._array.__repr__() + def __add__(self, other:Self|list[_T]) -> Self: + if isinstance(other, list): + return type(self)(self._array + other) + return type(self)(self._array + other._array) + +_N = TypeVar('_N', bound = npt.ArrayLike) +class CustomNPArray(CustomArray[_N]): + @cached_property + def as_np(self) -> np.ndarray: + return np.array(self._array) + def __array__(self) -> np.ndarray: + return self.as_np + + def __setitem__(self, key, value): + super().__setitem__(key, value) + self.__dict__.pop('as_np', None) + def append(self, object: _N) -> None: + self._array.append(object) + self.__dict__.pop('as_np', None) + + +class Vector3Array(CustomNPArray[Vector3]): + def __init__(self, + content: Iterable[Vector3]|np.ndarray|None = None, + ) -> None: + if isinstance(content, np.ndarray): + if content.ndim == 1: content.reshape((1,) + content.shape) + if content.ndim != 2: raise ValueError(content) # must be 1d or 2d + if content.shape[1] != 3: raise ValueError(content) # must be shape (n, 3) + content = [Vector3(*v) for v in content] + super().__init__(content) + +class TriangleArray(CustomNPArray[Triangle]): + _array:list[Triangle] + def __init__(self, + content: Iterable[Triangle]|np.ndarray|None = None, + ) -> None: + if isinstance(content, np.ndarray): + if content.ndim == 2: content.reshape((1,) + content.shape) + if content.ndim != 3: raise ValueError(content) # must be 2d or 3d + if content.shape[1] != 3 or content.shape[2] != 3: raise ValueError(content) # must be shape (n, 3, 3) + content = [Triangle(*[Vector3(*v) for v in t]) for t in content] + super().__init__(content) + @property + def verticies(self) -> tuple[Vector3Array, Vector3Array, Vector3Array]: + v0, v1, v2 = self.as_np.swapaxes(0, 1) + return (Vector3Array(v0), Vector3Array(v1), Vector3Array(v2)) + +class RayArray(CustomArray[Ray]): + @classmethod + def from_components(cls, directions:Iterable[Vector3], origins:Iterable[Vector3]) -> 'RayArray': + return cls(map(Ray, directions, origins)) \ No newline at end of file diff --git a/CustomTypes/custom_types.py b/CustomTypes/custom_types.py new file mode 100644 index 0000000..9ad27b6 --- /dev/null +++ b/CustomTypes/custom_types.py @@ -0,0 +1,92 @@ +from functools import cached_property +from typing import Self +import numpy as np + +class Vector3: + x:float + y:float + z:float + def __init__(self, + x: float | tuple[float, float, float] | Self, + y: float = 0, + z: float = 0, + ) -> None: + if isinstance(x, tuple): + x, y, z = x + elif isinstance(x, Vector3): + x, y, z = self.x, self.y, self.z + self.x, self.y, self.z = x, y, z + + def __str__(self): + return f"({self.x}, {self.y}, {self.z})" + def __repr__(self): + return f"Vector3({self.x}, {self.y}, {self.z})" + + @cached_property + def as_np(self) -> np.ndarray: + # print('\r',self) + try: + return np.array([self.x, self.y, self.z], dtype = float) + except ValueError as e: + print(self) + raise e + + def __array__(self, *args, **kwrds) -> np.ndarray: + return self.as_np.__array__(*args, **kwrds) + + def __add__(self, value:Self) -> Self: + if not isinstance(value, Vector3): return NotImplemented + return type(self)(*(self.as_np + value.as_np)) + def __sub__(self, value:Self) -> Self: + if not isinstance(value, Vector3): return NotImplemented + return type(self)(*(self.as_np - value.as_np)) + def __mul__(self, value:Self|float) -> Self: + if isinstance(value, Vector3): + return type(self)(*(self.as_np * value.as_np)) + if not isinstance(value, np.ScalarType): return NotImplemented + return type(self)(*(self.as_np * value)) + def __rmul__(self, value:float) -> Self: + if not isinstance(value, np.ScalarType): return NotImplemented + return type(self)(*(self.as_np * value)) + def __truediv__(self, value:Self|float) -> Self: + if isinstance(value, Vector3): + return type(self)(*(self.as_np / value.as_np)) + if not isinstance(value, np.ScalarType): return NotImplemented + return type(self)(*(self.as_np / value)) + def __rtruediv__(self, value:float) -> Self: + if not isinstance(value, np.ScalarType): return NotImplemented + return type(self)(*(value / self.as_np)) +class Triangle: + v1:Vector3 + v2:Vector3 + v3:Vector3 + + def __init__(self, + v1: Vector3 | tuple[Vector3, Vector3, Vector3], + v2: Vector3 = Vector3(0, 0, 0), + v3: Vector3 = Vector3(0, 0, 0), + ) -> None: + if isinstance(v1, tuple): + v1, v2, v3 = v1 + self.v1, self.v2, self.v3 = v1, v2, v3 + + def __str__(self): + return f"({self.v1}, {self.v2}, {self.v3}))" + def __repr__(self): + return f"Triangle({self.v1}, {self.v2}, {self.v3})" + + @cached_property + def as_np(self) -> np.ndarray: + return np.array([self.v1, self.v2, self.v3]) + def __array__(self, *args, **kwrds) -> np.ndarray: + return self.as_np.__array__(*args, **kwrds) + +class Ray: + direction:Vector3 + origin:Vector3 + def __init__(self, + direction:Vector3, + origin:Vector3, + ) -> None: + self.direction = direction + self.origin = origin diff --git a/Raytrace/BVHMesh.py b/Raytrace/BVHMesh.py index 6edb52e..d1dd99c 100644 --- a/Raytrace/BVHMesh.py +++ b/Raytrace/BVHMesh.py @@ -1,39 +1,41 @@ -from typing import Self, Literal +from typing import Self, Literal, Type, TypeVar import numpy as np -from Raytrace.TriangleMesh import TriangleMesh, Ray + +from .TriangleMesh import TriangleMesh +from CustomTypes import Ray, TriangleArray, Vector3, Vector3Array # Adapted from https://jacco.ompf2.com/2022/04/13/how-to-build-a-bvh-part-1-basics/ class BVHAABB: - min_pos:np.ndarray - max_pos:np.ndarray + min_pos:Vector3 + max_pos:Vector3 def __init__(self): - self.min_pos = np.array([np.nan]*3) - self.max_pos = np.array([np.nan]*3) + self.min_pos = Vector3((np.nan,)*3) + self.max_pos = Vector3((np.nan,)*3) @staticmethod - def from_array(triangles:np.ndarray) -> 'BVHAABB': + def from_array(triangles:TriangleArray) -> 'BVHAABB': out = BVHAABB() - if triangles.size == 0: return out - triangles = triangles.reshape((-1, 3)) - out.min_pos = triangles.min(axis = 0) - out.max_pos = triangles.max(axis = 0) + if len(triangles) == 0: return out + tris = triangles.as_np.reshape((-1, 3)) + out.min_pos = Vector3(*tris.min(axis = 0)) + out.max_pos = Vector3(*tris.max(axis = 0)) return out def raytrace(self, ray:Ray) -> float: old_error_state = np.seterr(divide='ignore') - t1 = (self.min_pos - ray.origin) * ray.inverse_direction - t2 = (self.max_pos - ray.origin) * ray.inverse_direction + t1 = (self.min_pos.as_np - ray.origin.as_np) / ray.direction.as_np + t2 = (self.max_pos.as_np - ray.origin.as_np) / ray.direction.as_np np.seterr(**old_error_state) t = np.stack([t1, t2]) tmin:float = t.min(axis=0).max() tmax:float = t.max(axis=0).min() return tmin if tmax >= tmin and tmax > 0 else np.inf - def grow(self, point:np.ndarray): - self.min_pos = np.nanmin(np.stack([point, self.min_pos]), axis=0) - self.max_pos = np.nanmax(np.stack([point, self.max_pos]), axis=0) + def grow(self, point:Vector3): + self.min_pos = Vector3(*np.nanmin(np.array([point, self.min_pos]), axis=0)) + self.max_pos = Vector3(*np.nanmax(np.array([point, self.max_pos]), axis=0)) def grow_aabb(self, other:Self): self.grow(other.min_pos) self.grow(other.max_pos) def area(self) -> float: - e = self.max_pos - self.min_pos + e = self.max_pos.as_np - self.min_pos.as_np return e[0] * e[1] + e[1] * e[2] + e[2] * e[0] def __str__(self): return f'({float(self.min_pos[0])}, {float(self.min_pos[1])}, {float(self.min_pos[2])})#({float(self.max_pos[0])}, {float(self.max_pos[1])}, {float(self.max_pos[2])})' @@ -43,14 +45,15 @@ class BVHNode: right:Self start_index:int tri_count:int - def get_subarray(self, triangles:np.ndarray) -> np.ndarray: + _T = TypeVar('_T', TriangleArray, Vector3Array) + def get_subarray(self, triangles:_T) -> _T: return triangles[self.start_index:self.start_index + self.tri_count] - def update_bounds(self, triangles:np.ndarray): + def update_bounds(self, triangles:TriangleArray): self.aabb = BVHAABB.from_array(self.get_subarray(triangles)) def is_leaf(self) -> bool: return self.tri_count > 0 class BVHMesh(TriangleMesh): - centroids:np.ndarray + centroids:Vector3Array root:BVHNode node_count:int def __init__(self, *args, min_node_size = 2, **kwargs) -> None: @@ -58,12 +61,12 @@ def __init__(self, *args, min_node_size = 2, **kwargs) -> None: self.build_BVH(min_node_size) def build_BVH(self, min_node_size = 100) -> None: # calculate triangle centroids for partitioning - self.centroids = self.triangles.sum(axis = 1) / 3 + self.centroids = Vector3Array(self.triangles.as_np.sum(axis = 1) / 3) # assign all triangles to root node self.node_count = 1 self.root = root = BVHNode() root.start_index = 0 - root.tri_count = self.triangles.shape[0] + root.tri_count = self.triangles.as_np.shape[0] if root.tri_count == 0: return root.update_bounds(self.triangles) # subdivide recursively @@ -78,10 +81,11 @@ def subdivide(self, node:BVHNode, min_node_size = 100) -> None: i = node.start_index j = i + node.tri_count - 1 while i <= j: - if self.centroids[i, axis] < split_pos: i += 1 + if self.centroids[i].as_np[axis] < split_pos: i += 1 else: - self.triangles[[i,j]] = self.triangles[[j,i]] - self.centroids[[i,j]] = self.centroids[[j,i]] + # Convoluted notation for a swap + self.triangles[i:i+1], self.triangles[j:j+1] = self.triangles[j:j+1], self.triangles[i:i+1] + self.centroids[i:i+1], self.centroids[j:j+1] = self.centroids[j:j+1], self.centroids[i:i+1] j -= 1 # abort split if one of the sides is empty left_count = i - node.start_index @@ -137,19 +141,22 @@ def find_best_split(self, node:BVHNode) -> tuple[float, int, float]: axis:int = -1 split_pos:float = 0 best_cost:float = np.inf - triangles:np.ndarray[tuple[int, Literal[3], Literal[3]], np.dtype[np.float32]] = node.get_subarray(self.triangles) - centroids:np.ndarray[tuple[int, Literal[3]], np.dtype[np.float32]] = node.get_subarray(self.centroids) + triangles:TriangleArray = node.get_subarray(self.triangles) + centroids:Vector3Array = node.get_subarray(self.centroids) for axis_i in range(3): - bounds_min = float(np.min(centroids[:,axis_i])) - bounds_max = float(np.max(centroids[:,axis_i])) + bounds_min = float(np.min(centroids.as_np[:,axis_i])) + bounds_max = float(np.max(centroids.as_np[:,axis_i])) if bounds_min == bounds_max: continue # populate the bins scale:float = BINS / (bounds_max - bounds_min) - bin_idx = ((centroids[:,axis_i] - bounds_min) * scale).astype(int) + bin_idx = ((centroids.as_np[:,axis_i] - bounds_min) * scale).astype(int) bin_idx[bin_idx > BINS - 1] = BINS - 1 - bin_bounds = [BVHAABB.from_array(triangles[bin_idx == n]) for n in range(BINS)] + bin_bounds = [BVHAABB.from_array( + TriangleArray([t for t, b in zip(triangles, bin_idx) if b == n]) # triangles[bin_idx == n] + ) for n in range(BINS)] bin_counts = [np.count_nonzero(bin_idx == n) for n in range(BINS)] + # gather data for the 7 planes between the 8 bins left_area = np.zeros(BINS - 1, float) right_area = np.zeros(BINS - 1, float) diff --git a/Raytrace/CompositeMesh.py b/Raytrace/CompositeMesh.py index f1dc5b6..16485f9 100644 --- a/Raytrace/CompositeMesh.py +++ b/Raytrace/CompositeMesh.py @@ -1,11 +1,12 @@ import numpy as np -from Raytrace.TriangleMesh import TriangleMesh, Ray +from .TriangleMesh import TriangleMesh +from CustomTypes import Ray, TriangleArray class CompositeMesh(TriangleMesh): meshes: list[TriangleMesh] def __init__(self, meshes:list[TriangleMesh]) -> None: self.meshes = meshes - self.triangles = np.concatenate([mesh.triangles for mesh in self.meshes]) + self.triangles = sum([mesh.triangles for mesh in self.meshes], start = TriangleArray()) def raytrace(self, ray:Ray) -> float: distances = [mesh.raytrace(ray) for mesh in self.meshes] return min(distances, default = np.inf) \ No newline at end of file diff --git a/Raytrace/Examples/ExampleMultipleSonarReadings.ipynb b/Raytrace/Examples/ExampleMultipleSonarReadings.ipynb index d5f6cb9..669d30e 100644 --- a/Raytrace/Examples/ExampleMultipleSonarReadings.ipynb +++ b/Raytrace/Examples/ExampleMultipleSonarReadings.ipynb @@ -2,12 +2,12 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import sys; sys.path.append('../..')\n", - "from Raytrace.TriangleMesh import Ray\n", + "from CustomTypes import Ray, RayArray, Vector3, Vector3Array\n", "from Raytrace.BVHMesh import BVHMesh\n", "from Raytrace.SideScan import SideScan, ScanReading\n", "import numpy as np\n", @@ -22,7 +22,8 @@ "source": [ "stl_path = 'sample_Hull_Mesh.stl'\n", "start_time = time.time()\n", - "mesh = BVHMesh(stl_path = stl_path, min_node_size = 100)\n", + "# mesh = BVHMesh(stl_path = stl_path, min_node_size = 100)\n", + "mesh = BVHMesh.load('sample_Hull_Mesh.pkl')\n", "end_time = time.time()\n", "\n", "print('Build Time:', end_time - start_time, 'seconds')" @@ -34,18 +35,18 @@ "metadata": {}, "outputs": [], "source": [ - "facing = np.array([1, 0, 0])\n", + "facing = Vector3(1, 0, 0)\n", "min_angle = 0\n", "max_angle = -np.pi/2\n", "sample_ray_count = 1000\n", "\n", - "origin_start = np.array([5, -1, 1])\n", - "origin_end = np.array([9, -1, 1])\n", + "origin_start = Vector3(5, -1, 1)\n", + "origin_end = Vector3(9, -1, 1)\n", "readings_count = 100\n", "# liniar interpolation between the two for this simple demo\n", - "origins = [origin_start * (1-i) + origin_end * i for i in np.arange(0, 1+1/(readings_count-1)/2, 1/(readings_count-1))]\n", + "origins = Vector3Array([origin_start * (1-i) + origin_end * i for i in np.arange(0, 1+1/(readings_count-1)/2, 1/(readings_count-1))])\n", "\n", - "orientations = [Ray(facing, origin) for origin in origins]\n", + "orientations = RayArray.from_components([facing] * readings_count, origins)\n", "\n", "rays_list = [SideScan.generate_rays(orientation, min_angle, max_angle, sample_ray_count) for orientation in orientations]\n", "\n", diff --git a/Raytrace/Examples/ExampleSingleSonarReading.ipynb b/Raytrace/Examples/ExampleSingleSonarReading.ipynb index 0a89788..3b4b17e 100644 --- a/Raytrace/Examples/ExampleSingleSonarReading.ipynb +++ b/Raytrace/Examples/ExampleSingleSonarReading.ipynb @@ -2,16 +2,17 @@ "cells": [ { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import sys; sys.path.append('../..')\n", - "from Raytrace.TriangleMesh import Ray\n", + "from CustomTypes import Ray\n", "from Raytrace.BVHMesh import BVHMesh\n", "from Raytrace.SideScan import SideScan, ScanReading\n", "import numpy as np\n", - "import time" + "import time\n", + "from CustomTypes import Vector3" ] }, { @@ -34,8 +35,8 @@ "metadata": {}, "outputs": [], "source": [ - "origin = np.array([5, -1, 1])\n", - "facing = np.array([1, 0, 0])\n", + "origin = Vector3(5, -1, 1)\n", + "facing = Vector3(1, 0, 0)\n", "min_angle = 0\n", "max_angle = -np.pi/2\n", "sample_ray_count = 1000\n", @@ -47,7 +48,7 @@ "raw_reading = SideScan(mesh).scan_rays(rays)\n", "reading = ScanReading(raw_reading)\n", "\n", - "print('Triangles:', mesh.triangles.shape[0])\n", + "print('Triangles:', len(mesh.triangles))\n", "raw_reading.print_summary()" ] }, diff --git a/Raytrace/PlotRays.py b/Raytrace/PlotRays.py index 6fa0dd2..34065e8 100644 --- a/Raytrace/PlotRays.py +++ b/Raytrace/PlotRays.py @@ -1,5 +1,5 @@ -from Raytrace.TriangleMesh import TriangleMesh -from Raytrace.SideScan import ScanReading, RawScanReading +from .TriangleMesh import TriangleMesh +from .SideScan import ScanReading, RawScanReading import numpy as np try: import plotly.graph_objs as go # type: ignore @@ -8,7 +8,7 @@ exit() def plot_mesh(mesh:TriangleMesh, **kwargs) -> go.Mesh3d: - x, y, z = mesh.triangles.reshape((-1,3)).swapaxes(0, 1) + x, y, z = mesh.triangles.as_np.reshape((-1,3)).swapaxes(0, 1) i, j, k = [list(range(i,x.shape[0],3)) for i in range(3)] return go.Mesh3d(x=x, y=y, z=z, i=i, j=j, k=k, **kwargs) diff --git a/Raytrace/SideScan.py b/Raytrace/SideScan.py index ac14c99..d4a4ed0 100644 --- a/Raytrace/SideScan.py +++ b/Raytrace/SideScan.py @@ -1,4 +1,5 @@ -from Raytrace.TriangleMesh import TriangleMesh, Ray +from .TriangleMesh import TriangleMesh +from CustomTypes import Ray, Vector3, RayArray, Vector3Array from typing import Self import numpy as np import time @@ -15,7 +16,7 @@ class RawScanReading: start_time:float = -1 end_time:float = -1 - def __init__(self, distances:np.ndarray, rays:list[Ray]): + def __init__(self, distances:np.ndarray, rays:RayArray): old_error_state = np.seterr(all='ignore') self.distances = distances self.origins = np.array([r.origin for r in rays]) @@ -34,7 +35,7 @@ def combine_min(self, other:Self) -> Self: new_distances[other_smaller] = other.distances[other_smaller] # HACK: new rays should be generated based on which distance was closer - new_rays = [Ray(i, j) for i, j in zip(self.directions, self.origins)] + new_rays = RayArray.from_components(self.directions, self.origins) return type(self)(new_distances, new_rays) def print_summary(self) -> None: @@ -111,7 +112,7 @@ def __init__(self, mesh:TriangleMesh, smooth_dist:float = 0.05, result_reselutio self.mesh = mesh self.smooth_dist = smooth_dist self.result_reselution = result_reselution - def scan_rays(self, rays:list[Ray]) -> RawScanReading: + def scan_rays(self, rays:RayArray) -> RawScanReading: distances = np.empty((len(rays),), np.float32) start_time = time.time() @@ -126,17 +127,18 @@ def scan_rays(self, rays:list[Ray]) -> RawScanReading: return out @staticmethod - def generate_rays(orientation:Ray, min_angle:float, max_angle:float, angle_reselution:int) -> list[Ray]: + def generate_rays(orientation:Ray, min_angle:float, max_angle:float, angle_reselution:int) -> RayArray: angle_step = (max_angle-min_angle)/(angle_reselution-1) angles = np.arange(min_angle, max_angle + angle_step/2, angle_step) origin = orientation.origin - pitch = np.arctan2(orientation.direction[2],np.sqrt(np.dot(orientation.direction[0:2], orientation.direction[0:2]))) - yaw = np.arctan2(orientation.direction[1], orientation.direction[0]) # yaw = 0 => facing due +x + pitch = np.arctan2(orientation.direction.as_np[2],np.sqrt(np.dot(orientation.direction.as_np[0:2], orientation.direction.as_np[0:2]))) + yaw = np.arctan2(orientation.direction.as_np[1], orientation.direction.as_np[0]) # yaw = 0 => facing due +x # Precomputed rotation matrix sin_comp = np.array([-np.sin(pitch) * np.cos(yaw), np.sin(pitch) * np.sin(yaw), np.cos(pitch)]) cos_comp = np.array([np.sin(yaw), np.cos(yaw), 0]) - return [Ray(np.sin(a) * sin_comp + np.cos(a) * cos_comp, origin) for a in angles] + directions = Vector3Array(np.array([np.sin(a) * sin_comp + np.cos(a) * cos_comp for a in angles])) + return RayArray.from_components(directions, [origin]*angle_reselution) \ No newline at end of file diff --git a/Raytrace/TriangleMesh.py b/Raytrace/TriangleMesh.py index 50be840..452ea44 100644 --- a/Raytrace/TriangleMesh.py +++ b/Raytrace/TriangleMesh.py @@ -1,27 +1,23 @@ +from typing import Any import numpy as np import pickle - -class Ray: - direction:np.ndarray - origin:np.ndarray - inverse_direction:np.ndarray - def __init__(self, direction:np.ndarray, origin:np.ndarray) -> None: - self.direction = direction - self.origin = origin - old_error_state = np.seterr(divide='ignore', invalid='ignore') - self.inverse_direction = np.ones(3) / direction - np.seterr(**old_error_state) +from CustomTypes import Triangle, TriangleArray, Ray +import stl class TriangleMesh: - # An n by 3 by 3 array of floats representing a list of triangles in the form triangle_index, vertex_index, axis_index - triangles:np.ndarray - - def __init__(self, triangles: np.ndarray|None = None, stl_path: str|None = None): + triangles:TriangleArray + def __init__(self, + triangles: np.ndarray|TriangleArray|None = None, + stl_path: str|None = None): if not stl_path is None: - import stl mesh = stl.Mesh.from_file(stl_path) - triangles = np.array([mesh.v0, mesh.v1, mesh.v2]).swapaxes(0, 1) - self.triangles = np.ndarray((0,3), float) if triangles is None else triangles + self.triangles = TriangleArray(np.array([mesh.v0, mesh.v1, mesh.v2]).swapaxes(0, 1)) + elif triangles is None: + self.triangles = TriangleArray([]) + elif isinstance(triangles, np.ndarray): + self.triangles = TriangleArray(triangles) + else: + self.triangles = triangles def raytrace(self, ray:Ray) -> float: return self.array_raytrace(ray) @@ -45,13 +41,13 @@ def array_raytrace(self, ray:Ray) -> float: return np.min(out) @staticmethod - def triangle_ray_intersection(triangle, ray:Ray, epsilon = 1e-10) -> float: + def triangle_ray_intersection(triangle:Triangle, ray:Ray, epsilon = 1e-10) -> float: # Translated from https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm#C++_implementation - v0, v1, v2 = triangle + v0, v1, v2 = triangle.as_np edge1 = v1 - v0 edge2 = v2 - v0 - ray_cross_e2 = np.cross(ray.direction, edge2) + ray_cross_e2 = np.cross(ray.direction.as_np, edge2) det = np.dot(edge1, ray_cross_e2) if -epsilon < det < epsilon: @@ -65,7 +61,7 @@ def triangle_ray_intersection(triangle, ray:Ray, epsilon = 1e-10) -> float: return -1 s_cross_e1 = np.cross(s, edge1) - v = inv_det * np.dot(ray.direction, s_cross_e1) + v = inv_det * np.dot(ray.direction.as_np, s_cross_e1) if v < -epsilon or 1 < u + v: return -1 @@ -79,19 +75,21 @@ def triangle_ray_intersection(triangle, ray:Ray, epsilon = 1e-10) -> float: else: # This means that there is a line intersection but not a ray intersection. return -1 @staticmethod - def batch_triangle_ray_intersection(triangle_array, ray:Ray, epsilon = 1e-10) -> np.ndarray: + def batch_triangle_ray_intersection(triangle_array:TriangleArray, ray:Ray, epsilon = 1e-10) -> np.ndarray[np.floating, Any]: vecdot = lambda a, b : np.sum(a*b, axis=-1) # Translated from https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm#C++_implementation - triangle_array_comp = triangle_array.swapaxes(0, 1) - v0, v1, v2 = triangle_array_comp[0], triangle_array_comp[1], triangle_array_comp[2] - + v0a, v1a, v2a = triangle_array.verticies + v0:np.ndarray = v0a.as_np + v1:np.ndarray = v1a.as_np + v2:np.ndarray = v2a.as_np + # np.subtract(v1, v0, out= v1) # np.subtract(v2, v0, out= v2) v1 = v1 - v0 v2 = v2 - v0 - ray_cross_e2 = np.cross(ray.direction, v2) + ray_cross_e2 = np.cross(ray.direction.as_np, v2) det = vecdot(v1, ray_cross_e2) # if -epsilon < det < epsilon: # return None # This ray is parallel to this triangle. @@ -109,7 +107,7 @@ def batch_triangle_ray_intersection(triangle_array, ray:Ray, epsilon = 1e-10) -> np.logical_or(valid, 1 < u + v, out=valid) np.invert(valid, out=valid) - t = np.empty(triangle_array.shape[0], dtype=np.float64) + t = np.empty(triangle_array.as_np.shape[0], dtype=np.float64) t.fill(-1) np.multiply(inv_det, vecdot(v2, s_cross_e1), out = t, where = valid) From 53ead0edd05d3651622345373ecc30e5f07131a3 Mon Sep 17 00:00:00 2001 From: jrb20008 Date: Fri, 21 Mar 2025 14:36:11 -0400 Subject: [PATCH 2/5] Changed Raytrace Imports Change the Raytrace library so that mesh classes can be imported directly --- AUVSim/auv.py | 2 +- Raytrace/Examples/ExampleMultipleSonarReadings.ipynb | 5 ++--- Raytrace/Examples/ExampleSingleSonarReading.ipynb | 2 +- Raytrace/PlotRays.py | 2 +- Raytrace/SideScan.py | 2 +- Raytrace/__init__.py | 3 +++ Raytrace/{BVHMesh.py => bvh_mesh.py} | 2 +- Raytrace/{CompositeMesh.py => composite_mesh.py} | 2 +- Raytrace/{TriangleMesh.py => triangle_mesh.py} | 0 9 files changed, 11 insertions(+), 9 deletions(-) rename Raytrace/{BVHMesh.py => bvh_mesh.py} (99%) rename Raytrace/{CompositeMesh.py => composite_mesh.py} (92%) rename Raytrace/{TriangleMesh.py => triangle_mesh.py} (100%) diff --git a/AUVSim/auv.py b/AUVSim/auv.py index 9215f86..a9d143f 100644 --- a/AUVSim/auv.py +++ b/AUVSim/auv.py @@ -1,4 +1,4 @@ -from Raytrace.TriangleMesh import TriangleMesh +from Raytrace import TriangleMesh from Raytrace.SideScan import SideScan import numpy as np from typing import Any diff --git a/Raytrace/Examples/ExampleMultipleSonarReadings.ipynb b/Raytrace/Examples/ExampleMultipleSonarReadings.ipynb index 669d30e..c30f15b 100644 --- a/Raytrace/Examples/ExampleMultipleSonarReadings.ipynb +++ b/Raytrace/Examples/ExampleMultipleSonarReadings.ipynb @@ -8,7 +8,7 @@ "source": [ "import sys; sys.path.append('../..')\n", "from CustomTypes import Ray, RayArray, Vector3, Vector3Array\n", - "from Raytrace.BVHMesh import BVHMesh\n", + "from Raytrace import BVHMesh\n", "from Raytrace.SideScan import SideScan, ScanReading\n", "import numpy as np\n", "import time" @@ -22,8 +22,7 @@ "source": [ "stl_path = 'sample_Hull_Mesh.stl'\n", "start_time = time.time()\n", - "# mesh = BVHMesh(stl_path = stl_path, min_node_size = 100)\n", - "mesh = BVHMesh.load('sample_Hull_Mesh.pkl')\n", + "mesh = BVHMesh(stl_path = stl_path, min_node_size = 100)\n", "end_time = time.time()\n", "\n", "print('Build Time:', end_time - start_time, 'seconds')" diff --git a/Raytrace/Examples/ExampleSingleSonarReading.ipynb b/Raytrace/Examples/ExampleSingleSonarReading.ipynb index 3b4b17e..4c4bed6 100644 --- a/Raytrace/Examples/ExampleSingleSonarReading.ipynb +++ b/Raytrace/Examples/ExampleSingleSonarReading.ipynb @@ -8,7 +8,7 @@ "source": [ "import sys; sys.path.append('../..')\n", "from CustomTypes import Ray\n", - "from Raytrace.BVHMesh import BVHMesh\n", + "from Raytrace import BVHMesh\n", "from Raytrace.SideScan import SideScan, ScanReading\n", "import numpy as np\n", "import time\n", diff --git a/Raytrace/PlotRays.py b/Raytrace/PlotRays.py index 34065e8..8099adf 100644 --- a/Raytrace/PlotRays.py +++ b/Raytrace/PlotRays.py @@ -1,4 +1,4 @@ -from .TriangleMesh import TriangleMesh +from . import TriangleMesh from .SideScan import ScanReading, RawScanReading import numpy as np try: diff --git a/Raytrace/SideScan.py b/Raytrace/SideScan.py index d4a4ed0..9d73542 100644 --- a/Raytrace/SideScan.py +++ b/Raytrace/SideScan.py @@ -1,4 +1,4 @@ -from .TriangleMesh import TriangleMesh +from . import TriangleMesh from CustomTypes import Ray, Vector3, RayArray, Vector3Array from typing import Self import numpy as np diff --git a/Raytrace/__init__.py b/Raytrace/__init__.py index e69de29..a7cc2e6 100644 --- a/Raytrace/__init__.py +++ b/Raytrace/__init__.py @@ -0,0 +1,3 @@ +from .triangle_mesh import TriangleMesh +from .bvh_mesh import BVHMesh +from .composite_mesh import CompositeMesh \ No newline at end of file diff --git a/Raytrace/BVHMesh.py b/Raytrace/bvh_mesh.py similarity index 99% rename from Raytrace/BVHMesh.py rename to Raytrace/bvh_mesh.py index d1dd99c..ff9ebb6 100644 --- a/Raytrace/BVHMesh.py +++ b/Raytrace/bvh_mesh.py @@ -1,7 +1,7 @@ from typing import Self, Literal, Type, TypeVar import numpy as np -from .TriangleMesh import TriangleMesh +from . import TriangleMesh from CustomTypes import Ray, TriangleArray, Vector3, Vector3Array # Adapted from https://jacco.ompf2.com/2022/04/13/how-to-build-a-bvh-part-1-basics/ diff --git a/Raytrace/CompositeMesh.py b/Raytrace/composite_mesh.py similarity index 92% rename from Raytrace/CompositeMesh.py rename to Raytrace/composite_mesh.py index 16485f9..3447f56 100644 --- a/Raytrace/CompositeMesh.py +++ b/Raytrace/composite_mesh.py @@ -1,5 +1,5 @@ import numpy as np -from .TriangleMesh import TriangleMesh +from . import TriangleMesh from CustomTypes import Ray, TriangleArray class CompositeMesh(TriangleMesh): diff --git a/Raytrace/TriangleMesh.py b/Raytrace/triangle_mesh.py similarity index 100% rename from Raytrace/TriangleMesh.py rename to Raytrace/triangle_mesh.py From 050cfd86f460ef8a28958cb515a53f33c5ee39ed Mon Sep 17 00:00:00 2001 From: jrb20008 Date: Fri, 21 Mar 2025 14:40:23 -0400 Subject: [PATCH 3/5] Restructured Raytrace Library Moved the mesh classes into their own folder --- Raytrace/Meshs/__init__.py | 3 +++ Raytrace/{ => Meshs}/bvh_mesh.py | 0 Raytrace/{ => Meshs}/composite_mesh.py | 0 Raytrace/{ => Meshs}/triangle_mesh.py | 0 Raytrace/__init__.py | 8 +++++--- 5 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 Raytrace/Meshs/__init__.py rename Raytrace/{ => Meshs}/bvh_mesh.py (100%) rename Raytrace/{ => Meshs}/composite_mesh.py (100%) rename Raytrace/{ => Meshs}/triangle_mesh.py (100%) diff --git a/Raytrace/Meshs/__init__.py b/Raytrace/Meshs/__init__.py new file mode 100644 index 0000000..a7cc2e6 --- /dev/null +++ b/Raytrace/Meshs/__init__.py @@ -0,0 +1,3 @@ +from .triangle_mesh import TriangleMesh +from .bvh_mesh import BVHMesh +from .composite_mesh import CompositeMesh \ No newline at end of file diff --git a/Raytrace/bvh_mesh.py b/Raytrace/Meshs/bvh_mesh.py similarity index 100% rename from Raytrace/bvh_mesh.py rename to Raytrace/Meshs/bvh_mesh.py diff --git a/Raytrace/composite_mesh.py b/Raytrace/Meshs/composite_mesh.py similarity index 100% rename from Raytrace/composite_mesh.py rename to Raytrace/Meshs/composite_mesh.py diff --git a/Raytrace/triangle_mesh.py b/Raytrace/Meshs/triangle_mesh.py similarity index 100% rename from Raytrace/triangle_mesh.py rename to Raytrace/Meshs/triangle_mesh.py diff --git a/Raytrace/__init__.py b/Raytrace/__init__.py index a7cc2e6..6827f80 100644 --- a/Raytrace/__init__.py +++ b/Raytrace/__init__.py @@ -1,3 +1,5 @@ -from .triangle_mesh import TriangleMesh -from .bvh_mesh import BVHMesh -from .composite_mesh import CompositeMesh \ No newline at end of file +from .Meshs import ( + TriangleMesh, + BVHMesh, + CompositeMesh, +) \ No newline at end of file From f459f26992cca06d3a472d3242c16ce9c377c3fa Mon Sep 17 00:00:00 2001 From: jrb20008 Date: Fri, 21 Mar 2025 15:13:19 -0400 Subject: [PATCH 4/5] Fixed Typo Related Bug Passing a Vector3 to the Vector3 constructor no longer causes an error --- CustomTypes/custom_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CustomTypes/custom_types.py b/CustomTypes/custom_types.py index 9ad27b6..ede188d 100644 --- a/CustomTypes/custom_types.py +++ b/CustomTypes/custom_types.py @@ -14,7 +14,7 @@ def __init__(self, if isinstance(x, tuple): x, y, z = x elif isinstance(x, Vector3): - x, y, z = self.x, self.y, self.z + y, z, x = x.y, x.z, x.x # x must be last because otherwize it is overwriten self.x, self.y, self.z = x, y, z def __str__(self): From 88f4320476481c26f420ba3d617ff7b75bd2fa49 Mon Sep 17 00:00:00 2001 From: jrb20008 Date: Fri, 21 Mar 2025 21:40:36 -0400 Subject: [PATCH 5/5] Redid Custom Types Custom types are now better integrated with numpy to get speed back to around where they were (well, 2/3rds the speed but whos counting) --- AUVSim/auv.py | 12 +- CustomTypes/__init__.py | 16 +-- CustomTypes/custom_arrays.py | 126 +++++++----------- CustomTypes/custom_arrays.pyi | 51 +++++++ CustomTypes/custom_np.py | 21 +++ CustomTypes/custom_np.pyi | 15 +++ CustomTypes/custom_types.py | 96 ++++--------- CustomTypes/custom_types.pyi | 40 ++++++ .../ExampleMultipleSonarReadings.ipynb | 4 +- Raytrace/Meshs/__init__.py | 6 +- Raytrace/Meshs/bvh_mesh.py | 45 +++---- Raytrace/Meshs/composite_mesh.py | 2 +- Raytrace/Meshs/triangle_mesh.py | 31 ++--- Raytrace/PlotRays.py | 2 +- Raytrace/SideScan.py | 16 +-- Raytrace/__init__.py | 6 +- 16 files changed, 263 insertions(+), 226 deletions(-) create mode 100644 CustomTypes/custom_arrays.pyi create mode 100644 CustomTypes/custom_np.py create mode 100644 CustomTypes/custom_np.pyi create mode 100644 CustomTypes/custom_types.pyi diff --git a/AUVSim/auv.py b/AUVSim/auv.py index a9d143f..d1c3e3a 100644 --- a/AUVSim/auv.py +++ b/AUVSim/auv.py @@ -2,7 +2,7 @@ from Raytrace.SideScan import SideScan import numpy as np from typing import Any -from CustomTypes import Ray, Vector3, Vector3Array, RayArray +from CustomTypes import Ray, Vector3, Vector3Array class AUV: ideal_distance:float # Currently unused @@ -19,8 +19,8 @@ def __init__(self, positions:Vector3Array, facings:Vector3Array): self.positions = positions self.facings = facings @property - def rays(self) -> RayArray: - return RayArray(map(Ray, self.facings, self.positions)) + def rays(self) -> list[Ray]: + return list(map(Ray, self.facings, self.positions)) class AUVPathGen: mesh:TriangleMesh @@ -30,8 +30,8 @@ def __init__(self, mesh:TriangleMesh, auv:AUV): self.auv = auv def get_path(self, travel_distance:float, samples:int) -> AUVPath: travel_step = travel_distance / (samples - 1) - positions = Vector3Array([self.auv.start_pos]) - facings = Vector3Array([self.auv.start_facing]) + positions = [self.auv.start_pos] + facings = [self.auv.start_facing] # Utility functions # HACK: There should be a function to generate one side ray @@ -57,4 +57,4 @@ def get_path(self, travel_distance:float, samples:int) -> AUVPath: new_facing = normalize(facings[-1] + rise) facings.append(new_facing) - return AUVPath(positions, facings) \ No newline at end of file + return AUVPath(Vector3Array(positions), Vector3Array(facings)) \ No newline at end of file diff --git a/CustomTypes/__init__.py b/CustomTypes/__init__.py index 0b11c6b..ff9b0ea 100644 --- a/CustomTypes/__init__.py +++ b/CustomTypes/__init__.py @@ -1,11 +1,11 @@ +from .custom_np import CustomNP as CustomNP from .custom_types import ( - Vector3, - Triangle, - Ray, + Vector3 as Vector3, + Triangle as Triangle, + Ray as Ray, ) from .custom_arrays import ( - TriangleArray, - Vector3Array, - RayArray, -) - \ No newline at end of file + FloatArray as FloatArray, + TriangleArray as TriangleArray, + Vector3Array as Vector3Array, +) \ No newline at end of file diff --git a/CustomTypes/custom_arrays.py b/CustomTypes/custom_arrays.py index 02ed5d8..e881f0b 100644 --- a/CustomTypes/custom_arrays.py +++ b/CustomTypes/custom_arrays.py @@ -1,90 +1,54 @@ -from . import Triangle, Vector3, Ray -from functools import cached_property -from typing import Generic, Iterable, Iterator, Self, overload, TypeVar +from typing import Any, Iterable, Literal, overload import numpy as np -import numpy.typing as npt -_T = TypeVar('_T') -class CustomArray(Generic[_T]): - _array:list[_T] - def __init__(self, content:Iterable[_T]|None = None) -> None: - if content is None: content = [] - self._array = list(content) - @overload - def __getitem__(self, index:int) -> _T: ... - @overload - def __getitem__(self, index:slice) -> Self: ... - def __getitem__(self, index): - if isinstance(index, slice): - return type(self)(self._array.__getitem__(index)) - return self._array.__getitem__(index) - @overload - def __setitem__(self, key:int, value:_T) -> None: ... - @overload - def __setitem__(self, key:slice, value:Iterable[_T]) -> None: ... - def __setitem__(self, key, value): - self._array.__setitem__(key, value) - def append(self, object: _T) -> None: - self._array.append(object) - - def __len__(self) -> int: - return len(self._array) - def __iter__(self) -> Iterator[_T]: - return self._array.__iter__() - - def __str__(self): - return self._array.__str__() - def __repr__(self): - return self._array.__repr__() - def __add__(self, other:Self|list[_T]) -> Self: - if isinstance(other, list): - return type(self)(self._array + other) - return type(self)(self._array + other._array) - -_N = TypeVar('_N', bound = npt.ArrayLike) -class CustomNPArray(CustomArray[_N]): - @cached_property - def as_np(self) -> np.ndarray: - return np.array(self._array) - def __array__(self) -> np.ndarray: - return self.as_np - - def __setitem__(self, key, value): - super().__setitem__(key, value) - self.__dict__.pop('as_np', None) - def append(self, object: _N) -> None: - self._array.append(object) - self.__dict__.pop('as_np', None) +from . import CustomNP, Triangle, Vector3 +class FloatArray(CustomNP): + def __new__(cls, + content: Iterable[float]|np.ndarray|None = None, + ): + if isinstance(content, np.ndarray): + if content.ndim != 1: raise ValueError(content.shape) # must be 1d + return content.view(cls) + if content is None: + return np.ndarray((0,), float).view(cls) + return np.array(content).view(cls) + def __getitem__(self, index): + if isinstance(index, slice): return super().__getitem__(index).view(type(self)) + return super().__getitem__(index) -class Vector3Array(CustomNPArray[Vector3]): - def __init__(self, - content: Iterable[Vector3]|np.ndarray|None = None, - ) -> None: +class Vector3Array(CustomNP): + def __new__(cls, + content: Iterable[Vector3]|np.ndarray|None = None, + ): if isinstance(content, np.ndarray): - if content.ndim == 1: content.reshape((1,) + content.shape) - if content.ndim != 2: raise ValueError(content) # must be 1d or 2d - if content.shape[1] != 3: raise ValueError(content) # must be shape (n, 3) - content = [Vector3(*v) for v in content] - super().__init__(content) + if content.ndim == 1: content = content.reshape((1,) + content.shape) + if content.ndim != 2: raise ValueError(content.shape) # must be 1d or 2d + if content.shape[1] != 3: raise ValueError(content.shape) # must be shape (n, 3) + return content.view(cls) + if content is None: + return np.ndarray((0, 3), float).view(cls) + return np.array(content).view(cls) + def __getitem__(self, index): + if isinstance(index, slice): return super().__getitem__(index).view(type(self)) + return super().__getitem__(index) -class TriangleArray(CustomNPArray[Triangle]): - _array:list[Triangle] - def __init__(self, - content: Iterable[Triangle]|np.ndarray|None = None, - ) -> None: +class TriangleArray(CustomNP): + def __new__(cls, + content: Iterable[Triangle]|np.ndarray|None = None, + ): if isinstance(content, np.ndarray): - if content.ndim == 2: content.reshape((1,) + content.shape) - if content.ndim != 3: raise ValueError(content) # must be 2d or 3d - if content.shape[1] != 3 or content.shape[2] != 3: raise ValueError(content) # must be shape (n, 3, 3) - content = [Triangle(*[Vector3(*v) for v in t]) for t in content] - super().__init__(content) + if content.ndim == 2: content = content.reshape((1,) + content.shape) + if content.ndim != 3: raise ValueError(content.shape) # must be 2d or 3d + if content.shape[1] != 3 or content.shape[2] != 3: raise ValueError(content.shape) # must be shape (n, 3, 3) + return content.view(cls) + if content is None: + return np.ndarray((0, 3, 3), float).view(cls) + return np.array(content).view(cls) + def __getitem__(self, index): + if isinstance(index, slice): return super().__getitem__(index).view(type(self)) + return super().__getitem__(index) @property def verticies(self) -> tuple[Vector3Array, Vector3Array, Vector3Array]: - v0, v1, v2 = self.as_np.swapaxes(0, 1) - return (Vector3Array(v0), Vector3Array(v1), Vector3Array(v2)) - -class RayArray(CustomArray[Ray]): - @classmethod - def from_components(cls, directions:Iterable[Vector3], origins:Iterable[Vector3]) -> 'RayArray': - return cls(map(Ray, directions, origins)) \ No newline at end of file + v0, v1, v2 = self.swapaxes(0, 1) + return (v0.view(Vector3Array), v1.view(Vector3Array), v2.view(Vector3Array)) \ No newline at end of file diff --git a/CustomTypes/custom_arrays.pyi b/CustomTypes/custom_arrays.pyi new file mode 100644 index 0000000..3a20b7e --- /dev/null +++ b/CustomTypes/custom_arrays.pyi @@ -0,0 +1,51 @@ +import numpy as np +from typing import Any, Iterable, Literal, Self, Type, TypeVar, overload + +from . import CustomNP, Triangle, Vector3 + +class FloatArray(CustomNP[tuple[int, Literal[3]], np.dtype[np.float_]]): + @overload + def __new__(cls) -> FloatArray:... + @overload + def __new__(cls, content: Iterable[float]) -> FloatArray:... + @overload + def __new__(cls, content: np.ndarray[tuple[int], np.dtype[np.float_]]) -> FloatArray:... + + @overload # type: ignore + def __getitem__(self, index:int) -> float: ... + @overload + def __getitem__(self, index:slice) -> Self: ... + +class Vector3Array(CustomNP[tuple[int, Literal[3]], np.dtype[np.float_]]): + @overload + def __new__(cls) -> Vector3Array:... + @overload + def __new__(cls, content: Iterable[Vector3]) -> Vector3Array:... + @overload + def __new__(cls, content: np.ndarray[tuple[int, Literal[3]], np.dtype[np.float_]]) -> Vector3Array:... + + @overload # type: ignore + def __getitem__(self, index:int) -> Vector3: ... + @overload + def __getitem__(self, index:tuple[int, Literal[0, 1, 2]]) -> float: ... + @overload + def __getitem__(self, index:slice) -> Self: ... + +class TriangleArray(CustomNP[tuple[int, Literal[3], Literal[3]], np.dtype[np.float_]]): + @overload + def __new__(cls) -> TriangleArray:... + @overload + def __new__(cls, content: Iterable[Triangle]) -> TriangleArray:... + @overload + def __new__(cls, content: np.ndarray[tuple[int, Literal[3], Literal[3]], np.dtype[np.float_]]) -> TriangleArray:... + + @overload # type: ignore + def __getitem__(self, index:tuple[int, Literal[0, 1, 2]]) -> Vector3: ... + @overload + def __getitem__(self, index:tuple[int, Literal[0, 1, 2], Literal[0, 1, 2]]) -> float: ... + @overload + def __getitem__(self, index:slice) -> Self: ... + + @property + def verticies(self) -> tuple[Vector3Array, Vector3Array, Vector3Array]: ... + diff --git a/CustomTypes/custom_np.py b/CustomTypes/custom_np.py new file mode 100644 index 0000000..9ab0a44 --- /dev/null +++ b/CustomTypes/custom_np.py @@ -0,0 +1,21 @@ + +from typing import Generic, TypeVar, Self +import numpy as np + + +_S = TypeVar('_S', bound=tuple) +_T = TypeVar('_T', bound=np.dtype) +class CustomNP(np.ndarray[_S, _T]): + def __add__(self, other): + return super().__add__(other).view(type(self)) + def __sub__(self, other): + return super().__sub__(other).view(type(self)) + def __mul__(self, other): + return super().__mul__(other).view(type(self)) + def __truediv__(self, other): + return super().__truediv__(other).view(type(self)) + def __rtruediv__(self, other): + return super().__rtruediv__(other).view(type(self)) + @property + def nd(self): + return self.view(np.ndarray) \ No newline at end of file diff --git a/CustomTypes/custom_np.pyi b/CustomTypes/custom_np.pyi new file mode 100644 index 0000000..9786798 --- /dev/null +++ b/CustomTypes/custom_np.pyi @@ -0,0 +1,15 @@ +from typing import TypeVar, Self +import numpy as np + + +_S = TypeVar('_S', bound=tuple) +_T = TypeVar('_T', bound=np.dtype) +class CustomNP(np.ndarray[_S, _T]): + def __add__(self, other:Self) -> Self: ... # type: ignore + def __sub__(self, other:Self) -> Self: ... # type: ignore + def __mul__(self, other:Self|float) -> Self: ... # type: ignore + def __truediv__(self, other:Self|float) -> Self: ... # type: ignore + def __rtruediv__(self, other:float) -> Self: ... # type: ignore + + @property + def nd(self) -> np.ndarray: ... diff --git a/CustomTypes/custom_types.py b/CustomTypes/custom_types.py index ede188d..193a87f 100644 --- a/CustomTypes/custom_types.py +++ b/CustomTypes/custom_types.py @@ -1,85 +1,35 @@ -from functools import cached_property -from typing import Self +from typing import Any, Iterable, Literal, Self, overload import numpy as np +from . import CustomNP -class Vector3: - x:float - y:float - z:float - def __init__(self, - x: float | tuple[float, float, float] | Self, - y: float = 0, - z: float = 0, - ) -> None: +class Vector3(CustomNP): + def __new__(cls, + x: float | tuple[float, float, float] | Self, y = 0, z = 0): if isinstance(x, tuple): x, y, z = x elif isinstance(x, Vector3): - y, z, x = x.y, x.z, x.x # x must be last because otherwize it is overwriten - self.x, self.y, self.z = x, y, z - + x, y, z = x + assert isinstance(x, (float, int)) + return np.array([x, y, z]).view(cls) def __str__(self): - return f"({self.x}, {self.y}, {self.z})" + return f"({self[0]}, {self[1]}, {self[2]})" def __repr__(self): - return f"Vector3({self.x}, {self.y}, {self.z})" - - @cached_property - def as_np(self) -> np.ndarray: - # print('\r',self) - try: - return np.array([self.x, self.y, self.z], dtype = float) - except ValueError as e: - print(self) - raise e - - def __array__(self, *args, **kwrds) -> np.ndarray: - return self.as_np.__array__(*args, **kwrds) + return f"Vector3({self[0]}, {self[1]}, {self[2]})" - def __add__(self, value:Self) -> Self: - if not isinstance(value, Vector3): return NotImplemented - return type(self)(*(self.as_np + value.as_np)) - def __sub__(self, value:Self) -> Self: - if not isinstance(value, Vector3): return NotImplemented - return type(self)(*(self.as_np - value.as_np)) - def __mul__(self, value:Self|float) -> Self: - if isinstance(value, Vector3): - return type(self)(*(self.as_np * value.as_np)) - if not isinstance(value, np.ScalarType): return NotImplemented - return type(self)(*(self.as_np * value)) - def __rmul__(self, value:float) -> Self: - if not isinstance(value, np.ScalarType): return NotImplemented - return type(self)(*(self.as_np * value)) - def __truediv__(self, value:Self|float) -> Self: - if isinstance(value, Vector3): - return type(self)(*(self.as_np / value.as_np)) - if not isinstance(value, np.ScalarType): return NotImplemented - return type(self)(*(self.as_np / value)) - def __rtruediv__(self, value:float) -> Self: - if not isinstance(value, np.ScalarType): return NotImplemented - return type(self)(*(value / self.as_np)) -class Triangle: - v1:Vector3 - v2:Vector3 - v3:Vector3 - - def __init__(self, - v1: Vector3 | tuple[Vector3, Vector3, Vector3], +class Triangle(CustomNP): + def __new__(cls, + v0: Vector3 | tuple[Vector3, Vector3, Vector3], + v1: Vector3 = Vector3(0, 0, 0), v2: Vector3 = Vector3(0, 0, 0), - v3: Vector3 = Vector3(0, 0, 0), - ) -> None: - if isinstance(v1, tuple): - v1, v2, v3 = v1 - self.v1, self.v2, self.v3 = v1, v2, v3 - - def __str__(self): - return f"({self.v1}, {self.v2}, {self.v3}))" - def __repr__(self): - return f"Triangle({self.v1}, {self.v2}, {self.v3})" + ) -> 'Triangle': + if isinstance(v0, tuple): + v0, v1, v2 = v0 + return np.array([v0, v1, v2]).view(cls) - @cached_property - def as_np(self) -> np.ndarray: - return np.array([self.v1, self.v2, self.v3]) - def __array__(self, *args, **kwrds) -> np.ndarray: - return self.as_np.__array__(*args, **kwrds) + def __str__(self) -> str: + return f"({self[0]}, {self[1]}, {self[2]})" + def __repr__(self) -> str: + return f"Vector3({self[0]}, {self[1]}, {self[2]})" class Ray: direction:Vector3 @@ -89,4 +39,4 @@ def __init__(self, origin:Vector3, ) -> None: self.direction = direction - self.origin = origin + self.origin = origin \ No newline at end of file diff --git a/CustomTypes/custom_types.pyi b/CustomTypes/custom_types.pyi new file mode 100644 index 0000000..04fac9a --- /dev/null +++ b/CustomTypes/custom_types.pyi @@ -0,0 +1,40 @@ +from typing import Any, Iterable, Iterator, Literal, TypeAlias, TypeVar, overload +import numpy as np +from . import CustomNP + +class Vector3(CustomNP[tuple[Literal[3]], np.dtype[np.float_]]): + @overload + def __new__(cls, x: float, y: float, z: float) -> Vector3: ... + @overload + def __new__(cls, x: tuple[float, float, float]) -> Vector3: ... + @overload + def __new__(cls, x: Vector3) -> Vector3: ... + + def __getitem__(self, index:Literal[0, 1, 2]) -> float: ... # type: ignore + +class Triangle(CustomNP[tuple[Literal[3], Literal[3]], np.dtype[np.float_]], Iterable[Vector3]): + @overload + def __new__(cls, v0: Vector3, v1: Vector3, v2: Vector3) -> Triangle: ... + @overload + def __new__(cls, v0: tuple[Vector3, Vector3, Vector3]) -> Triangle: ... + @overload + def __new__(cls, v0: Triangle) -> Triangle: ... + + @overload # type: ignore + def __getitem__(self, index:Literal[0, 1, 2]) -> Vector3: ... + @overload + def __getitem__(self, index:tuple[Literal[0, 1, 2], Literal[0, 1, 2]]) -> float: ... + + def __add__(self, other:Triangle) -> Triangle: ... # type: ignore + def __sub__(self, other:Triangle) -> Triangle: ... # type: ignore + + def __iter__(self) -> Iterator[Vector3]: ... +class Ray: + direction:Vector3 + origin:Vector3 + def __init__(self, + direction:Vector3, + origin:Vector3, + ) -> None: + self.direction = direction + self.origin = origin \ No newline at end of file diff --git a/Raytrace/Examples/ExampleMultipleSonarReadings.ipynb b/Raytrace/Examples/ExampleMultipleSonarReadings.ipynb index c30f15b..9d5f4e9 100644 --- a/Raytrace/Examples/ExampleMultipleSonarReadings.ipynb +++ b/Raytrace/Examples/ExampleMultipleSonarReadings.ipynb @@ -7,7 +7,7 @@ "outputs": [], "source": [ "import sys; sys.path.append('../..')\n", - "from CustomTypes import Ray, RayArray, Vector3, Vector3Array\n", + "from CustomTypes import Ray, Vector3, Vector3Array\n", "from Raytrace import BVHMesh\n", "from Raytrace.SideScan import SideScan, ScanReading\n", "import numpy as np\n", @@ -45,7 +45,7 @@ "# liniar interpolation between the two for this simple demo\n", "origins = Vector3Array([origin_start * (1-i) + origin_end * i for i in np.arange(0, 1+1/(readings_count-1)/2, 1/(readings_count-1))])\n", "\n", - "orientations = RayArray.from_components([facing] * readings_count, origins)\n", + "orientations = list(map(Ray, [facing] * readings_count, origins))\n", "\n", "rays_list = [SideScan.generate_rays(orientation, min_angle, max_angle, sample_ray_count) for orientation in orientations]\n", "\n", diff --git a/Raytrace/Meshs/__init__.py b/Raytrace/Meshs/__init__.py index a7cc2e6..cc039d9 100644 --- a/Raytrace/Meshs/__init__.py +++ b/Raytrace/Meshs/__init__.py @@ -1,3 +1,3 @@ -from .triangle_mesh import TriangleMesh -from .bvh_mesh import BVHMesh -from .composite_mesh import CompositeMesh \ No newline at end of file +from .triangle_mesh import TriangleMesh as TriangleMesh +from .bvh_mesh import BVHMesh as BVHMesh +from .composite_mesh import CompositeMesh as CompositeMesh \ No newline at end of file diff --git a/Raytrace/Meshs/bvh_mesh.py b/Raytrace/Meshs/bvh_mesh.py index ff9ebb6..6e70524 100644 --- a/Raytrace/Meshs/bvh_mesh.py +++ b/Raytrace/Meshs/bvh_mesh.py @@ -15,27 +15,28 @@ def __init__(self): def from_array(triangles:TriangleArray) -> 'BVHAABB': out = BVHAABB() if len(triangles) == 0: return out - tris = triangles.as_np.reshape((-1, 3)) - out.min_pos = Vector3(*tris.min(axis = 0)) - out.max_pos = Vector3(*tris.max(axis = 0)) + verts = triangles.reshape((-1, 3)).view(Vector3Array) + out.min_pos = verts.min(axis = 0).view(Vector3) + out.max_pos = verts.max(axis = 0).view(Vector3) + return out def raytrace(self, ray:Ray) -> float: old_error_state = np.seterr(divide='ignore') - t1 = (self.min_pos.as_np - ray.origin.as_np) / ray.direction.as_np - t2 = (self.max_pos.as_np - ray.origin.as_np) / ray.direction.as_np + t1 = (self.min_pos - ray.origin) / ray.direction + t2 = (self.max_pos - ray.origin) / ray.direction np.seterr(**old_error_state) t = np.stack([t1, t2]) tmin:float = t.min(axis=0).max() tmax:float = t.max(axis=0).min() return tmin if tmax >= tmin and tmax > 0 else np.inf def grow(self, point:Vector3): - self.min_pos = Vector3(*np.nanmin(np.array([point, self.min_pos]), axis=0)) - self.max_pos = Vector3(*np.nanmax(np.array([point, self.max_pos]), axis=0)) + self.min_pos = np.nanmin(np.array([point, self.min_pos]), axis=0).view(Vector3) + self.max_pos = np.nanmax(np.array([point, self.max_pos]), axis=0).view(Vector3) def grow_aabb(self, other:Self): self.grow(other.min_pos) self.grow(other.max_pos) def area(self) -> float: - e = self.max_pos.as_np - self.min_pos.as_np + e = self.max_pos - self.min_pos return e[0] * e[1] + e[1] * e[2] + e[2] * e[0] def __str__(self): return f'({float(self.min_pos[0])}, {float(self.min_pos[1])}, {float(self.min_pos[2])})#({float(self.max_pos[0])}, {float(self.max_pos[1])}, {float(self.max_pos[2])})' @@ -61,12 +62,12 @@ def __init__(self, *args, min_node_size = 2, **kwargs) -> None: self.build_BVH(min_node_size) def build_BVH(self, min_node_size = 100) -> None: # calculate triangle centroids for partitioning - self.centroids = Vector3Array(self.triangles.as_np.sum(axis = 1) / 3) + self.centroids = self.triangles.sum(axis = 1).view(Vector3Array) / 3 # assign all triangles to root node self.node_count = 1 self.root = root = BVHNode() root.start_index = 0 - root.tri_count = self.triangles.as_np.shape[0] + root.tri_count = len(self.triangles) if root.tri_count == 0: return root.update_bounds(self.triangles) # subdivide recursively @@ -81,11 +82,11 @@ def subdivide(self, node:BVHNode, min_node_size = 100) -> None: i = node.start_index j = i + node.tri_count - 1 while i <= j: - if self.centroids[i].as_np[axis] < split_pos: i += 1 + if self.centroids[i, axis] < split_pos: i += 1 else: - # Convoluted notation for a swap - self.triangles[i:i+1], self.triangles[j:j+1] = self.triangles[j:j+1], self.triangles[i:i+1] - self.centroids[i:i+1], self.centroids[j:j+1] = self.centroids[j:j+1], self.centroids[i:i+1] + self.triangles.nd[[i,j]] = self.triangles.nd[[j,i]] + self.centroids.nd[[i,j]] = self.centroids.nd[[j,i]] + j -= 1 # abort split if one of the sides is empty left_count = i - node.start_index @@ -136,25 +137,23 @@ def BVH_raytrace(self, ray:Ray) -> float: if np.isfinite(dist2): stack.append(child2) return best - def find_best_split(self, node:BVHNode) -> tuple[float, int, float]: + def find_best_split(self, node:BVHNode) -> tuple[float, Literal[0, 1, 2], float]: BINS = 8 - axis:int = -1 + axis:Literal[0, 1, 2] = 0 split_pos:float = 0 best_cost:float = np.inf triangles:TriangleArray = node.get_subarray(self.triangles) centroids:Vector3Array = node.get_subarray(self.centroids) - for axis_i in range(3): - bounds_min = float(np.min(centroids.as_np[:,axis_i])) - bounds_max = float(np.max(centroids.as_np[:,axis_i])) + for axis_i in (0, 1, 2): + bounds_min = float(np.min(centroids.nd[:,axis_i])) + bounds_max = float(np.max(centroids.nd[:,axis_i])) if bounds_min == bounds_max: continue # populate the bins scale:float = BINS / (bounds_max - bounds_min) - bin_idx = ((centroids.as_np[:,axis_i] - bounds_min) * scale).astype(int) + bin_idx = ((centroids.nd[:,axis_i] - bounds_min) * scale).astype(int) bin_idx[bin_idx > BINS - 1] = BINS - 1 - bin_bounds = [BVHAABB.from_array( - TriangleArray([t for t, b in zip(triangles, bin_idx) if b == n]) # triangles[bin_idx == n] - ) for n in range(BINS)] + bin_bounds = [BVHAABB.from_array(triangles[bin_idx == n]) for n in range(BINS)] bin_counts = [np.count_nonzero(bin_idx == n) for n in range(BINS)] # gather data for the 7 planes between the 8 bins diff --git a/Raytrace/Meshs/composite_mesh.py b/Raytrace/Meshs/composite_mesh.py index 3447f56..3ea68a7 100644 --- a/Raytrace/Meshs/composite_mesh.py +++ b/Raytrace/Meshs/composite_mesh.py @@ -6,7 +6,7 @@ class CompositeMesh(TriangleMesh): meshes: list[TriangleMesh] def __init__(self, meshes:list[TriangleMesh]) -> None: self.meshes = meshes - self.triangles = sum([mesh.triangles for mesh in self.meshes], start = TriangleArray()) + self.triangles = np.concatenate([mesh.triangles for mesh in self.meshes]).view(TriangleArray) def raytrace(self, ray:Ray) -> float: distances = [mesh.raytrace(ray) for mesh in self.meshes] return min(distances, default = np.inf) \ No newline at end of file diff --git a/Raytrace/Meshs/triangle_mesh.py b/Raytrace/Meshs/triangle_mesh.py index 452ea44..acf3664 100644 --- a/Raytrace/Meshs/triangle_mesh.py +++ b/Raytrace/Meshs/triangle_mesh.py @@ -1,7 +1,7 @@ from typing import Any import numpy as np import pickle -from CustomTypes import Triangle, TriangleArray, Ray +from CustomTypes import Triangle, TriangleArray, Ray, Vector3, Vector3Array, FloatArray import stl class TriangleMesh: @@ -44,30 +44,30 @@ def array_raytrace(self, ray:Ray) -> float: def triangle_ray_intersection(triangle:Triangle, ray:Ray, epsilon = 1e-10) -> float: # Translated from https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm#C++_implementation - v0, v1, v2 = triangle.as_np + v0, v1, v2 = triangle edge1 = v1 - v0 edge2 = v2 - v0 - ray_cross_e2 = np.cross(ray.direction.as_np, edge2) - det = np.dot(edge1, ray_cross_e2) + ray_cross_e2 = np.cross(ray.direction, edge2).view(Vector3) + det:float = np.dot(edge1, ray_cross_e2) if -epsilon < det < epsilon: return -1 # This ray is parallel to this triangle. inv_det = 1.0 / det s = ray.origin - v0 - u = inv_det * np.dot(s, ray_cross_e2) + u:float = inv_det * np.dot(s, ray_cross_e2) if u < -epsilon or 1 < u: return -1 - s_cross_e1 = np.cross(s, edge1) - v = inv_det * np.dot(ray.direction.as_np, s_cross_e1) + s_cross_e1 = np.cross(s, edge1).view(Vector3) + v:float = inv_det * np.dot(ray.direction, s_cross_e1) if v < -epsilon or 1 < u + v: return -1 # At this stage we can compute t to find out where the intersection point is on the line. - t = inv_det * np.dot(edge2, s_cross_e1) + t:float = inv_det * np.dot(edge2, s_cross_e1) if t > epsilon: # ray intersection return t @@ -76,29 +76,26 @@ def triangle_ray_intersection(triangle:Triangle, ray:Ray, epsilon = 1e-10) -> fl return -1 @staticmethod def batch_triangle_ray_intersection(triangle_array:TriangleArray, ray:Ray, epsilon = 1e-10) -> np.ndarray[np.floating, Any]: - vecdot = lambda a, b : np.sum(a*b, axis=-1) + def vecdot(a:Vector3Array|Vector3, b:Vector3Array) -> FloatArray: return np.sum(a*b, axis=-1).view(FloatArray) # Translated from https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm#C++_implementation - v0a, v1a, v2a = triangle_array.verticies - v0:np.ndarray = v0a.as_np - v1:np.ndarray = v1a.as_np - v2:np.ndarray = v2a.as_np + v0, v1, v2 = triangle_array.verticies # np.subtract(v1, v0, out= v1) # np.subtract(v2, v0, out= v2) v1 = v1 - v0 v2 = v2 - v0 - ray_cross_e2 = np.cross(ray.direction.as_np, v2) + ray_cross_e2 = np.cross(ray.direction, v2).view(Vector3Array) det = vecdot(v1, ray_cross_e2) # if -epsilon < det < epsilon: # return None # This ray is parallel to this triangle. old_error_state = np.seterr(divide='ignore', invalid='ignore') inv_det = 1.0 / det - s = ray.origin - v0 + s = (ray.origin - v0).view(Vector3Array) u = inv_det * vecdot(s, ray_cross_e2) - s_cross_e1 = np.cross(s, v1) + s_cross_e1 = np.cross(s, v1).view(Vector3Array) v = inv_det * vecdot(ray.direction, s_cross_e1) valid = u < -epsilon @@ -107,7 +104,7 @@ def batch_triangle_ray_intersection(triangle_array:TriangleArray, ray:Ray, epsil np.logical_or(valid, 1 < u + v, out=valid) np.invert(valid, out=valid) - t = np.empty(triangle_array.as_np.shape[0], dtype=np.float64) + t = np.empty(len(triangle_array), dtype=float) t.fill(-1) np.multiply(inv_det, vecdot(v2, s_cross_e1), out = t, where = valid) diff --git a/Raytrace/PlotRays.py b/Raytrace/PlotRays.py index 8099adf..a28dccc 100644 --- a/Raytrace/PlotRays.py +++ b/Raytrace/PlotRays.py @@ -8,7 +8,7 @@ exit() def plot_mesh(mesh:TriangleMesh, **kwargs) -> go.Mesh3d: - x, y, z = mesh.triangles.as_np.reshape((-1,3)).swapaxes(0, 1) + x, y, z = mesh.triangles.reshape((-1,3)).swapaxes(0, 1) i, j, k = [list(range(i,x.shape[0],3)) for i in range(3)] return go.Mesh3d(x=x, y=y, z=z, i=i, j=j, k=k, **kwargs) diff --git a/Raytrace/SideScan.py b/Raytrace/SideScan.py index 9d73542..1900053 100644 --- a/Raytrace/SideScan.py +++ b/Raytrace/SideScan.py @@ -1,5 +1,5 @@ from . import TriangleMesh -from CustomTypes import Ray, Vector3, RayArray, Vector3Array +from CustomTypes import Ray, Vector3, Vector3Array from typing import Self import numpy as np import time @@ -16,7 +16,7 @@ class RawScanReading: start_time:float = -1 end_time:float = -1 - def __init__(self, distances:np.ndarray, rays:RayArray): + def __init__(self, distances:np.ndarray, rays:list[Ray]): old_error_state = np.seterr(all='ignore') self.distances = distances self.origins = np.array([r.origin for r in rays]) @@ -35,7 +35,7 @@ def combine_min(self, other:Self) -> Self: new_distances[other_smaller] = other.distances[other_smaller] # HACK: new rays should be generated based on which distance was closer - new_rays = RayArray.from_components(self.directions, self.origins) + new_rays = list(map(Ray, self.directions, self.origins)) return type(self)(new_distances, new_rays) def print_summary(self) -> None: @@ -112,7 +112,7 @@ def __init__(self, mesh:TriangleMesh, smooth_dist:float = 0.05, result_reselutio self.mesh = mesh self.smooth_dist = smooth_dist self.result_reselution = result_reselution - def scan_rays(self, rays:RayArray) -> RawScanReading: + def scan_rays(self, rays:list[Ray]) -> RawScanReading: distances = np.empty((len(rays),), np.float32) start_time = time.time() @@ -127,18 +127,18 @@ def scan_rays(self, rays:RayArray) -> RawScanReading: return out @staticmethod - def generate_rays(orientation:Ray, min_angle:float, max_angle:float, angle_reselution:int) -> RayArray: + def generate_rays(orientation:Ray, min_angle:float, max_angle:float, angle_reselution:int) -> list[Ray]: angle_step = (max_angle-min_angle)/(angle_reselution-1) angles = np.arange(min_angle, max_angle + angle_step/2, angle_step) origin = orientation.origin - pitch = np.arctan2(orientation.direction.as_np[2],np.sqrt(np.dot(orientation.direction.as_np[0:2], orientation.direction.as_np[0:2]))) - yaw = np.arctan2(orientation.direction.as_np[1], orientation.direction.as_np[0]) # yaw = 0 => facing due +x + pitch:float = np.arctan2(orientation.direction[2],np.sqrt(np.dot(orientation.direction.nd[0:2], orientation.direction.nd[0:2]))) + yaw:float = np.arctan2(orientation.direction[1], orientation.direction[0]) # yaw = 0 => facing due +x # Precomputed rotation matrix sin_comp = np.array([-np.sin(pitch) * np.cos(yaw), np.sin(pitch) * np.sin(yaw), np.cos(pitch)]) cos_comp = np.array([np.sin(yaw), np.cos(yaw), 0]) directions = Vector3Array(np.array([np.sin(a) * sin_comp + np.cos(a) * cos_comp for a in angles])) - return RayArray.from_components(directions, [origin]*angle_reselution) + return list(map(Ray, directions, [origin]*angle_reselution)) \ No newline at end of file diff --git a/Raytrace/__init__.py b/Raytrace/__init__.py index 6827f80..9954358 100644 --- a/Raytrace/__init__.py +++ b/Raytrace/__init__.py @@ -1,5 +1,5 @@ from .Meshs import ( - TriangleMesh, - BVHMesh, - CompositeMesh, + TriangleMesh as TriangleMesh, + BVHMesh as BVHMesh, + CompositeMesh as CompositeMesh, ) \ No newline at end of file