Skip to content

Commit ce18542

Browse files
authored
Various improvements (#100)
* various improvements * fix test * fix test again * add test
1 parent 211c86f commit ce18542

File tree

7 files changed

+50
-32
lines changed

7 files changed

+50
-32
lines changed

pylinalg/matrix.py

+14-13
Original file line numberDiff line numberDiff line change
@@ -737,8 +737,7 @@ def _mat_inv(m) -> np.ndarray:
737737
det = n11 * t11 + n21 * t12 + n31 * t13 + n41 * t14
738738

739739
if det == 0:
740-
out.fill(0)
741-
return out # singular matrix
740+
raise np.linalg.LinAlgError("Singular matrix")
742741

743742
det_inv = 1 / det
744743

@@ -853,11 +852,11 @@ def _mat_inv(m) -> np.ndarray:
853852
if int(np.__version__.split(".")[0]) >= 2:
854853
_default_mat_inv_method = "numpy"
855854
else:
856-
_default_mat_inv_method = "manual"
855+
_default_mat_inv_method = "python"
857856

858857

859858
def mat_inverse(
860-
matrix, /, *, method=_default_mat_inv_method, dtype=None, out=None
859+
matrix, /, *, method=_default_mat_inv_method, raise_err=False, dtype=None, out=None
861860
) -> np.ndarray:
862861
"""
863862
Compute the inverse of a matrix.
@@ -868,7 +867,9 @@ def mat_inverse(
868867
The matrix to invert.
869868
method : str, optional
870869
The method to use for inversion. The default is "numpy" when
871-
numpy version is 2.0.0 or newer, otherwise "manual".
870+
numpy version is 2.0.0 or newer, otherwise "python".
871+
raise_err : bool, optional
872+
Raise a ValueError if the matrix is singular. Default is False.
872873
dtype : data-type, optional
873874
Overrides the data type of the result.
874875
out : ndarray, optional
@@ -886,11 +887,11 @@ def mat_inverse(
886887
-----
887888
The default method is "numpy" when numpy version >= 2.0.0,
888889
which uses the `numpy.linalg.inv` function.
889-
The alternative method is "manual", which uses a manual implementation of
890-
the inversion algorithm. The manual method is used to avoid a performance
890+
The alternative method is "python", which uses a pure python implementation of
891+
the inversion algorithm. The python method is used to avoid a performance
891892
issue with `numpy.linalg.inv` on some platforms when numpy version < 2.0.0.
892893
See: https://github.com/pygfx/pygfx/issues/763
893-
The manual method is slower than the numpy method, but it is guaranteed to work.
894+
The python method is slower than the numpy method, but it is guaranteed to work.
894895
895896
When the matrix is singular, it will return a matrix filled with zeros,
896897
This is a common behavior in real-time graphics applications.
@@ -899,18 +900,18 @@ def mat_inverse(
899900

900901
fn = {
901902
"numpy": np.linalg.inv,
902-
"manual": _mat_inv,
903+
"python": _mat_inv,
903904
}[method]
904905

905906
matrix = np.asarray(matrix)
906907
try:
907908
inverse = fn(matrix)
908-
except np.linalg.LinAlgError:
909+
except np.linalg.LinAlgError as err:
910+
if raise_err:
911+
raise ValueError("The provided matrix is not invertible.") from err
909912
inverse = np.zeros_like(matrix, dtype=dtype)
910913
if out is None:
911-
if dtype is not None:
912-
return inverse.astype(dtype, copy=False)
913-
return inverse
914+
return np.asarray(inverse, dtype=dtype)
914915
else:
915916
out[:] = inverse
916917
return out

pylinalg/misc.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ def quat_to_axis_angle(quaternion, /, *, out=None, dtype=None) -> np.ndarray:
131131
quaternion = np.asarray(quaternion)
132132

133133
if out is None:
134-
quaternion = quaternion.astype(dtype)
134+
quaternion = quaternion.astype(dtype, copy=False)
135135
out = (
136136
quaternion[..., :3] / np.sqrt(1 - quaternion[..., 3] ** 2),
137137
2 * np.arccos(quaternion[..., 3]),

pylinalg/vector.py

+14-7
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,9 @@ def vec_transform(vectors, matrix, /, *, w=1, out=None, dtype=None) -> np.ndarra
115115
return out
116116

117117

118-
def vec_unproject(vector, matrix, /, *, depth=0, out=None, dtype=None) -> np.ndarray:
118+
def vec_unproject(
119+
vector, matrix, /, *, matrix_is_inv=False, depth=0, out=None, dtype=None
120+
) -> np.ndarray:
119121
"""
120122
Un-project a vector from 2D space to 3D space.
121123
@@ -130,6 +132,9 @@ def vec_unproject(vector, matrix, /, *, depth=0, out=None, dtype=None) -> np.nda
130132
The vector to be un-projected.
131133
matrix: ndarray, [4, 4]
132134
The camera's intrinsic matrix.
135+
matrix_is_inv: bool, optional
136+
Default is False. If True, the provided matrix is assumed to be the
137+
inverse of the camera's intrinsic matrix.
133138
depth : number, optional
134139
The distance of the unprojected vector from the camera.
135140
out : ndarray, optional
@@ -162,10 +167,12 @@ def vec_unproject(vector, matrix, /, *, depth=0, out=None, dtype=None) -> np.nda
162167
if out is None:
163168
out = np.empty((*result_shape, 3), dtype=dtype)
164169

165-
try:
166-
inverse_projection = np.linalg.inv(matrix)
167-
except np.linalg.LinAlgError as err:
168-
raise ValueError("The provided matrix is not invertible.") from err
170+
if matrix_is_inv:
171+
inverse_projection = matrix
172+
else:
173+
from .matrix import mat_inverse
174+
175+
inverse_projection = mat_inverse(matrix, raise_err=True)
169176

170177
vector_hom = np.empty((*result_shape, 4), dtype=dtype)
171178
vector_hom[..., 2] = depth
@@ -295,7 +302,7 @@ def vec_dist(vector_a, vector_b, /, *, out=None, dtype=None) -> np.ndarray:
295302

296303
shape = vector_a.shape[:-1]
297304
if out is None:
298-
out = np.linalg.norm(vector_a - vector_b, axis=-1).astype(dtype)
305+
out = np.linalg.norm(vector_a - vector_b, axis=-1).astype(dtype, copy=False)
299306
elif len(shape) >= 0:
300307
out[:] = np.linalg.norm(vector_a - vector_b, axis=-1)
301308
else:
@@ -346,7 +353,7 @@ def vec_angle(vector_a, vector_b, /, *, out=None, dtype=None) -> np.ndarray:
346353
)
347354

