Skip to content

Commit cb91b76

Browse files
Jammy2211claude
andcommitted
perf(numba): drop the redundant passes in curvature-matrix F assembly (#505)
Three redundant passes over F in the numba sparse imaging inversion, none of which changed a single value: 1. A global `curvature_matrix_mirrored_from` ran over the whole (P, P) matrix after assembly. Every block was already symmetric or had a known transpose: the mapper x mapper blocks are folded and mirrored inside `curvature_matrix_via_sparse_operator_from`, and the linear-func x linear-func blocks already wrote both triangles. The mirror is removed and the two off-diagonal block writers (mapper x mapper, mapper x linear-func) now place their transpose alongside the block, so F leaves assembly symmetric. 2. `_curvature_matrix_mapper_diag` wrapped the three `mapper.unique_mappings` arrays in `np.array(...)`, copying them on every evaluation. They are already contiguous ndarrays of the dtype the numba kernel wants (see `UniqueMappings.__init__`, which casts on construction) and every other caller in this module passes them through untouched. 3. `_curvature_matrix_mapper_func_blocks_from` formed `operated_mapping_matrix / noise_map ** 2` inside the mapper loop and then copied the result again with `np.array`. The weights do not depend on the mapper, so they are formed once per linear func ahead of the loop and passed straight to the kernel. `curvature_matrix_mirrored_from` in `inversion_imaging_numba_util` had no remaining caller (in this repo or downstream) and is deleted rather than left as dead numba code. Also corrects the `curvature_matrix` docstring: it claimed the property is "not a cached property" and is overwritten in memory by the regularization add, but it is decorated `@cached_property` and `curvature_reg_matrix` adds out-of-place via `np.add`. Bit-identical output verified by capturing `inversion.curvature_matrix` before and after on the autolens_profiling breakdown fiducial: `np.array_equal` True for both euclid and hst, with the figures of merit matching the pins exactly (hst 27661.910133664103, euclid 6213.3068738858765). `pytest test_autoarray/inversion`: 392 passed. Step 1 of #505. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fr6iJ5T1RDARWfxttCuGkK
1 parent dedde5a commit cb91b76

2 files changed

Lines changed: 35 additions & 41 deletions

File tree

‎autoarray/inversion/inversion/imaging_numba/inversion_imaging_numba_util.py‎

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -492,26 +492,6 @@ def curvature_matrix_with_added_to_diag_from(
492492
return curvature_matrix
493493

494494

495-
@numba_util.jit()
496-
def curvature_matrix_mirrored_from(
497-
curvature_matrix: np.ndarray,
498-
) -> np.ndarray:
499-
curvature_matrix_mirrored = np.zeros(
500-
(curvature_matrix.shape[0], curvature_matrix.shape[1])
501-
)
502-
503-
for i in range(curvature_matrix.shape[0]):
504-
for j in range(curvature_matrix.shape[1]):
505-
if curvature_matrix[i, j] != 0:
506-
curvature_matrix_mirrored[i, j] = curvature_matrix[i, j]
507-
curvature_matrix_mirrored[j, i] = curvature_matrix[i, j]
508-
if curvature_matrix[j, i] != 0:
509-
curvature_matrix_mirrored[i, j] = curvature_matrix[j, i]
510-
curvature_matrix_mirrored[j, i] = curvature_matrix[j, i]
511-
512-
return curvature_matrix_mirrored
513-
514-
515495
@numba_util.jit()
516496
def curvature_matrix_via_sparse_operator_from(
517497
psf_precision_operator: np.ndarray,

‎autoarray/inversion/inversion/imaging_numba/sparse.py‎

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -332,10 +332,14 @@ def curvature_matrix(self) -> np.ndarray:
332332
for simultaneously. In the w-tilde formalism this requires us to consider the mappings between data and every
333333
linear object, meaning that the linear alegbra has both on and off diagonal terms.
334334
335-
The `curvature_matrix` computed here is overwritten in memory when the regularization matrix is added to it,
336-
because for large matrices this avoids overhead. For this reason, `curvature_matrix` is not a cached property
337-
to ensure if we access it after computing the `curvature_reg_matrix` it is correctly recalculated in a new
338-
array of memory.
335+
Every block of F is written into the matrix already symmetrized: the mapper x mapper blocks are
336+
folded and mirrored inside `curvature_matrix_via_sparse_operator_from`, and the off-diagonal
337+
blocks (mapper x mapper, mapper x linear-func, linear-func x linear-func) are each placed
338+
together with their transpose. A global symmetrizing pass over the assembled matrix would
339+
therefore be a no-op, and is not run.
340+
341+
`curvature_matrix` is a cached property, and `curvature_reg_matrix` adds the regularization
342+
matrix to it out-of-place, so the cached F is never overwritten by that addition.
339343
"""
340344
if self.has(cls=AbstractLinearObjFuncList):
341345
curvature_matrix = self._curvature_matrix_func_list_and_mapper
@@ -344,10 +348,6 @@ def curvature_matrix(self) -> np.ndarray:
344348
else:
345349
curvature_matrix = self._curvature_matrix_multi_mapper
346350

347-
curvature_matrix = inversion_imaging_numba_util.curvature_matrix_mirrored_from(
348-
curvature_matrix=curvature_matrix,
349-
)
350-
351351
if len(self.no_regularization_index_list) > 0:
352352
curvature_matrix = (
353353
inversion_imaging_numba_util.curvature_matrix_with_added_to_diag_from(
@@ -386,11 +386,9 @@ def _curvature_matrix_mapper_diag(self) -> Optional[np.ndarray]:
386386
psf_precision_operator=self.sparse_operator.psf_precision_operator_sparse,
387387
psf_precision_indexes=self.sparse_operator.indexes,
388388
psf_precision_lengths=self.sparse_operator.lengths,
389-
data_to_pix_unique=np.array(
390-
mapper_i.unique_mappings.data_to_pix_unique
391-
),
392-
data_weights=np.array(mapper_i.unique_mappings.data_weights),
393-
pix_lengths=np.array(mapper_i.unique_mappings.pix_lengths),
389+
data_to_pix_unique=mapper_i.unique_mappings.data_to_pix_unique,
390+
data_weights=mapper_i.unique_mappings.data_weights,
391+
pix_lengths=mapper_i.unique_mappings.pix_lengths,
394392
pix_pixels=mapper_i.params,
395393
)
396394

@@ -493,6 +491,11 @@ def _curvature_matrix_multi_mapper(self) -> np.ndarray:
493491
mapper_param_range_j[0] : mapper_param_range_j[1],
494492
] = off_diag
495493

494+
curvature_matrix[
495+
mapper_param_range_j[0] : mapper_param_range_j[1],
496+
mapper_param_range_i[0] : mapper_param_range_i[1],
497+
] = off_diag.T
498+
496499
return curvature_matrix
497500

498501
@property
@@ -540,8 +543,9 @@ def _curvature_matrix_mapper_func_blocks_from(
540543
noise-weighted curvature vector of a linear function (the dense sliding-window correlation in
541544
`curvature_matrix_off_diags_via_mapper_and_linear_func_curvature_vector_from`).
542545
543-
Only the `[mapper, linear_func]` blocks are written; their transposes are filled in by the
544-
global mirror applied in `curvature_matrix`.
546+
Each `[mapper, linear_func]` block is written together with its transpose into the
547+
`[linear_func, mapper]` block, so F leaves this helper symmetric and no global mirroring
548+
pass is required.
545549
546550
Parameters
547551
----------
@@ -557,24 +561,29 @@ def _curvature_matrix_mapper_func_blocks_from(
557561
cls=AbstractLinearObjFuncList
558562
)
559563

564+
# The noise-weighted curvature weights of a linear func do not depend on the mapper, so
565+
# they are formed once per linear func rather than once per (mapper, linear func) pair.
566+
curvature_weights_list = [
567+
np.array(
568+
self.linear_func_operated_mapping_matrix_dict[linear_func]
569+
/ self.noise_map[:, None] ** 2
570+
)
571+
for linear_func in linear_func_list
572+
]
573+
560574
for i in range(len(mapper_list)):
561575
mapper = mapper_list[i]
562576
mapper_param_range = mapper_param_range_list[i]
563577

564578
for func_index, linear_func in enumerate(linear_func_list):
565579
linear_func_param_range = linear_func_param_range_list[func_index]
566580

567-
data_linear_func_matrix = (
568-
self.linear_func_operated_mapping_matrix_dict[linear_func]
569-
/ self.noise_map[:, None] ** 2
570-
)
571-
572581
off_diag = inversion_imaging_numba_util.curvature_matrix_off_diags_via_mapper_and_linear_func_curvature_vector_from(
573582
data_to_pix_unique=mapper.unique_mappings.data_to_pix_unique,
574583
data_weights=mapper.unique_mappings.data_weights,
575584
pix_lengths=mapper.unique_mappings.pix_lengths,
576585
pix_pixels=mapper.params,
577-
curvature_weights=np.array(data_linear_func_matrix),
586+
curvature_weights=curvature_weights_list[func_index],
578587
mask=self.mask.array,
579588
psf_kernel=self.psf.kernel.native.array,
580589
)
@@ -584,6 +593,11 @@ def _curvature_matrix_mapper_func_blocks_from(
584593
linear_func_param_range[0] : linear_func_param_range[1],
585594
] = off_diag
586595

596+
curvature_matrix[
597+
linear_func_param_range[0] : linear_func_param_range[1],
598+
mapper_param_range[0] : mapper_param_range[1],
599+
] = off_diag.T
600+
587601
return curvature_matrix
588602

589603
def _curvature_matrix_func_func_blocks_from(

0 commit comments

Comments
 (0)