dkist-processing-math 2.2.2__py3-none-any.whl → 3.0.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ Add ``apply_affine_transform`` which applies a combined center-preserving rotation and shift to an iterable of 2D arrays in a single interpolation pass. The rotation is specified as a scalar ``angle`` in radians. Also add ``affine_inverse_transform_matrix`` for callers that need the underlying homogeneous-coordinates matrix directly.
@@ -0,0 +1 @@
1
+ Remove ``affine_transform_arrays``, ``translate_arrays``, ``rotate_arrays_about_point``, ``scale_matrix``, ``rotation_matrix``, ``shear_matrix``, and ``translation_matrix`` from ``dkist_processing_math.transform``. These were superseded by ``apply_affine_transform`` (and, for callers that need the raw matrix, ``affine_inverse_transform_matrix``).
@@ -1,5 +1,6 @@
1
1
  """Arithmetic."""
2
2
 
3
+ from datetime import datetime
3
4
  from typing import Generator
4
5
  from typing import Iterable
5
6
  from typing import Union
@@ -124,3 +125,21 @@ def divide_fits_access_by_array(
124
125
  for obj in access_objs:
125
126
  obj.data = next(divide_arrays_by_array(obj.data, array_to_divide_by))
126
127
  yield obj
128
+
129
+
130
+ def closest_datetime(
131
+ target_datetime: str | datetime, datetimes: list[str | datetime] | str | datetime
132
+ ) -> str | datetime:
133
+ """Find the closest datetime to a target datetime. Return type is of the same type as the datetimes list."""
134
+ if isinstance(target_datetime, str):
135
+ target_datetime = datetime.fromisoformat(target_datetime)
136
+ if isinstance(datetimes, str) or isinstance(datetimes, datetime):
137
+ datetimes = [datetimes]
138
+ converted_datetimes = [
139
+ datetime.fromisoformat(d) if isinstance(d, str) else d for d in datetimes
140
+ ]
141
+ closest_index, _ = min(
142
+ enumerate(converted_datetimes),
143
+ key=lambda dt: abs(dt[1] - target_datetime),
144
+ )
145
+ return datetimes[closest_index]
@@ -1,3 +1,4 @@
1
+ from datetime import datetime
1
2
  from typing import Generator
2
3
 
3
4
  import numpy as np
@@ -5,6 +6,7 @@ import pytest
5
6
  from astropy.io import fits
6
7
  from dkist_processing_common.models.fits_access import FitsAccessBase
7
8
 
9
+ from dkist_processing_math.arithmetic import closest_datetime
8
10
  from dkist_processing_math.arithmetic import divide_arrays_by_array
9
11
  from dkist_processing_math.arithmetic import divide_fits_access_by_array
10
12
  from dkist_processing_math.arithmetic import subtract_array_from_arrays
@@ -18,7 +20,7 @@ def multiple_test_fits_access(request, numpy_arrays):
18
20
  for i, array in enumerate(numpy_arrays):
19
21
  hdu = fits.PrimaryHDU(data=array)
20
22
  hdu.header["TEST"] = i
21
- access_list.append(FitsAccessBase(hdu=hdu))
23
+ access_list.append(FitsAccessBase(header=hdu.header, data=array))
22
24
  if request.param == "multiple":
23
25
  return access_list
24
26
  return access_list[0]
@@ -126,3 +128,54 @@ def test_divide_arrays_by_array_wrong_shape(numpy_arrays_wrong_shape, numpy_arra
126
128
  """
127
129
  with pytest.raises(ValueError):
128
130
  list(divide_arrays_by_array(numpy_arrays_wrong_shape, numpy_array))
131
+
132
+
133
+ @pytest.mark.parametrize(
134
+ "datetimes, expected_result",
135
+ [
136
+ pytest.param(
137
+ ["2025-12-31T00:00:00", "2026-01-07T00:00:00"],
138
+ "2025-12-31T00:00:00",
139
+ id="multiple solar gain ops",
140
+ ),
141
+ pytest.param("2025-12-31T00:00:00", "2025-12-31T00:00:00", id="single solar gain op"),
142
+ ],
143
+ )
144
+ def test_closest_datetime_with_strings(datetimes, expected_result):
145
+ """
146
+ Given: a target datetime and a list of string datetimes
147
+ When: running the closest_datetime method
148
+ Then: the closest datetime from the list of datetimes is returned
149
+ """
150
+ result = closest_datetime(
151
+ target_datetime="2026-01-01T00:00:00",
152
+ datetimes=datetimes,
153
+ )
154
+ assert result == expected_result
155
+
156
+
157
+ @pytest.mark.parametrize(
158
+ "datetimes, expected_result",
159
+ [
160
+ pytest.param(
161
+ [
162
+ datetime(2025, 12, 31),
163
+ datetime(2026, 1, 7),
164
+ ],
165
+ datetime(2025, 12, 31),
166
+ id="multiple solar gain ops",
167
+ ),
168
+ pytest.param(datetime(2025, 12, 31), datetime(2025, 12, 31), id="single solar gain op"),
169
+ ],
170
+ )
171
+ def test_closest_datetime_with_datetimes(datetimes, expected_result):
172
+ """
173
+ Given: a target datetime and a list of datetimes
174
+ When: running the closest_datetime method
175
+ Then: the closest datetime from the list of datetimes is returned
176
+ """
177
+ result = closest_datetime(
178
+ target_datetime=datetime(2026, 1, 1),
179
+ datetimes=datetimes,
180
+ )
181
+ assert result == expected_result
@@ -1,18 +1,12 @@
1
1
  import numpy as np
2
2
  import pytest
3
- from scipy import ndimage as spnd
4
3
  from skimage import transform as transform
5
4
 
6
- from dkist_processing_math.transform import affine_transform_arrays
5
+ from dkist_processing_math.transform import affine_inverse_transform_matrix
6
+ from dkist_processing_math.transform import apply_affine_transform
7
7
  from dkist_processing_math.transform import bin_arrays
8
8
  from dkist_processing_math.transform import do_hough
9
9
  from dkist_processing_math.transform import make_binary
10
- from dkist_processing_math.transform import rotate_arrays_about_point
11
- from dkist_processing_math.transform import rotation_matrix
12
- from dkist_processing_math.transform import scale_matrix
13
- from dkist_processing_math.transform import shear_matrix
14
- from dkist_processing_math.transform import translate_arrays
15
- from dkist_processing_math.transform import translation_matrix
16
10
  from dkist_processing_math.transform.binning import resize_arrays
17
11
  from dkist_processing_math.transform.binning import resize_arrays_local_mean
18
12
 
@@ -149,328 +143,238 @@ def test_resize_arrays_local_mean_single_array(numpy_array, resized_array):
149
143
  np.testing.assert_allclose(result, resized_array)
150
144
 
151
145
 
152
- def test_affine_transform_specify_matrix(numpy_array):
153
- """
154
- Given: a numpy_array
155
- When: applying the affine transform and specifying the full transformation matrix
156
- Then: an Iterator containing the transformed array is returned
157
- """
158
- angle = np.radians(90)
159
- # Create the Affine transform for the rotation
160
- rotation = transform.AffineTransform(rotation=angle)
161
- # Offset to the center of the array for the rotation
162
- p = np.array([4.5, 4.5])
163
- # Create the Affine transform for the translation
164
- trans = transform.AffineTransform(translation=p)
165
- # Create the composite transform matrix for the rotation about the center
166
- tform = trans.params @ rotation.params @ np.linalg.inv(trans.params)
167
- # Perform the transformation
168
- result = affine_transform_arrays(numpy_array, matrix=tform)
169
- # Compare against the same rotation computed with a different method.
170
- np.testing.assert_allclose(next(result), transform.rotate(numpy_array, -90, center=(4.5, 4.5)))
171
-
172
-
173
- def test_affine_transform_specify_rotate(numpy_array):
174
- """
175
- Given: a numpy_array
176
- When: applying the affine transform and specifying a rotation angle and rotation point
177
- Then: an Iterator containing the rotated array is returned
178
- """
179
- angle = np.radians(90)
180
- rotation = transform.AffineTransform(rotation=angle)
181
- # Offset to the center of the array for the rotation
182
- p = np.array([4.5, 4.5, 0])
183
- # Transform the offset to rotated space
184
- o = p - np.dot(rotation.params, p)
185
- result = affine_transform_arrays(numpy_array, rotation=np.radians(90), translation=o[0:2][::-1])
186
- np.testing.assert_allclose(next(result), spnd.rotate(numpy_array, -90))
187
-
188
-
189
- def test_affine_transform_specify_scale_x(numpy_array):
190
- """
191
- Given: a numpy_array
192
- When: applying the affine transform and specifying a scale factor for the X axis only
193
- Then: an Iterator containing the array scaled along the X axis is returned
194
- """
195
-
196
- num_rows = numpy_array.shape[0]
197
- num_cols = numpy_array.shape[1]
198
- desired_output = np.zeros_like(numpy_array)
199
- desired_output[0:num_rows, 0 : num_cols // 2] = numpy_array[0:num_rows, 0:num_cols:2]
200
- result = affine_transform_arrays(numpy_array, scale=(0.5, 1.0))
201
- np.testing.assert_allclose(next(result), desired_output)
202
-
203
-
204
- def test_affine_transform_specify_scale_y(numpy_array):
205
- """
206
- Given: a numpy_array
207
- When: applying the affine transform and specifying a scale factor for the Y axis only
208
- Then: an Iterator containing the array scaled along the Y axis is returned
209
- """
210
- num_rows = numpy_array.shape[0]
211
- num_cols = numpy_array.shape[1]
212
- desired_output = np.zeros_like(numpy_array)
213
- desired_output[0 : num_rows // 2, 0:num_cols] = numpy_array[0:num_rows:2, 0:num_cols]
214
- result = affine_transform_arrays(numpy_array, scale=(1.0, 0.5))
215
- np.testing.assert_allclose(next(result), desired_output)
216
-
217
-
218
- def test_affine_transform_specify_scale_both(numpy_array):
219
- """
220
- Given: a numpy_array
221
- When: applying the affine transform and specifying a scale factor for each axis
222
- Then: an Iterator containing the array scaled separately along each axis is returned
223
- """
224
- tform = transform.SimilarityTransform(scale=0.5)
225
- desired_output = transform.warp(numpy_array, tform.inverse)
226
- result = affine_transform_arrays(numpy_array, scale=(0.5, 0.5))
227
- np.testing.assert_allclose(next(result), desired_output)
228
-
229
-
230
- def test_affine_transform_specify_scale_scalar(numpy_array):
231
- """
232
- Given: a numpy_array
233
- When: applying the affine transform and specifying a scalar scale factor to be applied to both axes
234
- Then: an Iterator containing the array scaled identically on each axis is returned
235
- """
236
- tform = transform.SimilarityTransform(scale=0.5)
237
- desired_output = transform.warp(numpy_array, tform.inverse)
238
- result = affine_transform_arrays(numpy_array, scale=0.5)
239
- np.testing.assert_allclose(next(result), desired_output)
146
+ @pytest.mark.parametrize(
147
+ "array_shape, shift, angle, order, expected_matrix",
148
+ [
149
+ pytest.param(
150
+ (100, 100),
151
+ np.array([0.0, 0.0]),
152
+ 0.0,
153
+ "shift_then_rotate",
154
+ np.eye(3),
155
+ id="identity",
156
+ ),
157
+ pytest.param(
158
+ (100, 200),
159
+ np.array([0.0, 0.0]),
160
+ np.pi,
161
+ "shift_then_rotate",
162
+ np.array([[-1.0, 0.0, 99.0], [0.0, -1.0, 199.0], [0.0, 0.0, 1.0]]),
163
+ id="180_rotation_about_center",
164
+ ),
165
+ pytest.param(
166
+ (100, 100),
167
+ np.array([5.0, -3.0]),
168
+ 0.0,
169
+ "shift_then_rotate",
170
+ np.array([[1.0, 0.0, -5.0], [0.0, 1.0, 3.0], [0.0, 0.0, 1.0]]),
171
+ id="pure_translation",
172
+ ),
173
+ pytest.param(
174
+ (100, 100),
175
+ np.array([0.0, 0.0]),
176
+ np.pi / 2,
177
+ "shift_then_rotate",
178
+ np.array([[0.0, 1.0, 0.0], [-1.0, 0.0, 99.0], [0.0, 0.0, 1.0]]),
179
+ id="90_deg_rotation",
180
+ ),
181
+ ],
182
+ )
183
+ def test_affine_inverse_transform_matrix_hand_derived(
184
+ array_shape, shift, angle, order, expected_matrix
185
+ ):
186
+ """
187
+ Given: 2D array shape / shift / angle / order combinations with easy-to-derive-by-hand expected matrices
188
+ When: building the homogeneous inverse transform matrix
189
+ Then: the result matches the hand-derived expectation
190
+ """
191
+ result = affine_inverse_transform_matrix(array_shape, shift, angle, order=order)
192
+ np.testing.assert_allclose(result, expected_matrix, atol=1e-10)
193
+
194
+
195
+ def test_affine_inverse_transform_matrix_rejects_invalid_order():
196
+ """
197
+ Given: an `order` value that isn't one of the two recognized options
198
+ When: building the homogeneous inverse transform matrix
199
+ Then: a ValueError is raised, rather than silently falling back to some default behavior
200
+ """
201
+ with pytest.raises(ValueError, match="order must be"):
202
+ affine_inverse_transform_matrix(
203
+ (10, 10), np.array([0.0, 0.0]), 0.0, order="not_a_real_order"
204
+ )
205
+
206
+
207
+ def test_affine_inverse_transform_matrix_requires_2d():
208
+ """
209
+ Given: an array_shape with more than 2 dimensions
210
+ When: building the homogeneous inverse transform matrix
211
+ Then: a ValueError is raised
212
+ """
213
+ with pytest.raises(ValueError, match="array_shape must be 2D"):
214
+ affine_inverse_transform_matrix((10, 10, 10), np.zeros(3), 0.3, order="shift_then_rotate")
215
+
216
+
217
+ @pytest.mark.parametrize(
218
+ "shift",
219
+ [
220
+ pytest.param(5.0, id="scalar_shift"),
221
+ pytest.param(np.array([5.0]), id="length_one_shift"),
222
+ pytest.param(np.array([1.0, 2.0, 3.0]), id="wrong_length_shift"),
223
+ ],
224
+ )
225
+ def test_affine_inverse_transform_matrix_rejects_mismatched_shift(shift):
226
+ """
227
+ Given: a `shift` whose shape doesn't match `array_shape`'s dimensionality -- including scalar
228
+ and length-1 shifts, which would otherwise silently broadcast rather than raising
229
+ When: building the homogeneous inverse transform matrix for a 2D array_shape
230
+ Then: a ValueError is raised
231
+ """
232
+ with pytest.raises(ValueError, match="shift must have shape"):
233
+ affine_inverse_transform_matrix((10, 10), shift, 0.0, order="shift_then_rotate")
234
+
235
+
236
+ def test_apply_affine_transform_identity():
237
+ """
238
+ Given: a zero shift and zero rotation
239
+ When: applying the correction
240
+ Then: the output array is numerically identical to the input
241
+ """
242
+ array = np.arange(100.0).reshape(10, 10)
243
+ result = next(
244
+ apply_affine_transform(array, np.array([0.0, 0.0]), 0.0, order="shift_then_rotate")
245
+ )
246
+ np.testing.assert_allclose(result, array, atol=1e-10)
240
247
 
241
248
 
242
- def test_affine_transform_specify_shear(numpy_array):
249
+ def test_apply_affine_transform_single_array_wrapped():
243
250
  """
244
- Given: a numpy_array
245
- When: applying the affine transform and specifying a shear angle
246
- Then: an Iterator containing the sheared array is returned
251
+ Given: a single ndarray (not wrapped in a list)
252
+ When: applying the correction
253
+ Then: it is accepted and the generator yields exactly one result
247
254
  """
248
- shear_mat = np.array([[1, 0.1, 0], [0.2, 1, 0], [0, 0, 1]])
249
- shear_mat_inv = np.linalg.inv(shear_mat)
250
- desired_output = transform.warp(numpy_array, shear_mat_inv)
251
- result = affine_transform_arrays(numpy_array, shear=(0.1, 0.2))
252
- np.testing.assert_allclose(next(result), desired_output)
255
+ array = np.zeros((5, 5))
256
+ results = list(
257
+ apply_affine_transform(array, np.array([0.0, 0.0]), 0.0, order="shift_then_rotate")
258
+ )
259
+ assert len(results) == 1
260
+ assert results[0].shape == (5, 5)
253
261
 
254
262
 
255
- def test_affine_transform_specify_translate(numpy_array):
263
+ def test_apply_affine_transform_nonfinite_values_replaced():
256
264
  """
257
- Given: a numpy_array
258
- When: applying the affine transform and specifying a translation vector
259
- Then: an Iterator containing the translated array is returned
265
+ Given: an input array containing NaN and ±inf
266
+ When: applying the correction
267
+ Then: the output is fully finite (NaN/inf do not survive interpolation)
260
268
  """
261
- result = affine_transform_arrays(numpy_array, translation=(2, 5))
262
- desired_output = spnd.shift(numpy_array, (2, 5))
263
- np.testing.assert_allclose(next(result), desired_output)
269
+ array = np.ones((10, 10))
270
+ array[0, 0] = np.nan
271
+ array[0, 1] = np.inf
272
+ array[0, 2] = -np.inf
273
+ result = next(
274
+ apply_affine_transform(array, np.array([0.0, 0.0]), 0.0, order="shift_then_rotate")
275
+ )
276
+ assert np.isfinite(result).all()
264
277
 
265
278
 
266
- def test_affine_transform_all_params_none(numpy_array):
267
- """
268
- Given: a numpy_array
269
- When: applying the affine transform and specifying no parameters
270
- Then: an error is raised
279
+ def test_apply_affine_transform_hand_derived_shift():
271
280
  """
272
- with pytest.raises(ValueError):
273
- affine_transform_arrays(numpy_array)
274
-
281
+ Given: a distinctive single-pixel array and a pure integer shift (no rotation)
282
+ When: applying the correction with shift_then_rotate
283
+ Then: the bright pixel lands exactly where hand-derived arithmetic says it should
275
284
 
276
- def test_affine_transform_matrix_and_one_param(numpy_array):
285
+ With shift=[2, 3] and no rotation, a pixel at (row=1, col=1) moves to (row=3, col=4).
286
+ This directly tests that the sign convention is correct end-to-end -- that a positive
287
+ shift moves content in the positive direction -- with no ambiguity from interpolation.
277
288
  """
278
- Given: a numpy_array
279
- When: applying the affine transform and specifying both a full matrix and one or more transform parameters
280
- Then: an error is raised
281
- """
282
- with pytest.raises(ValueError):
283
- affine_transform_arrays(numpy_array, matrix=np.ones((2, 2)), shear=0.1)
289
+ array = np.zeros((10, 10))
290
+ array[1, 1] = 1.0
291
+ shift = np.array([2.0, 3.0])
284
292
 
293
+ result = next(apply_affine_transform(array, shift, 0.0, order="shift_then_rotate"))
285
294
 
286
- def test_affine_transform_matrix_wrong_shape(numpy_array):
287
- """
288
- Given: a numpy_array
289
- When: applying the affine transform and specifying both a full matrix and one or more transform parameters
290
- Then: an error is raised
291
- """
292
- with pytest.raises(ValueError):
293
- affine_transform_arrays(numpy_array, matrix=np.ones((2, 2)))
295
+ assert result[3, 4] == pytest.approx(1.0)
294
296
 
295
297
 
296
- def test_affine_transform_arrays(numpy_arrays):
298
+ def test_apply_affine_transform_yields_one_per_input():
297
299
  """
298
- Given: an iterable of numpy_arrays
299
- When: applying the affine transform
300
- Then: a Iterator containing the transformed arrays is returned
300
+ Given: a list of N arrays
301
+ When: applying the correction
302
+ Then: the generator yields exactly N arrays, each with the correct shape
301
303
  """
302
- results = affine_transform_arrays(numpy_arrays, translation=(7, 3))
303
- desired_outputs = [spnd.shift(array, (7, 3)) for array in numpy_arrays]
304
- for result, desired_output in zip(results, desired_outputs):
305
- np.testing.assert_allclose(result, desired_output)
306
-
307
-
308
- def test_affine_transform_arrays_single_array(numpy_array):
309
- """
310
- Given: a single numpy array
311
- When: applying the affine transform to an iterable of arrays
312
- Then: an Iterator containing the transformed array is returned
313
- """
314
- results = affine_transform_arrays(numpy_array, translation=(2, 3))
315
- result = next(results)
316
- desired_output = spnd.shift(numpy_array, (2, 3))
317
- np.testing.assert_allclose(result, desired_output)
304
+ arrays = [np.ones((8, 8)) * i for i in range(3)]
305
+ results = list(
306
+ apply_affine_transform(arrays, np.array([0.0, 0.0]), 0.0, order="shift_then_rotate")
307
+ )
308
+ assert len(results) == 3
309
+ assert all(r.shape == (8, 8) for r in results)
318
310
 
319
311
 
320
- def test_rotate_arrays_about_point(numpy_arrays):
312
+ def test_apply_affine_transform_hand_derived_rotation():
321
313
  """
322
- Given: an iterable of numpy arrays
323
- When: rotating an iterable of arrays about a point
324
- Then: an Iterator containing the rotated arrays is returned
325
- """
326
- angle = -45
327
- p = np.array([5, 2])
328
- results = rotate_arrays_about_point(numpy_arrays, angle=np.radians(angle), point=p)
329
- desired_outputs = [transform.rotate(array, angle, center=p) for array in numpy_arrays]
330
- for result, desired_output in zip(results, desired_outputs):
331
- np.testing.assert_allclose(result, desired_output)
332
-
314
+ Given: a distinctive single-pixel array and a 90° CCW rotation (no shift)
315
+ When: applying the transform with shift_then_rotate
316
+ Then: the bright pixel lands exactly where hand-derived arithmetic says it should
333
317
 
334
- def test_rotate_arrays_about_point_single_array(numpy_array):
318
+ With angle=π/2 and no shift, a pixel at (row=1, col=7) in a 10×10 array (center=(4.5, 4.5))
319
+ rotates CCW to (row=2, col=1). Derivation: the pull-back maps output (r', c') to input
320
+ (c', 9−r'), so output[2, 1] samples input[1, 7].
335
321
  """
336
- Given: a single numpy array
337
- When: rotating an iterable of arrays about a point
338
- Then: an Iterator containing the rotated array is returned
339
- """
340
- angle = -45
341
- p = np.array([5, 2])
342
- results = rotate_arrays_about_point(numpy_array, angle=np.radians(angle), point=p)
343
- result = next(results)
344
- desired_output = transform.rotate(numpy_array, angle, center=p)
345
- np.testing.assert_allclose(result, desired_output)
346
-
322
+ array = np.zeros((10, 10))
323
+ array[1, 7] = 1.0
347
324
 
348
- def test_translate_arrays(numpy_arrays):
349
- """
350
- Given: an iterable of numpy arrays
351
- When: translating an iterable of arrays by a vector
352
- Then: an Iterator containing the translated arrays is returned
353
- """
354
- results = translate_arrays(numpy_arrays, translation=(5, 2))
355
- desired_outputs = [spnd.shift(array, (5, 2)) for array in numpy_arrays]
356
- for result, desired_output in zip(results, desired_outputs):
357
- np.testing.assert_allclose(result, desired_output)
325
+ result = next(
326
+ apply_affine_transform(array, np.array([0.0, 0.0]), np.pi / 2, order="shift_then_rotate")
327
+ )
358
328
 
359
-
360
- def test_translate_arrays_single_array(numpy_array):
361
- """
362
- Given: a single numpy array
363
- When: translating an iterable of arrays by a vector
364
- Then: an Iterator containing the translated array is returned
365
- """
366
- results = translate_arrays(numpy_array, translation=(5, 2))
367
- result = next(results)
368
- desired_output = spnd.shift(numpy_array, (5, 2))
369
- np.testing.assert_allclose(result, desired_output)
329
+ assert result[2, 1] == pytest.approx(1.0)
370
330
 
371
331
 
372
- def test_scale_matrix(numpy_array):
373
- """
374
- Given: a single numpy array
375
- When: creating a scale matrix operator
376
- Then: a correct scale matrix is produced
332
+ def test_apply_affine_transform_hand_derived_shift_and_rotate():
377
333
  """
378
- scale_mat = scale_matrix((0.2, 0.4))
379
- desired_output = affine_transform_arrays(numpy_array, scale=(0.2, 0.4))
380
- result = affine_transform_arrays(numpy_array, matrix=scale_mat)
381
- np.testing.assert_allclose(next(result), next(desired_output))
334
+ Given: a distinctive single-pixel array, a shift of [1, 0], and a 90° CCW rotation
335
+ When: applying shift_then_rotate
336
+ Then: the bright pixel lands exactly where hand-derived arithmetic says it should
382
337
 
383
-
384
- def test_rotation_matrix(numpy_array):
385
- """
386
- Given: a single numpy array
387
- When: creating a rotation matrix operator
388
- Then: a correct rotation matrix is produced
338
+ Shift [1, 0] moves (1, 7) → (2, 7); 90° CCW about center (4.5, 4.5) then moves
339
+ (2, 7) → (2, 2). The pull-back for output (2, 2): after un-centering, applying CW
340
+ rotation, re-centering, and un-shifting gives input (1, 7).
389
341
  """
390
- angle = np.radians(30)
391
- rotation_mat = rotation_matrix(angle)
392
- desired_output = affine_transform_arrays(numpy_array, rotation=angle)
393
- result = affine_transform_arrays(numpy_array, matrix=rotation_mat)
394
- np.testing.assert_allclose(next(result), next(desired_output))
342
+ array = np.zeros((10, 10))
343
+ array[1, 7] = 1.0
395
344
 
345
+ result = next(
346
+ apply_affine_transform(array, np.array([1.0, 0.0]), np.pi / 2, order="shift_then_rotate")
347
+ )
396
348
 
397
- def test_shear_matrix(numpy_array):
398
- """
399
- Given: a single numpy array
400
- When: creating a shear matrix operator
401
- Then: a correct shear matrix is produced
402
- """
403
- shear = (0.1, 0.2)
404
- shear_mat = shear_matrix(shear)
405
- desired_shear_mat = np.array([[1, 0.1, 0], [0.2, 1, 0], [0, 0, 1]])
406
- np.testing.assert_allclose(shear_mat, desired_shear_mat)
407
- desired_output = affine_transform_arrays(numpy_array, shear=shear)
408
- result = affine_transform_arrays(numpy_array, matrix=shear_mat)
409
- np.testing.assert_allclose(next(result), next(desired_output))
349
+ assert result[2, 2] == pytest.approx(1.0)
410
350
 
411
351
 
412
- def test_shear_matrix_scalar(numpy_array):
352
+ def test_affine_inverse_transform_matrix_matches_numpy_inverse():
413
353
  """
414
- Given: a single numpy array
415
- When: creating a shear matrix operator
416
- Then: a correct shear matrix is produced
417
- """
418
- shear = 0.1
419
- shear_mat = shear_matrix(shear)
420
- desired_shear_mat = np.array([[1, 0.1, 0], [0.1, 1, 0], [0, 0, 1]])
421
- np.testing.assert_allclose(shear_mat, desired_shear_mat)
422
- desired_output = affine_transform_arrays(numpy_array, shear=shear)
423
- result = affine_transform_arrays(numpy_array, matrix=shear_mat)
424
- np.testing.assert_allclose(next(result), next(desired_output))
425
-
354
+ Given: a known shift and rotation angle
355
+ When: computing the inverse transform matrix
356
+ Then: the result matches np.linalg.inv of the hand-composed forward matrix
426
357
 
427
- def test_translation_matrix(numpy_array):
358
+ This verifies the matrix algebra is correct end-to-end, independently of any
359
+ hand-derived expected values.
428
360
  """
429
- Given: a single numpy array
430
- When: creating a translation matrix operator
431
- Then: a correct translation matrix is produced
432
- """
433
- trans = (2.3, 5.7)
434
- trans_mat = translation_matrix(trans)
435
- desired_output = affine_transform_arrays(numpy_array, translation=trans)
436
- result = affine_transform_arrays(numpy_array, matrix=trans_mat)
437
- np.testing.assert_allclose(next(result), next(desired_output))
361
+ shape = (11, 21)
362
+ shift = np.array([3.0, -5.0])
363
+ angle = np.pi / 4
438
364
 
365
+ cx, cy = (shape[0] - 1) / 2.0, (shape[1] - 1) / 2.0 # 5.0, 10.0
366
+ cos_a, sin_a = np.cos(angle), np.sin(angle)
439
367
 
440
- def test_translation_matrix_scalar(numpy_array):
441
- """
442
- Given: a single numpy array
443
- When: creating a translation matrix operator
444
- Then: a correct translation matrix is produced
445
- """
446
- trans = 4.2
447
- trans_mat = translation_matrix(trans)
448
- desired_output = affine_transform_arrays(numpy_array, translation=trans)
449
- result = affine_transform_arrays(numpy_array, matrix=trans_mat)
450
- np.testing.assert_allclose(next(result), next(desired_output))
368
+ # Hand-composed forward matrix for shift_then_rotate:
369
+ # T(center) @ R_fwd @ T(-center) @ T(shift)
370
+ R_fwd = np.array([[cos_a, -sin_a, 0.0], [sin_a, cos_a, 0.0], [0.0, 0.0, 1.0]])
371
+ T_cen = np.array([[1.0, 0.0, cx], [0.0, 1.0, cy], [0.0, 0.0, 1.0]])
372
+ T_neg_cen = np.array([[1.0, 0.0, -cx], [0.0, 1.0, -cy], [0.0, 0.0, 1.0]])
373
+ T_s = np.array([[1.0, 0.0, shift[0]], [0.0, 1.0, shift[1]], [0.0, 0.0, 1.0]])
374
+ forward = T_cen @ R_fwd @ T_neg_cen @ T_s
451
375
 
452
-
453
- def test_matrix_compositions(numpy_array):
454
- """
455
- Given: a single numpy array
456
- When: creating a composition of transform matrices
457
- Then: a correct transform results from the composition
458
- """
459
- trans = (2.3, 5.7)
460
- trans_mat = translation_matrix(trans)
461
- shear = (0.1, 0.2)
462
- shear_mat = shear_matrix(shear)
463
- angle = np.radians(30)
464
- rotation_mat = rotation_matrix(angle)
465
- scale = (0.2, 0.4)
466
- scale_mat = scale_matrix(scale)
467
- # The order in which the transforms are applied reads right to left:
468
- composition = trans_mat @ rotation_mat @ scale_mat @ shear_mat
469
- result = affine_transform_arrays(numpy_array, matrix=composition)
470
- desired_output = affine_transform_arrays(
471
- numpy_array, scale=scale, shear=shear, rotation=angle, translation=trans
472
- )
473
- np.testing.assert_allclose(next(result), next(desired_output))
376
+ result = affine_inverse_transform_matrix(shape, shift, angle, order="shift_then_rotate")
377
+ np.testing.assert_allclose(result, np.linalg.inv(forward), atol=1e-10)
474
378
 
475
379
 
476
380
  def test_make_binary_image():
@@ -1,24 +1,14 @@
1
1
  """init."""
2
2
 
3
- from .affine import affine_transform_arrays
4
- from .affine import rotate_arrays_about_point
5
- from .affine import rotation_matrix
6
- from .affine import scale_matrix
7
- from .affine import shear_matrix
8
- from .affine import translate_arrays
9
- from .affine import translation_matrix
3
+ from .affine import affine_inverse_transform_matrix
4
+ from .affine import apply_affine_transform
10
5
  from .binary import make_binary
11
6
  from .binning import bin_arrays
12
7
  from .hough import do_hough
13
8
 
14
9
  __all__ = [
15
- "affine_transform_arrays",
16
- "translate_arrays",
17
- "rotate_arrays_about_point",
18
- "rotation_matrix",
19
- "translation_matrix",
20
- "scale_matrix",
21
- "shear_matrix",
10
+ "affine_inverse_transform_matrix",
11
+ "apply_affine_transform",
22
12
  "make_binary",
23
13
  "bin_arrays",
24
14
  "do_hough",
@@ -1,261 +1,186 @@
1
1
  """Affine transforms."""
2
2
 
3
- from functools import partial
4
- from typing import Iterable
5
- from typing import Iterator
6
- from typing import Tuple
7
- from typing import Union
3
+ from collections.abc import Generator
4
+ from collections.abc import Iterable
5
+ from typing import Literal
8
6
 
9
7
  import numpy as np
10
- from skimage import transform as skit
11
-
12
-
13
- def affine_transform_arrays(
14
- arrays: Union[Iterable[np.ndarray], np.ndarray],
15
- matrix: np.ndarray = None,
16
- scale: Union[Tuple[float, float], np.ndarray, float] = None,
17
- translation: Union[Tuple[float, float], np.ndarray, float] = None,
18
- rotation: float = None,
19
- shear: Union[Tuple[float, float], np.ndarray, float] = None,
20
- **kwargs,
21
- ) -> Iterator[np.ndarray]:
22
- """
23
- Transform an iterable of input arrays using a generalized affine transform operator.
24
-
25
- A transform matrix may be specified or separate parameters for scale, translation,
26
- rotation and shear may be specified instead. This method abstracts the use of the
27
- scikit-image affine transform, which may easily be replaced with one from scipy or
28
- another origin. If transform parameters are specified, they are applied in this order:
29
- shear, scale, rotation, translation
8
+ import scipy.ndimage as spnd
30
9
 
31
- Parameters
32
- ----------
33
- arrays
34
- The array(s) to be transformed
35
- matrix
36
- The transformation matrix to be used for the transform. If specified, none of
37
- [translation, rotation, or shear] may be used. Optional. If matrix is not specified,
38
- at least one of the following must be used: scale, translation, rotation, shear
39
- scale
40
- The scale factor to be applied in the transform: (Sx, Sy). If a scalar is used, the same
41
- value is applied to each axis.
42
- translation
43
- The translation to be applied in the transform: (Tx, Ty) in pixel units.
44
- rotation
45
- The rotation angle, in radians, to be applied to the transformation.
46
- A positive angle is counter-clockwise in a right handed coordinate system
47
- shear
48
- The shear factor to be applied in the transform: (Shx, Shy). If a scalar is used, the same
49
- value is applied to each axis.
50
- **kwargs
51
- Optional arguments to be passed on to skimage.transform.warp() if desired. See
52
- https://scikit-image.org/docs/stable/api/skimage.transform.html#skimage.transform.warp
53
- for more details.
54
-
55
- Returns
56
- -------
57
- The transformed array(s)
58
10
 
11
+ def affine_inverse_transform_matrix(
12
+ array_shape: tuple[int, ...],
13
+ shift: np.ndarray,
14
+ angle: float,
15
+ order: Literal["rotate_then_shift", "shift_then_rotate"],
16
+ ) -> np.ndarray:
59
17
  """
60
- tform = None
61
- any_param_set = any(param is not None for param in [scale, translation, rotation, shear])
62
-
63
- # We do our own pre-checks here so that we're not totally tied to use scikit-image in the future
64
- #
65
- if not any_param_set and matrix is None:
66
- raise ValueError(
67
- "You must specify matrix or at least one of scale, rotate, shear, or shift."
68
- )
18
+ Build the homogeneous-coordinates inverse transform matrix for a center-preserving rotation and shift.
69
19
 
70
- elif any_param_set and matrix is not None:
71
- raise ValueError(
72
- "You cannot specify both matrix and any of scale, rotate, shear, or shift."
73
- )
74
-
75
- elif matrix is not None:
76
- if matrix.shape != (3, 3):
77
- raise ValueError(f"Invalid shape of transformation matrix: {matrix.shape}")
78
- tform = skit.AffineTransform(matrix=matrix)
79
-
80
- elif any_param_set:
81
- # Construct the matrix from parameters
82
- if scale is None:
83
- scale = (1.0, 1.0)
84
- elif np.isscalar(scale):
85
- scale = (scale, scale)
86
-
87
- if translation is None:
88
- translation = (0.0, 0.0)
89
- elif np.isscalar(translation):
90
- translation = (translation, translation)
91
-
92
- if rotation is None:
93
- rotation = 0.0
94
-
95
- if shear is None:
96
- shear = (0.0, 0.0)
97
-
98
- translation = translation[::-1] # Because skimage and numpy have different axes ordering
99
- shear_mat = shear_matrix(shear)
100
- temp_tform = skit.AffineTransform(scale=scale, translation=translation, rotation=rotation)
101
- tform_mat = temp_tform.params @ shear_mat
102
- tform = skit.AffineTransform(matrix=tform_mat)
103
-
104
- arrays = [arrays] if isinstance(arrays, np.ndarray) else arrays
105
- partial_warp = partial(skit.warp, inverse_map=tform.inverse, **kwargs)
106
- return map(partial_warp, arrays)
107
-
108
-
109
- def rotate_arrays_about_point(
110
- arrays: Union[Iterable[np.ndarray], np.ndarray],
111
- angle: float = 0.0,
112
- point: Union[Tuple[float, float], np.ndarray] = None,
113
- **kwargs,
114
- ) -> Iterator[np.ndarray]:
115
- """
116
- Rotate an iterable of arrays about a specified point.
20
+ This is the matrix form needed by `scipy.ndimage.affine_transform`'s `matrix` kwarg
21
+ to apply a rotation about the array's center and a shift in a single pass. A positive
22
+ angle rotates content counter-clockwise (the conventional mathematical direction).
23
+ The `order` argument controls which forward operation is being modeled (see below).
117
24
 
118
25
  Parameters
119
26
  ----------
120
- arrays
121
- The array(s) to be rotated
27
+ array_shape
28
+ The shape of the 2D array the transform will be applied to.
122
29
 
123
- angle
124
- The angle, in radians, for the rotation to be applied, optional. Default is zero
125
- A positive angle is counter-clockwise in a right handed coordinate system and
126
- clockwise in a left-handed coordinate system
30
+ shift
31
+ The shift to apply, in pixels, with length `ndim` (matching `array_shape`). A positive
32
+ `shift` moves the array's content in the positive direction along each axis (e.g.
33
+ ``shift=[0, 1]`` moves content one pixel toward higher column indices).
127
34
 
128
- point
129
- The point, in pixel coordinates (x, y) about which to rotate. Optional. Default is None,
130
- meaning the center of the array will be used. Note that the origin pixel is centered at
131
- (0, 0) and has extent: [-0.5: 0.5, -0.5: 0.5]
35
+ angle
36
+ The rotation angle, in radians.
132
37
 
133
- kwargs
134
- Any arguments to be passed to skimage.transform.rotate. See
135
- https://scikit-image.org/docs/stable/api/skimage.transform.html#skimage.transform.rotate
136
- for more details.
38
+ order
39
+ Whether the modeled forward operation shifts first and then rotates
40
+ (``"shift_then_rotate"``) or rotates first and then shifts (``"rotate_then_shift"``).
137
41
 
138
42
  Returns
139
43
  -------
140
- An Iterator yielding the rotated arrays.
44
+ A ``(3, 3)`` homogeneous transform matrix encoding the rotation and shift together.
141
45
 
142
46
  """
143
- arrays = [arrays] if isinstance(arrays, np.ndarray) else arrays
144
- partial_rotate = partial(skit.rotate, angle=np.degrees(angle), center=point, **kwargs)
145
- return map(partial_rotate, arrays)
146
-
147
-
148
- def translate_arrays(
149
- arrays: Union[Iterable[np.ndarray], np.ndarray],
150
- translation: Union[Tuple[float, float], np.ndarray] = (0.0, 0.0),
151
- **kwargs,
152
- ) -> Iterator[np.ndarray]:
153
- """
154
- Translate an iterable of arrays by a specified vector.
155
-
156
- Parameters
157
- ----------
158
- arrays
159
- The array(s) to be translated
160
-
161
- translation
162
- The translation to be applied in the transform: (Tx, Ty) in pixel units. Optional.
163
-
164
- **kwargs
165
- Optional arguments to be passed on to skimage.transform.warp() if desired. See
166
- https://scikit-image.org/docs/stable/api/skimage.transform.html#skimage.transform.warp
167
- for more details.
168
-
169
- Returns
170
- -------
171
- np.ndarray
172
- The translated array
173
-
47
+ ndim = len(array_shape)
48
+
49
+ shift = np.asarray(shift, dtype=np.float64)
50
+ if shift.shape != (ndim,):
51
+ raise ValueError(f"shift must have shape ({ndim},) to match array_shape, got {shift.shape}")
52
+
53
+ if order not in ("rotate_then_shift", "shift_then_rotate"):
54
+ raise ValueError(f"order must be 'rotate_then_shift' or 'shift_then_rotate', got {order!r}")
55
+
56
+ if ndim != 2:
57
+ raise ValueError(f"array_shape must be 2D; got {ndim = }.")
58
+
59
+ center = 0.5 * (np.asarray(array_shape, dtype=np.float64) - 1.0)
60
+
61
+ # This function returns the pull-back (inverse) of the requested forward transform, because
62
+ # `scipy.ndimage.affine_transform` maps each *output* pixel coordinate back to the *input*
63
+ # coordinate to sample. The inverse of T(v) is T(-v); the inverse of rotation R is R(-angle).
64
+ # Each matrix below uses its inverse form directly, so the negations are visible at the
65
+ # construction site rather than applied to the variables earlier in the function.
66
+ R = _rotation_matrix(-angle) # R_fwd^-1
67
+ T_uncenter = _translation_matrix(center) # T_fwd(-center)^-1 = T(+center)
68
+ T_center = _translation_matrix(-center) # T_fwd(+center)^-1 = T(-center)
69
+ T_shift = _translation_matrix(-shift) # T_fwd(+shift)^-1 = T(-shift)
70
+
71
+ if order == "shift_then_rotate":
72
+ # Inverse of: T(+center) @ R_fwd @ T(-center) @ T(+shift)
73
+ return T_shift @ T_uncenter @ R @ T_center
74
+ else: # order == "rotate_then_shift"
75
+ # Inverse of: T(+shift) @ T(+center) @ R_fwd @ T(-center)
76
+ return T_uncenter @ R @ T_center @ T_shift
77
+
78
+
79
+ def apply_affine_transform(
80
+ arrays: Iterable[np.ndarray] | np.ndarray,
81
+ shift: np.ndarray,
82
+ angle: float,
83
+ order: Literal["rotate_then_shift", "shift_then_rotate"],
84
+ interp_order: int = 1,
85
+ mode: str = "nearest",
86
+ cval: float = np.nan,
87
+ ) -> Generator[np.ndarray, None, None]:
174
88
  """
175
- arrays = [arrays] if isinstance(arrays, np.ndarray) else arrays
176
- return affine_transform_arrays(arrays, translation=translation, **kwargs)
89
+ Apply a center-preserving rotation and shift to each input array.
177
90
 
91
+ Builds a single homogeneous-coordinates inverse transform matrix (via
92
+ `affine_inverse_transform_matrix`) and applies it with one
93
+ `scipy.ndimage.affine_transform` call per array, resolving both the rotation and shift in a
94
+ single interpolation pass. Non-finite values (NaN, ±inf) are replaced before interpolation so
95
+ that they don't spread during resampling.
178
96
 
179
- def scale_matrix(scale_factors: Union[Tuple[float, float], float] = (1.0, 1.0)) -> np.ndarray:
180
- """
181
- Create an Affine Transform matrix for a scale operation.
97
+ The transform matrix is recomputed only when the array shape changes, so passing a sequence of
98
+ same-shaped arrays (the typical case) incurs only one matrix build.
182
99
 
183
100
  Parameters
184
101
  ----------
185
- scale_factors
186
- The scale factors to use in scaling the array. If a scalar is passed, it
187
- is applied to both axes.
188
-
189
- Returns
190
- -------
191
- A 3x3 homogeneous transform matrix implementing the scaling
192
-
193
- """
194
- return skit.AffineTransform(scale=scale_factors).params
195
-
196
-
197
- def rotation_matrix(angle: float = 0.0) -> np.ndarray:
198
- """
199
- Create an Affine Transform matrix for a rotation operation.
102
+ arrays
103
+ One or more arrays to correct. A single `np.ndarray` is wrapped automatically.
200
104
 
201
- This assumes the rotation is about (0, 0) and it is up to the user
202
- to calculate the appropriate translation offset to be applied in the
203
- skimage.warp() method to achieve rotation about an arbitrary point
105
+ shift
106
+ The shift to apply, in pixels. See `affine_inverse_transform_matrix` for convention.
204
107
 
205
- Parameters
206
- ----------
207
108
  angle
208
- The angle of rotation in radians. This represents counter-clockwise rotation
209
- in a right-handed coordinate system and clockwise rotation in a left-handed
210
- system
109
+ The rotation angle, in radians. See `affine_inverse_transform_matrix` for details.
211
110
 
212
- Returns
213
- -------
214
- A 3x3 homogeneous transform matrix implementing the rotation
111
+ order
112
+ Whether the modeled forward operation shifts first then rotates (``"shift_then_rotate"``)
113
+ or rotates first then shifts (``"rotate_then_shift"``).
215
114
 
216
- """
217
- return skit.AffineTransform(rotation=angle).params
115
+ interp_order
116
+ Spline interpolation order for `scipy.ndimage.affine_transform`. Must be 0–5.
117
+ 0 = nearest-neighbor, 1 = bilinear (default), …, 5 = quintic.
218
118
 
119
+ mode
120
+ How points outside the array boundary are handled. Passed directly to
121
+ `scipy.ndimage.affine_transform`. Default ``"nearest"``.
219
122
 
220
- def shear_matrix(shear_factors: Union[Tuple[float, float], float] = (1.0, 1.0)) -> np.ndarray:
221
- """
222
- Create an Affine Transform matrix for a shear operation.
123
+ cval
124
+ Fill value used when ``mode="constant"``. Default `np.nan`.
223
125
 
224
- Parameters
225
- ----------
226
- shear_factors
227
- The shear scaling factors.
228
-
229
- Returns
230
- -------
231
- A 3x3 homogeneous transform matrix implementing the shear
126
+ Yields
127
+ ------
128
+ np.ndarray
129
+ Corrected arrays, one per input, each with the same shape as its input.
232
130
 
233
131
  """
234
- if isinstance(shear_factors, float):
235
- shear_factors = shear_factors, shear_factors
236
- shear_mat = np.eye(3, 3)
237
- shear_mat[0, 1] = shear_factors[0]
238
- shear_mat[1, 0] = shear_factors[1]
239
- return shear_mat
132
+ arrays = [arrays] if isinstance(arrays, np.ndarray) else arrays
240
133
 
134
+ previous_shape = None
135
+ matrix = None
136
+
137
+ for array in arrays:
138
+ array = np.asarray(array, dtype=np.float64)
139
+
140
+ # Replace non-finite values before interpolation; affine_transform spreads NaN/inf.
141
+ finite_mask = np.isfinite(array)
142
+ if not finite_mask.all():
143
+ finite_values = array[finite_mask]
144
+ if finite_values.size == 0:
145
+ # Entire array is non-finite -- fill with a safe scalar.
146
+ array.fill(cval if np.isfinite(cval) else 0.0)
147
+ else:
148
+ pos_inf = np.isposinf(array)
149
+ if pos_inf.any():
150
+ array[pos_inf] = finite_values.max()
151
+ neg_inf = np.isneginf(array)
152
+ if neg_inf.any():
153
+ array[neg_inf] = finite_values.min()
154
+ nan_mask = np.isnan(array)
155
+ if nan_mask.any():
156
+ array[nan_mask] = np.nanmedian(array)
157
+
158
+ if array.shape != previous_shape:
159
+ matrix = affine_inverse_transform_matrix(array.shape, shift, angle, order=order)
160
+ previous_shape = array.shape
161
+
162
+ yield spnd.affine_transform(
163
+ array,
164
+ matrix=matrix,
165
+ order=interp_order,
166
+ mode=mode,
167
+ cval=cval,
168
+ prefilter=(interp_order > 1),
169
+ )
241
170
 
242
- def translation_matrix(offsets: Union[Tuple[float, float], float] = (1.0, 1.0)) -> np.ndarray:
243
- """
244
- Create an Affine Transform matrix for a translation operation.
245
171
 
246
- Parameters
247
- ----------
248
- offsets
249
- The translation ofsets.
172
+ def _rotation_matrix(angle: float) -> np.ndarray:
173
+ """Build a (3, 3) homogeneous rotation matrix for the given angle in radians."""
174
+ cosine_angle, sine_angle = np.cos(angle), np.sin(angle)
175
+ return np.array(
176
+ [[cosine_angle, -sine_angle, 0.0], [sine_angle, cosine_angle, 0.0], [0.0, 0.0, 1.0]],
177
+ dtype=np.float64,
178
+ )
250
179
 
251
- Returns
252
- -------
253
- A 3x3 homogeneous transform matrix implementing the translation
254
180
 
255
- """
256
- if isinstance(offsets, float):
257
- offsets = offsets, offsets
258
- trans_mat = np.eye(3, 3)
259
- trans_mat[0, 2] = offsets[1]
260
- trans_mat[1, 2] = offsets[0]
261
- return trans_mat
181
+ def _translation_matrix(shift: np.ndarray) -> np.ndarray:
182
+ """Build an (ndim+1, ndim+1) homogeneous pure-translation matrix."""
183
+ ndim = len(shift)
184
+ m = np.eye(ndim + 1, dtype=np.float64)
185
+ m[:ndim, ndim] = shift
186
+ return m
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dkist-processing-math
3
- Version: 2.2.2
3
+ Version: 3.0.0rc1
4
4
  Summary: Common math functionality used in the DKIST calibrations suite
5
5
  Author-email: NSO / AURA <dkistdc@nso.edu>
6
6
  License: BSD-3-Clause
@@ -1,18 +1,20 @@
1
1
  changelog/.gitempty,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ changelog/27.feature.rst,sha256=zdqB-5Ag4Na7eCDDj3QVeXmcyj2Y1DgSEjkIuH1kVGI,336
3
+ changelog/27.removal.rst,sha256=F3M7wUvuCSTj_oR6Bbgne6s2NZW5CDLEtbzUA4C9_KY,349
2
4
  dkist_processing_math/__init__.py,sha256=z2HXBhrQyX-yBiLTWnPUgVevMA7QI_3ZdJKn3ev1F8I,252
3
- dkist_processing_math/arithmetic.py,sha256=WgO3lQI4GIVVOiXb_F7tQCZ5XQKzm_kF_zcy_3IL964,3695
5
+ dkist_processing_math/arithmetic.py,sha256=-kLO64IT2RxyykH1jlthvC0pegkhyJpIj9oa_9rdAdY,4457
4
6
  dkist_processing_math/feature.py,sha256=xdhPkkoOX9aO50BeMjU_2vKwAOG1AUBA3TvfHzZuP6A,2004
5
7
  dkist_processing_math/linear_algebra.py,sha256=yCA6ZuNORFnAyXywj73QMmGCpv9ZC2FSoMEPc8wToQY,4180
6
8
  dkist_processing_math/statistics.py,sha256=KkZTy8wdslPSQ5qP4z_We60Vizaxqna7rmMwxkoj36U,3178
7
9
  dkist_processing_math/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
10
  dkist_processing_math/tests/conftest.py,sha256=TV0o-R2lU1MvZleme0w-akcgO5wOTlFNKhjSlmDwkqg,1593
9
- dkist_processing_math/tests/test_arithmetic.py,sha256=18YI4Fr_m0KU0dYR8_ef7CMmnOlb-Zc4I8ERgoYdxdA,5658
11
+ dkist_processing_math/tests/test_arithmetic.py,sha256=IzNm6u1ER5w_zlasOgTXlXSZjmpXnXpUmRb20XPx4DM,7320
10
12
  dkist_processing_math/tests/test_feature.py,sha256=ZB8r_IUrAyTAlKbxEXwr8Q2zS7KPmlJhaU1l0guK0Xc,2311
11
13
  dkist_processing_math/tests/test_linear_algebra.py,sha256=k-XqIIBPsenNMKqo2nQnDkkN-6T1nvVT8BrJnupAIFU,6495
12
14
  dkist_processing_math/tests/test_statistics.py,sha256=N0S8C9Bwhd1VgCAkm6RqIZF_Ulwrcet9wqfbndw8KZs,2998
13
- dkist_processing_math/tests/test_transform.py,sha256=M761Yceh3FKV00udvb6_AeKuyBpQrUAG-mLfO-PbHmc,20253
14
- dkist_processing_math/transform/__init__.py,sha256=dFAi8bpLDa1Gp-kNrAsjcLnkRqyFNYbfzafC6gUUXHs,618
15
- dkist_processing_math/transform/affine.py,sha256=2U_qG700otfz5ePjbaa_m47ttRYRaNAj71W9NtcZ4xU,8596
15
+ dkist_processing_math/tests/test_transform.py,sha256=-y-ZZK8GXxWbnBNdA5GDD83IWnoBWFF5YQAZPfxDp8k,15862
16
+ dkist_processing_math/transform/__init__.py,sha256=D7g9xVIvSHF92QKgsNCsYA3hrCys01uN3gkKcOdnyTo,337
17
+ dkist_processing_math/transform/affine.py,sha256=e6VX_34ns3E5UZql1o8KnaiJHO-Lhn5R6xEBooP38Zg,7129
16
18
  dkist_processing_math/transform/binary.py,sha256=a_wYYORIvfD8AmQor3t5fMO8LwoFondyX-5awGQeUZk,1207
17
19
  dkist_processing_math/transform/binning.py,sha256=--CUP0nYxrWXbou48UJg8liSQJkwxBlDsKRXZTvhRCU,5173
18
20
  dkist_processing_math/transform/hough.py,sha256=CUGkoFNLSTeCOF_0L-hT7BtPomB-oAdc0VVb_UfKGiY,1371
@@ -24,7 +26,7 @@ docs/landing_page.rst,sha256=JCyR9SRhYS6zeEUlonIzko1kQ0tAFDIfi7pCoQanZeA,251
24
26
  docs/make.bat,sha256=mBAhtURwhQ7yc95pqwJzlhqBSvRknr1aqZ5s8NKvdKs,4513
25
27
  docs/requirements.txt,sha256=Kbl_X4c7RQZw035YTeNB63We6I7pvXFU4T0Uflp2yDY,29
26
28
  licenses/LICENSE.rst,sha256=piZaQplkzOMmH1NXg6QIdo9wwo9pPCoHkvm2-DmH76E,1462
27
- dkist_processing_math-2.2.2.dist-info/METADATA,sha256=lHuOull8f05fjgkr_N7NwKMLiojK04JPN0WX3u4f0dw,4467
28
- dkist_processing_math-2.2.2.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
29
- dkist_processing_math-2.2.2.dist-info/top_level.txt,sha256=7NeFrOlR4xhZ3LMT4jN99LQugukjr6l9u3w_s6b-uO4,46
30
- dkist_processing_math-2.2.2.dist-info/RECORD,,
29
+ dkist_processing_math-3.0.0rc1.dist-info/METADATA,sha256=yRgnxmdZ8V1wSsm5yNi2haHG8Iwxz-YNNfuw00E-2hQ,4470
30
+ dkist_processing_math-3.0.0rc1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
31
+ dkist_processing_math-3.0.0rc1.dist-info/top_level.txt,sha256=7NeFrOlR4xhZ3LMT4jN99LQugukjr6l9u3w_s6b-uO4,46
32
+ dkist_processing_math-3.0.0rc1.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.1)
2
+ Generator: setuptools (84.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5