348355
if out is None:
349-
out = np.arccos(the_cos).astype(dtype)
356+
out = np.arccos(the_cos).astype(dtype, copy=False)
350357
elif len(shape) >= 0:
351358
out[:] = np.arccos(the_cos)
352359
else:

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[project]
44
name = "pylinalg"
5-
version = "0.6.2"
5+
version = "0.6.3"
66
description = "Linear algebra utilities for Python"
77
readme = "README.md"
88
license = { file = "LICENSE" }

tests/test_matrix.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -385,15 +385,15 @@ def test_mat_euler_vs_scipy():
385385
def test_mat_inverse():
386386
a = la.mat_from_translation([1, 2, 3])
387387
np_inv = la.mat_inverse(a, method="numpy")
388-
manual_inv = la.mat_inverse(a, method="manual")
389-
npt.assert_array_equal(np_inv, manual_inv)
388+
python_inv = la.mat_inverse(a, method="python")
389+
npt.assert_array_equal(np_inv, python_inv)
390390

391391
# test for singular matrix
392392
b = la.mat_from_scale([0, 2, 3])
393393
np_inv = la.mat_inverse(b, method="numpy")
394-
manual_inv = la.mat_inverse(b, method="manual")
394+
python_inv = la.mat_inverse(b, method="python")
395395
npt.assert_array_equal(np_inv, np.zeros((4, 4)))
396-
npt.assert_array_equal(manual_inv, np.zeros((4, 4)))
396+
npt.assert_array_equal(python_inv, np.zeros((4, 4)))
397397

398398

399399
def test_mat_has_shear():

tests/test_pylinalg.py

+6-6
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ def popattr(key):
5858
# confirm the signature of each callable
5959
sig = inspect.signature(getattr(la, key))
6060

61-
assert (
62-
sig.return_annotation is not inspect.Signature.empty
63-
), f"Missing return annotation on {key}"
61+
assert sig.return_annotation is not inspect.Signature.empty, (
62+
f"Missing return annotation on {key}"
63+
)
6464
if sig.return_annotation is bool:
6565
key_parts = key.split("_")
6666
assert key_parts[1] in ("is", "has")
@@ -77,9 +77,9 @@ def popattr(key):
7777
assert param.KEYWORD_ONLY
7878
has_out = True
7979
assert has_out, f"Function {key} does not have 'out' keyword-only argument"
80-
assert (
81-
has_dtype
82-
), f"Function {key} does not have 'dtype' keyword-only argument"
80+
assert has_dtype, (
81+
f"Function {key} does not have 'dtype' keyword-only argument"
82+
)
8383

8484
# assert that all callables are available in __all__
8585
assert set(__all__) == set(callables)

tests/test_vectors.py

+10
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,16 @@ def test_vec_unproject_exceptions():
303303
la.vec_unproject(vector, matrix)
304304

305305

306+
def test_vec_unproject_is_inverse():
307+
a = la.mat_perspective(-1, 1, -1, 1, 1, 100)
308+
a_inv = la.mat_inverse(a)
309+
vecs = np.array([[1, 2], [4, 5], [7, 8]])
310+
311+
expected = la.vec_unproject(vecs, a)
312+
actual = la.vec_unproject(vecs, a_inv, matrix_is_inv=True)
313+
npt.assert_array_equal(expected, actual)
314+
315+
306316
def test_vector_apply_rotation_ordered():
307317
"""Test that a positive pi/2 rotation about the z-axis and then the y-axis
308318
results in a different output then in standard rotation ordering."""

0 commit comments

Comments
 (0)