Skip to content

Commit

Permalink
Typing Overhaul (#3)
Browse files Browse the repository at this point in the history
* Added Custom Types

Everything runs abut half as fast but it is much less error prone and more readable

* Changed Raytrace Imports

Change the Raytrace library so that mesh classes can be imported directly

* Restructured Raytrace Library

Moved the mesh classes into their own folder

* Fixed Typo Related Bug

Passing a Vector3 to the Vector3 constructor no longer causes an error

* 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)
  • Loading branch information
jrb20008 authored Mar 25, 2025
1 parent 0405a26 commit fd0b115
Show file tree
Hide file tree
Showing 18 changed files with 351 additions and 102 deletions.
27 changes: 14 additions & 13 deletions AUVSim/auv.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
from Raytrace.TriangleMesh import TriangleMesh, Ray
from Raytrace import TriangleMesh
from Raytrace.SideScan import SideScan
import numpy as np
from typing import Any
from CustomTypes import Ray, Vector3, Vector3Array

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) -> list[Ray]:
return list(map(Ray, self.facings, self.positions))

class AUVPathGen:
mesh:TriangleMesh
Expand Down Expand Up @@ -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)
return AUVPath(Vector3Array(positions), Vector3Array(facings))
1 change: 1 addition & 0 deletions CustomTypes/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pycache__/
11 changes: 11 additions & 0 deletions CustomTypes/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from .custom_np import CustomNP as CustomNP
from .custom_types import (
Vector3 as Vector3,
Triangle as Triangle,
Ray as Ray,
)
from .custom_arrays import (
FloatArray as FloatArray,
TriangleArray as TriangleArray,
Vector3Array as Vector3Array,
)
54 changes: 54 additions & 0 deletions CustomTypes/custom_arrays.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from typing import Any, Iterable, Literal, overload
import numpy as np

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(CustomNP):
def __new__(cls,
content: Iterable[Vector3]|np.ndarray|None = None,
):
if isinstance(content, np.ndarray):
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(CustomNP):
def __new__(cls,
content: Iterable[Triangle]|np.ndarray|None = None,
):
if isinstance(content, np.ndarray):
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.swapaxes(0, 1)
return (v0.view(Vector3Array), v1.view(Vector3Array), v2.view(Vector3Array))
51 changes: 51 additions & 0 deletions CustomTypes/custom_arrays.pyi
Original file line number Diff line number Diff line change
@@ -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]: ...

21 changes: 21 additions & 0 deletions CustomTypes/custom_np.py
Original file line number Diff line number Diff line change
@@ -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)
15 changes: 15 additions & 0 deletions CustomTypes/custom_np.pyi
Original file line number Diff line number Diff line change
@@ -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: ...
42 changes: 42 additions & 0 deletions CustomTypes/custom_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from typing import Any, Iterable, Literal, Self, overload
import numpy as np
from . import CustomNP

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):
x, y, z = x
assert isinstance(x, (float, int))
return np.array([x, y, z]).view(cls)
def __str__(self):
return f"({self[0]}, {self[1]}, {self[2]})"
def __repr__(self):
return f"Vector3({self[0]}, {self[1]}, {self[2]})"

class Triangle(CustomNP):
def __new__(cls,
v0: Vector3 | tuple[Vector3, Vector3, Vector3],
v1: Vector3 = Vector3(0, 0, 0),
v2: Vector3 = Vector3(0, 0, 0),
) -> 'Triangle':
if isinstance(v0, tuple):
v0, v1, v2 = v0
return np.array([v0, v1, v2]).view(cls)

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
origin:Vector3
def __init__(self,
direction:Vector3,
origin:Vector3,
) -> None:
self.direction = direction
self.origin = origin
40 changes: 40 additions & 0 deletions CustomTypes/custom_types.pyi
Original file line number Diff line number Diff line change
@@ -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
16 changes: 8 additions & 8 deletions Raytrace/Examples/ExampleMultipleSonarReadings.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
"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 Raytrace.BVHMesh import BVHMesh\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",
"import time"
Expand All @@ -34,18 +34,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 = 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",
Expand Down
15 changes: 8 additions & 7 deletions Raytrace/Examples/ExampleSingleSonarReading.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -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 Raytrace.BVHMesh import BVHMesh\n",
"from CustomTypes import Ray\n",
"from Raytrace import BVHMesh\n",
"from Raytrace.SideScan import SideScan, ScanReading\n",
"import numpy as np\n",
"import time"
"import time\n",
"from CustomTypes import Vector3"
]
},
{
Expand All @@ -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",
Expand All @@ -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()"
]
},
Expand Down
3 changes: 3 additions & 0 deletions Raytrace/Meshs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .triangle_mesh import TriangleMesh as TriangleMesh
from .bvh_mesh import BVHMesh as BVHMesh
from .composite_mesh import CompositeMesh as CompositeMesh
Loading

0 comments on commit fd0b115

Please sign in to comment.