From f45b30b06e19e7ab6d8bd224b633b19a134f05e9 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 02:30:11 +0000 Subject: [PATCH 1/2] perf: rotation-matrix transform_grid_2d_to_reference_frame (#598) Build the clockwise rotation from one scalar cos/sin of the angle and apply it as two multiply-adds per coordinate, instead of the polar form's per-pixel sqrt + arctan2 + sin + cos. Algebraically identical (max abs difference 2e-15 on a 15k-point grid); ~7x cheaper on the numpy path, and every transform-decorated profile evaluation pays it once per call. Phase 3 of the numpy-deflections-cpu epic (PyAutoGalaxy#598). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Re4JLhTCFnGYeez8oEyM6M --- autoarray/geometry/geometry_util.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/autoarray/geometry/geometry_util.py b/autoarray/geometry/geometry_util.py index 5f63cbf6d..38fb407a9 100644 --- a/autoarray/geometry/geometry_util.py +++ b/autoarray/geometry/geometry_util.py @@ -519,15 +519,20 @@ def transform_grid_2d_to_reference_frame( shifted_grid_2d = grid_2d - xp.array(centre) - radius = xp.sqrt(xp.sum(xp.square(shifted_grid_2d), axis=1)) - theta_coordinate_to_profile = xp.arctan2( - shifted_grid_2d[:, 0], shifted_grid_2d[:, 1] - ) - xp.radians(angle) + # A clockwise rotation by `angle` is the rotation matrix applied to the shifted (y, x) + # coordinates, built from one scalar cos / sin of the angle. This is algebraically + # identical to the polar form (r, theta - angle) -> (r sin, r cos) but costs two + # multiply-adds per coordinate instead of a per-pixel sqrt, arctan2, sin and cos. + cos_angle = xp.cos(xp.radians(angle)) + sin_angle = xp.sin(xp.radians(angle)) + + y = shifted_grid_2d[:, 0] + x = shifted_grid_2d[:, 1] return xp.vstack( [ - radius * xp.sin(theta_coordinate_to_profile), - radius * xp.cos(theta_coordinate_to_profile), + y * cos_angle - x * sin_angle, + x * cos_angle + y * sin_angle, ] ).T From 755e43d145b44b0b509b6daeabcfa14daad54cce Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 3 Sep 2026 02:32:30 +0000 Subject: [PATCH 2/2] perf: VectorYX2D reuses a Grid2D already paired with its mask (#598) The @to_vector_yx decorator hands the input Grid2D straight into VectorYX2D, which re-converted its values and rebuilt a second Grid2D on every transform-decorated profile evaluation. Reuse the grid when it is already a Grid2D on the same mask (and native storage is not requested); other inputs take the existing conversion path. One Grid2D construction per deflection call instead of two; results unchanged. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Re4JLhTCFnGYeez8oEyM6M --- autoarray/structures/vectors/uniform.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/autoarray/structures/vectors/uniform.py b/autoarray/structures/vectors/uniform.py index 36e8520b5..510de6cee 100644 --- a/autoarray/structures/vectors/uniform.py +++ b/autoarray/structures/vectors/uniform.py @@ -140,11 +140,19 @@ def __init__( grid_2d=values, mask_2d=mask, store_native=store_native ) - grid = grid_2d_util.convert_grid_2d( - grid_2d=grid, mask_2d=mask, store_native=store_native - ) + # A `Grid2D` already paired with this mask is the grid the vectors are located on: the + # `@to_vector_yx` decorator hands the input grid straight through, so re-converting its + # values and rebuilding a second `Grid2D` on every profile evaluation would only pay the + # constructor twice for the same object. + if isinstance(grid, Grid2D) and grid.mask is mask and not store_native: + self.grid = grid + else: + grid = grid_2d_util.convert_grid_2d( + grid_2d=grid, mask_2d=mask, store_native=store_native + ) + + self.grid = Grid2D(values=grid, mask=mask) - self.grid = Grid2D(values=grid, mask=mask) self.mask = mask super().__init__(values)