-
Notifications
You must be signed in to change notification settings - Fork 51
Expand file tree
/
Copy pathmath.py
More file actions
468 lines (346 loc) · 13.6 KB
/
math.py
File metadata and controls
468 lines (346 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
import math
import warnings
from functools import wraps
from itertools import compress
from typing import List, Optional, Tuple, Union
import numpy as np
from deprecated import deprecated
from scipy import ndimage
from scipy.spatial.transform import Rotation
from kwave import __version__
from kwave.data import Vector
@deprecated(
version="0.4.1",
reason="Use scipy.spatial.transform.Rotation.from_euler('x', angle, degrees=True).as_matrix() instead",
)
def Rx(theta: float) -> np.ndarray:
"""Create a rotation matrix for rotation about the x-axis.
Args:
theta: Rotation angle in degrees
Returns:
3x3 rotation matrix
"""
return Rotation.from_euler("x", theta, degrees=True).as_matrix()
@deprecated(
version="0.4.1",
reason="Use scipy.spatial.transform.Rotation.from_euler('y', angle, degrees=True).as_matrix() instead",
)
def Ry(theta: float) -> np.ndarray:
"""Create a rotation matrix for rotation about the y-axis.
Args:
theta: Rotation angle in degrees
Returns:
3x3 rotation matrix
"""
return Rotation.from_euler("y", theta, degrees=True).as_matrix()
@deprecated(
version="0.4.1",
reason="Use scipy.spatial.transform.Rotation.from_euler('z', angle, degrees=True).as_matrix() instead",
)
def Rz(theta: float) -> np.ndarray:
"""Create a rotation matrix for rotation about the z-axis.
Args:
theta: Rotation angle in degrees
Returns:
3x3 rotation matrix
"""
return Rotation.from_euler("z", theta, degrees=True).as_matrix()
@deprecated(
version="0.4.1",
reason="Use make_affine() instead. It provides the same functionality with a clearer name and better documentation.",
)
def get_affine_matrix(translation: Vector, rotation: Union[float, List[float]], seq: str = "xyz") -> np.ndarray:
return make_affine(translation, rotation, seq)
def make_affine(translation: Vector, rotation: Union[float, List[float]], seq: str = "xyz") -> np.ndarray:
"""
Create an affine transformation matrix combining rotation and translation.
Uses scipy.spatial.transform.Rotation internally.
Args:
translation: [dx, dy] or [dx, dy, dz]
rotation: Single angle (degrees) for 2D or list of angles for 3D
seq: Rotation sequence for 3D (default: 'xyz')
Returns:
3x3 (2D) or 4x4 (3D) affine transformation matrix
Examples:
# 2D transform (rotation around z-axis)
T1 = make_affine([1, 2], 45)
# 3D transform with xyz Euler angles
T2 = make_affine([1, 2, 3], [45, 30, 60])
# 3D transform with custom sequence
T3 = make_affine([1, 2, 3], [45, 30], 'xy')
"""
if len(translation) == 2:
# 2D transformation
R = Rotation.from_euler("z", rotation, degrees=True).as_matrix()[:2, :2]
T = np.eye(3)
T[:2, :2] = R
T[:2, 2] = translation
return T
else:
# 3D transformation
R = Rotation.from_euler(seq, rotation, degrees=True)
T = np.eye(4)
T[:3, :3] = R.as_matrix()
T[:3, 3] = translation
return T
def cosd(angle_in_degrees):
"""Compute cosine of angle in degrees."""
return np.cos(np.radians(angle_in_degrees))
def sind(angle_in_degrees):
"""Compute sine of angle in degrees."""
return np.sin(np.radians(angle_in_degrees))
def largest_prime_factor(n: int) -> int:
"""
Finds the largest prime factor of a positive integer.
Args:
n: The positive integer to be factored.
Returns:
The largest prime factor of n.
"""
i = 2
while i * i <= n:
if n % i:
i += 1
else:
n //= i
return n
def rwh_primes(n: int) -> List[int]:
"""
Generates a list of prime numbers less than a given integer.
Args:
n: The upper bound for the list of primes.
Returns:
A list of prime numbers less than n.
"""
sieve = bytearray([True]) * (n // 2 + 1)
for i in range(1, int(n**0.5) // 2 + 1):
if sieve[i]:
sieve[2 * i * (i + 1) :: 2 * i + 1] = bytearray((n // 2 - 2 * i * (i + 1)) // (2 * i + 1) + 1)
return [2, *compress(range(3, n, 2), sieve[1:])]
def primefactors(n: int) -> List[int]:
"""
Finds the prime factors of a given integer.
Args:
n: The integer to factor.
Returns:
A list of prime factors of n.
"""
factors = []
while n % 2 == 0:
(factors.append(2),)
n = n / 2
# n became odd
for i in range(3, int(math.sqrt(n)) + 1, 2):
while n % i == 0:
factors.append(i)
n = n / i
if n > 2:
factors.append(n)
return factors
def next_pow2(n: int) -> int:
"""
Calculate the next power of 2 that is greater than or equal to `n`.
This function takes a positive integer `n` and returns the smallest power of 2 that is greater
than or equal to `n`.
Args:
n: The number to find the next power of 2 for.
Returns:
The smallest power of 2 that is greater than or equal to `n`.
"""
# decrement `n` (to handle cases when `n` itself is a power of 2)
n = n - 1
# set all bits after the last set bit
n |= n >> 1
n |= n >> 2
n |= n >> 4
n |= n >> 8
n |= n >> 16
# increment `n` and return
return np.log2(n + 1)
def phase_shift_interpolate(data: np.ndarray, shift: float, shift_dim: Optional[int] = None) -> np.ndarray:
"""
Interpolates array data using phase shifts in the Fourier domain.
This function resamples the input data along the specified dimension using a
regular grid that is offset by the non-dimensional distance shift.
The resampling is performed using a Fourier interpolant.
This can be used to shift the acoustic particle velocity recorded by the
first-order simulation functions to the regular (non-staggered) temporal
grid by setting shift to 1/2.
Example:
# Move velocity data from staggered to regular grid points
v_regular = phase_shift_interpolate(v_staggered, shift=0.5)
Args:
data: The input array to be interpolated.
shift: Non-dimensional shift amount, where:
0 = no shift
1/2 = shift for staggered grid
1 = full grid point
shift_dim: The dimension along which to apply the phase shift.
Default is highest non-singleton dimension.
Returns:
The interpolated array after applying the phase shift.
"""
# Handle dimension selection (matching MATLAB behavior)
if shift_dim is None:
# Find highest non-singleton dimension
shift_dim = data.ndim - 1
if data.ndim == 2 and data.shape[1] == 1:
shift_dim = 0
else:
shift_dim = shift_dim - 1
if not (0 <= shift_dim <= 3):
raise ValueError("Input dim must be 0, 1, 2 or 3.")
elif shift_dim >= data.ndim:
warnings.warn(f"Shift dimension {shift_dim} is greater than the number of dimensions in the input array {data.ndim}.")
shift_dim = data.ndim - 1
# Create shift array with zeros except for the shift dimension
shifts = np.zeros(data.ndim)
shifts[shift_dim] = shift
# Take FFT of input data
fft_data = np.fft.fft(data, axis=shift_dim)
# Apply fourier shift (scipy expects input in Fourier domain)
# Note: scipy.ndimage.fourier_shift applies the shift in the opposite direction
# compared to MATLAB, so we negate the shift
shifted_data = ndimage.fourier_shift(fft_data, -shifts)
# Return to spatial domain, ensuring real output
return np.real(np.fft.ifft(shifted_data, axis=shift_dim))
@deprecated(
version="0.4.1",
reason="This function has been renamed to phase_shift_interpolate() to better reflect its functionality.",
)
def fourier_shift(data: np.ndarray, shift: float, shift_dim: Optional[int] = None) -> np.ndarray:
"""Wrapper for phase_shift_interpolate. See its documentation for details."""
return phase_shift_interpolate(data, shift, shift_dim)
def round_even(x):
"""
Rounds to the nearest even integer.
Args:
x: Input value
Returns:
Nearest even integer.
"""
return 2 * round(x / 2)
def round_odd(x):
"""
Rounds to the nearest odd integer.
Args:
x: Input value
Returns:
Nearest odd integer.
"""
return 2 * round((x + 1) / 2) - 1
def find_closest(A: np.ndarray, a: Union[float, int]) -> Tuple[Union[float, int], Tuple[int, ...]]:
"""
Returns the value and index of the item in A that is closest to the value a.
This function finds the value and index of the item in the input array A that is closest to the given value a.
For vectors, the value and index correspond to the closest element in A. For matrices, value and index are row
vectors corresponding to the closest element from each column. For N-D arrays, the function finds the closest
value along the first matrix dimension (singleton dimensions are removed before the search). If there is more
than one element with the closest value, the index of the first one is returned.
Args:
A: The array to search.
a: The value to find.
Returns:
A tuple containing the value and index of the closest element in A to a.
"""
assert isinstance(A, np.ndarray), "A must be an np.array"
idx = np.unravel_index(np.argmin(abs(A - a)), A.shape)
return A[idx], idx
def sinc(x: Union[int, float, np.ndarray]) -> Union[int, float, np.ndarray]:
"""
Calculates the sinc function of a given value or array of values.
Args:
x: The value or array of values for which to calculate the sinc function.
Returns:
The sinc function of x.
"""
return np.sinc(x / np.pi)
def gaussian(
x: Union[int, float, np.ndarray],
magnitude: Optional[Union[int, float]] = None,
mean: Optional[float] = 0,
variance: Optional[float] = 1,
) -> Union[int, float, np.ndarray]:
"""
Returns a Gaussian distribution f(x) with the specified magnitude, mean, and variance. If these values are not specified,
the magnitude is normalised and values of variance = 1 and mean = 0 are used. For example running:
import matplotlib.pyplot as plt
x = np.arange(-3, 0.05, 3)
plt.plot(x, gaussian(x))
will plot a normalised Gaussian distribution.
Note, the full width at half maximum of the resulting distribution can be calculated by FWHM = 2 * sqrt(2 * log(2) * variance).
Args:
x: The input values.
magnitude: Bell height. Defaults to normalised.
mean: Mean or expected value. Defaults to 0.
variance: Variance, or bell width. Defaults to 1.
Returns:
A Gaussian distribution.
"""
if magnitude is None:
magnitude = (2 * math.pi * variance) ** -0.5
gauss_distr = magnitude * np.exp(-((x - mean) ** 2) / (2 * variance))
return gauss_distr
def _compute_direction(start_pos: np.ndarray, end_pos: np.ndarray) -> Tuple[np.ndarray, float]:
"""Compute normalized direction vector and magnitude between two points."""
direction = np.asarray(end_pos) - np.asarray(start_pos)
magnitude = np.linalg.norm(direction)
direction = direction / magnitude
return direction, magnitude
def _compute_rotation_axis(reference: np.ndarray, direction: np.ndarray) -> Tuple[np.ndarray, float]:
"""Compute normalized rotation axis and its magnitude."""
axis = np.cross(reference, direction)
axis_norm = np.linalg.norm(axis)
return axis, axis_norm
def _create_rotation_matrix(axis: np.ndarray, angle: float) -> np.ndarray:
"""Create rotation matrix using Rodrigues' formula."""
cos_theta = np.cos(angle)
sin_theta = np.sin(angle)
# Skew-symmetric matrix of axis
skew = np.array([[0, -axis[2], axis[1]], [axis[2], 0, -axis[0]], [-axis[1], axis[0], 0]])
# Outer product
outer = np.outer(axis, axis)
return cos_theta * np.eye(3) + sin_theta * skew + (1 - cos_theta) * outer
def compute_rotation_between_vectors(start_pos: np.ndarray, end_pos: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
"""Compute rotation matrix between two 3D points.
Args:
start_pos: Starting position vector
end_pos: Ending position vector
Returns:
Tuple containing:
- 3x3 rotation matrix
- Normalized direction vector
"""
direction, magnitude = _compute_direction(start_pos, end_pos)
if np.isclose(magnitude, 0):
return np.eye(3), np.zeros(3)
reference = np.array([0.0, 0.0, -1.0])
axis, axis_norm = _compute_rotation_axis(reference, direction)
if axis_norm > np.finfo(float).eps:
axis = axis / axis_norm
angle = np.arccos(np.clip(np.dot(reference, direction), -1.0, 1.0))
rot_mat = _create_rotation_matrix(axis, angle)
else:
# Vectors are parallel or anti-parallel
rot_mat = np.eye(3) if np.dot(reference, direction) > 0 else -np.eye(3)
return rot_mat, direction
def compute_linear_transform(pos1, pos2, offset=None):
"""
Compute linear transformation between two 3D points.
This function computes the linear transformation that maps a vector pointing from
pos1 to pos2 into the canonical direction [0, 0, -1].
Args:
pos1: Starting position (3D point)
pos2: Ending position (3D point)
offset: Offset vector (3D point)
Returns:
Tuple containing:
- 3x3 rotation matrix
- offset position
"""
rot_mat, direction = compute_rotation_between_vectors(np.asarray(pos1), np.asarray(pos2))
if offset is not None:
offset_pos = np.asarray(pos1) + offset * direction
else:
offset_pos = 0
return rot_mat, offset_pos