|
| 1 | +import warnings |
| 2 | + |
1 | 3 | import numpy as np |
2 | 4 | import pytest |
3 | 5 |
|
@@ -676,19 +678,143 @@ def test__log_det_method__slogdet_is_finite_where_cholesky_fails_on_non_positive |
676 | 678 | assert result == pytest.approx(np.linalg.slogdet(matrix)[1], 1.0e-8) |
677 | 679 |
|
678 | 680 |
|
679 | | -def test__reconstruction_noise_map__asymmetric_curvature_reg_matrix__correct_diagonal_noise_values(): |
| 681 | +def test__reconstruction_noise_map__correct_diagonal_noise_values(): |
680 | 682 | curvature_reg_matrix = np.array([[1.0, 1.0, 1.0], [1.0, 2.0, 1.0], [1.0, 1.0, 3.0]]) |
681 | 683 |
|
682 | 684 | inversion = aa.m.MockInversion(curvature_reg_matrix=curvature_reg_matrix) |
683 | 685 |
|
684 | | - assert inversion.reconstruction_noise_map_with_covariance[0, 0] == pytest.approx( |
685 | | - np.sqrt(2.5), 1.0e-2 |
686 | | - ) |
| 686 | + assert inversion.reconstruction_covariance_matrix[0, 0] == pytest.approx(2.5, 1.0e-2) |
687 | 687 | assert inversion.reconstruction_noise_map == pytest.approx( |
688 | 688 | np.sqrt(np.array([2.5, 1.0, 0.5])), 1.0e-3 |
689 | 689 | ) |
690 | 690 |
|
691 | 691 |
|
| 692 | +def test__reconstruction_covariance_matrix__off_diagonals_are_finite_and_negative(): |
| 693 | + """ |
| 694 | + The off-diagonal entries of a covariance matrix are covariances and are routinely negative. |
| 695 | +
|
| 696 | + `reconstruction_covariance_matrix` previously applied `np.sqrt` elementwise to the whole inverse, so every |
| 697 | + negative off-diagonal became NaN by construction -- for any matrix, however well-conditioned -- while |
| 698 | + emitting `RuntimeWarning: invalid value encountered in sqrt`. Only the [0, 0] diagonal element was asserted, |
| 699 | + so nothing caught it. |
| 700 | + """ |
| 701 | + curvature_reg_matrix = np.array([[1.0, 1.0, 1.0], [1.0, 2.0, 1.0], [1.0, 1.0, 3.0]]) |
| 702 | + |
| 703 | + inversion = aa.m.MockInversion(curvature_reg_matrix=curvature_reg_matrix) |
| 704 | + |
| 705 | + with warnings.catch_warnings(): |
| 706 | + warnings.simplefilter("error", RuntimeWarning) |
| 707 | + covariance = inversion.reconstruction_covariance_matrix |
| 708 | + |
| 709 | + assert np.all(np.isfinite(covariance)) |
| 710 | + |
| 711 | + # this matrix has anti-correlated pixels, so the off-diagonals are genuinely negative |
| 712 | + assert covariance[0, 1] < 0.0 |
| 713 | + assert covariance == pytest.approx(np.linalg.inv(curvature_reg_matrix), 1.0e-8) |
| 714 | + |
| 715 | + |
| 716 | +def test__reconstruction_covariance_matrix__is_accurate_and_symmetric_when_ill_conditioned(): |
| 717 | + """ |
| 718 | + Ground truth is exact by construction: for `A = Q diag(w) Q.T` the inverse is `Q diag(1/w) Q.T`. |
| 719 | +
|
| 720 | + The symmetry half only guards the symmetrization line -- `0.5 * (C + C.T)` is bitwise symmetric for any C -- |
| 721 | + so the accuracy assertion against the constructed truth is what tests the factorization itself. |
| 722 | + """ |
| 723 | + rng = np.random.default_rng(1234) |
| 724 | + q, _ = np.linalg.qr(rng.standard_normal((25, 25))) |
| 725 | + eigenvalues = np.logspace(0, 9, 25) |
| 726 | + |
| 727 | + curvature_reg_matrix = (q * eigenvalues) @ q.T |
| 728 | + curvature_reg_matrix = 0.5 * (curvature_reg_matrix + curvature_reg_matrix.T) |
| 729 | + |
| 730 | + covariance_true = (q * (1.0 / eigenvalues)) @ q.T |
| 731 | + |
| 732 | + inversion = aa.m.MockInversion(curvature_reg_matrix=curvature_reg_matrix) |
| 733 | + |
| 734 | + covariance = inversion.reconstruction_covariance_matrix |
| 735 | + |
| 736 | + # cond ~ 1e9, so the achievable accuracy is eps * cond ~ 2e-7; the measured error is ~3e-9. This is not a |
| 737 | + # claim that Cholesky beats LU here -- it does not, `np.linalg.inv` measures ~7e-10 on this matrix. |
| 738 | + assert covariance == pytest.approx(covariance_true, abs=1.0e-7) |
| 739 | + assert covariance == pytest.approx(covariance.T, abs=1.0e-15) |
| 740 | + |
| 741 | + |
| 742 | +def test__reconstruction_covariance_matrix__asymmetric_input_is_symmetrized_not_silently_upper_triangle(): |
| 743 | + """ |
| 744 | + `cho_factor` reads only the upper triangle, so an asymmetric input would be inverted as though its lower |
| 745 | + triangle matched its upper -- silently, and differing from the true inverse. |
| 746 | + """ |
| 747 | + curvature_reg_matrix = np.array([[2.0, 0.5], [0.1, 2.0]]) |
| 748 | + symmetrized = 0.5 * (curvature_reg_matrix + curvature_reg_matrix.T) |
| 749 | + |
| 750 | + inversion = aa.m.MockInversion(curvature_reg_matrix=curvature_reg_matrix) |
| 751 | + |
| 752 | + assert inversion.reconstruction_covariance_matrix == pytest.approx( |
| 753 | + np.linalg.inv(symmetrized), 1.0e-8 |
| 754 | + ) |
| 755 | + |
| 756 | + |
| 757 | +def test__reconstruction_covariance_matrix__non_finite_matrix_raises_lin_alg_error(): |
| 758 | + """ |
| 759 | + scipy raises `ValueError` on a non-finite matrix, which the plotting and CSV callers do not catch -- they |
| 760 | + guard on `LinAlgError`. The CSV writer explicitly promises not to abort the enclosing model-fit, so the |
| 761 | + non-finite case is converted rather than allowed to escape. |
| 762 | + """ |
| 763 | + curvature_reg_matrix = np.array([[1.0, np.nan], [np.nan, 2.0]]) |
| 764 | + |
| 765 | + inversion = aa.m.MockInversion(curvature_reg_matrix=curvature_reg_matrix) |
| 766 | + |
| 767 | + with pytest.raises(np.linalg.LinAlgError, match="non-finite"): |
| 768 | + inversion.reconstruction_covariance_matrix |
| 769 | + |
| 770 | + |
| 771 | +def test__reconstruction_noise_map__is_sqrt_of_covariance_diagonal(): |
| 772 | + """ |
| 773 | + The invariant, asserted directly rather than via hand-computed values. |
| 774 | +
|
| 775 | + `reconstruction_noise_map` used to be `np.diagonal(...)` of an already-square-rooted matrix, which was |
| 776 | + correct only incidentally -- because `np.sqrt` is elementwise. It now takes the square root of the |
| 777 | + covariance diagonal itself, so the relationship is stated rather than emergent. |
| 778 | + """ |
| 779 | + curvature_reg_matrix = np.array([[1.0, 1.0, 1.0], [1.0, 2.0, 1.0], [1.0, 1.0, 3.0]]) |
| 780 | + |
| 781 | + inversion = aa.m.MockInversion(curvature_reg_matrix=curvature_reg_matrix) |
| 782 | + |
| 783 | + assert inversion.reconstruction_noise_map == pytest.approx( |
| 784 | + np.sqrt(np.diag(inversion.reconstruction_covariance_matrix)), 1.0e-12 |
| 785 | + ) |
| 786 | + |
| 787 | + |
| 788 | +def test__reconstruction_covariance_matrix__raises_on_a_non_positive_definite_matrix(): |
| 789 | + """ |
| 790 | + A covariance is only defined for a positive-definite matrix. |
| 791 | +
|
| 792 | + `np.linalg.inv` raises only on an exactly singular matrix, so an indefinite `curvature_reg_matrix` returned |
| 793 | + a plausible-looking covariance with no error and no warning. The Cholesky factorization rejects it, and the |
| 794 | + plotting and CSV callers already catch `LinAlgError`. |
| 795 | + """ |
| 796 | + # symmetric, non-singular, but indefinite (eigenvalues +1 and -1) |
| 797 | + curvature_reg_matrix = np.array([[0.0, 1.0], [1.0, 0.0]]) |
| 798 | + |
| 799 | + inversion = aa.m.MockInversion(curvature_reg_matrix=curvature_reg_matrix) |
| 800 | + |
| 801 | + assert np.isfinite(np.linalg.inv(curvature_reg_matrix)).all() # inv is silent here |
| 802 | + |
| 803 | + with pytest.raises(np.linalg.LinAlgError): |
| 804 | + inversion.reconstruction_covariance_matrix |
| 805 | + |
| 806 | + |
| 807 | +def test__reconstruction_noise_map_with_covariance__is_deprecated_alias(): |
| 808 | + curvature_reg_matrix = np.array([[1.0, 1.0, 1.0], [1.0, 2.0, 1.0], [1.0, 1.0, 3.0]]) |
| 809 | + |
| 810 | + inversion = aa.m.MockInversion(curvature_reg_matrix=curvature_reg_matrix) |
| 811 | + |
| 812 | + with pytest.warns(DeprecationWarning, match="reconstruction_covariance_matrix"): |
| 813 | + covariance = inversion.reconstruction_noise_map_with_covariance |
| 814 | + |
| 815 | + assert covariance == pytest.approx(inversion.reconstruction_covariance_matrix, 1.0e-12) |
| 816 | + |
| 817 | + |
692 | 818 | def test__max_pixel_list_from_and_centre__returns_top_pixels_and_brightest_centre(): |
693 | 819 |
|
694 | 820 | source_plane_mesh_grid = aa.Grid2DIrregular( |
|
0 commit comments