|
| 1 | +import json |
| 2 | + |
| 3 | +import numpy as np |
| 4 | +import pytest |
| 5 | +from scipy.sparse.linalg import splu |
| 6 | + |
| 7 | +from ...diffgeo import compute_divergence, compute_geodesic_f, compute_gradient |
| 8 | +from ...heat import diffusion |
| 9 | +from ...solver import Solver |
| 10 | +from ...tria_mesh import TriaMesh |
| 11 | + |
| 12 | + |
| 13 | +@pytest.fixture |
| 14 | +def loaded_data(): |
| 15 | + """ |
| 16 | + Load and provide the expected outcomes data from a JSON file. |
| 17 | +
|
| 18 | + Returns: |
| 19 | + dict: Dictionary containing the expected outcomes data. |
| 20 | + """ |
| 21 | + with open("lapy/utils/tests/expected_outcomes.json", "r") as f: |
| 22 | + expected_outcomes = json.load(f) |
| 23 | + return expected_outcomes |
| 24 | + |
| 25 | + |
| 26 | +# Fixture to load the TetMesh |
| 27 | +@pytest.fixture |
| 28 | +def load_square_mesh(): |
| 29 | + T = TriaMesh.read_off("data/square-mesh.off") |
| 30 | + return T |
| 31 | + |
| 32 | + |
| 33 | +def test_tria_qualities(load_square_mesh): |
| 34 | + """ |
| 35 | + Test triangle mesh quality computation. |
| 36 | + """ |
| 37 | + T = load_square_mesh |
| 38 | + computed_q = T.tria_qualities() |
| 39 | + expected_q_length = 768 |
| 40 | + assert len(computed_q) == expected_q_length |
| 41 | + |
| 42 | + |
| 43 | +# Laplace |
| 44 | +def test_Laplace_Geodesics(load_square_mesh): |
| 45 | + """ |
| 46 | + Test Laplace solver for geodesics on a mesh. |
| 47 | + """ |
| 48 | + |
| 49 | + T = load_square_mesh |
| 50 | + |
| 51 | + # compute first eigenfunction |
| 52 | + fem = Solver(T, lump=True) |
| 53 | + eval, evec = fem.eigs() |
| 54 | + # vfunc = evec[:, 1] |
| 55 | + |
| 56 | + # Get A,B (lumped), and inverse of B (as it is diagonal due to lumping) |
| 57 | + A, B = fem.stiffness, fem.mass |
| 58 | + Bi = B.copy() |
| 59 | + Bi.data **= -1 |
| 60 | + |
| 61 | + assert B.sum() == 1.0 |
| 62 | + assert Bi is not B |
| 63 | + # Convert A to a dense NumPy array |
| 64 | + A_dense = A.toarray() |
| 65 | + |
| 66 | + # Assert that A is symmetric |
| 67 | + assert (A_dense == A_dense.T).all() |
| 68 | + |
| 69 | + expected_eval_length = 10 |
| 70 | + assert len(eval) == expected_eval_length |
| 71 | + |
| 72 | + |
| 73 | +# Geodesics |
| 74 | +def test_Laplace_Geodesics_with_Gradient_Divergence(load_square_mesh): |
| 75 | + """ |
| 76 | + Test Laplace geodesics using gradient and divergence. |
| 77 | + """ |
| 78 | + T = load_square_mesh |
| 79 | + |
| 80 | + # Load eigenfunction |
| 81 | + fem = Solver(T, lump=True) |
| 82 | + eval, evec = fem.eigs() |
| 83 | + vfunc = evec[:, 1] |
| 84 | + |
| 85 | + # Compute Laplacian using -div(grad(f)) |
| 86 | + grad = compute_gradient(T, vfunc) |
| 87 | + divx = -compute_divergence(T, grad) |
| 88 | + |
| 89 | + # Get the lumped mass matrix B |
| 90 | + fem = Solver(T, lump=True) |
| 91 | + B = fem.mass |
| 92 | + Bi = B.copy() |
| 93 | + Bi.data **= -1 |
| 94 | + |
| 95 | + # Apply Laplacian operator and then the inverse of B |
| 96 | + Laplacian_result = -divx # The Laplacian result |
| 97 | + |
| 98 | + # Apply the inverse of B to recover vfunc |
| 99 | + recovered_vfunc = Bi.dot(Laplacian_result) |
| 100 | + |
| 101 | + # Check if the original vfunc and the recovered vfunc length are equal |
| 102 | + assert len(recovered_vfunc) == len(vfunc) |
| 103 | + |
| 104 | + expected_eval_length = 10 |
| 105 | + assert len(eval) == expected_eval_length |
| 106 | + |
| 107 | + |
| 108 | +def test_heat_diffusion_shape(load_square_mesh): |
| 109 | + """ |
| 110 | + Test the shape of the heat diffusion result on a square mesh. |
| 111 | +
|
| 112 | + Parameters: |
| 113 | + load_square_mesh: Fixture providing a loaded square mesh. |
| 114 | +
|
| 115 | + This test function computes the heat diffusion and verifies that the shape |
| 116 | + of the result matches the expected shape. |
| 117 | +
|
| 118 | + Returns: |
| 119 | + None |
| 120 | + """ |
| 121 | + T = load_square_mesh |
| 122 | + bvert = T.boundary_loops() |
| 123 | + u = diffusion(T, bvert, m=1) |
| 124 | + expected_shape = (len(T.v),) |
| 125 | + assert u.shape == expected_shape |
| 126 | + |
| 127 | + |
| 128 | +def test_Geodesics_format(loaded_data, load_square_mesh): |
| 129 | + """ |
| 130 | + Test geodesics format and accuracy. |
| 131 | + """ |
| 132 | + T = load_square_mesh |
| 133 | + bvert = T.boundary_loops() |
| 134 | + u = diffusion(T, bvert, m=1) |
| 135 | + # compute gradient of heat diffusion |
| 136 | + tfunc = compute_gradient(T, u) |
| 137 | + |
| 138 | + # normalize gradient |
| 139 | + X = -tfunc / np.sqrt((tfunc**2).sum(1))[:, np.newaxis] |
| 140 | + X = np.nan_to_num(X) |
| 141 | + divx = compute_divergence(T, X) |
| 142 | + # compute distance |
| 143 | + |
| 144 | + useCholmod = True |
| 145 | + try: |
| 146 | + from sksparse.cholmod import cholesky |
| 147 | + except ImportError: |
| 148 | + useCholmod = False |
| 149 | + |
| 150 | + fem = Solver(T, lump=True) |
| 151 | + A, B = fem.stiffness, fem.mass |
| 152 | + H = -A |
| 153 | + b0 = divx |
| 154 | + |
| 155 | + # solve H x = b0 |
| 156 | + # we don't need the B matrix here, as divx is the integrated divergence |
| 157 | + print("Matrix Format now: " + H.getformat()) |
| 158 | + if useCholmod: |
| 159 | + print("Solver: cholesky decomp - performance optimal ...") |
| 160 | + chol = cholesky(H) |
| 161 | + x = chol(b0) |
| 162 | + else: |
| 163 | + print("Solver: spsolve (LU decomp) - performance not optimal ...") |
| 164 | + lu = splu(H) |
| 165 | + x = lu.solve(b0) |
| 166 | + |
| 167 | + # remove shift |
| 168 | + x = x - min(x) |
| 169 | + |
| 170 | + Bi = B.copy() |
| 171 | + vf = fem.poisson(-Bi * divx) |
| 172 | + vf = vf - min(vf) |
| 173 | + gf = compute_geodesic_f(T, u) |
| 174 | + expected_matrix_format = loaded_data["expected_outcomes"]["test_Geodesics_format"][ |
| 175 | + "expected_matrix_format" |
| 176 | + ] |
| 177 | + assert H.getformat() == expected_matrix_format |
| 178 | + assert not useCholmod, "Solver: cholesky decomp - performance optimal ..." |
| 179 | + expected_max_x = loaded_data["expected_outcomes"]["test_Geodesics_format"][ |
| 180 | + "max_distance" |
| 181 | + ] |
| 182 | + expected_sqrt_2_div_2 = loaded_data["expected_outcomes"]["test_Geodesics_format"][ |
| 183 | + "expected_sqrt_2_div_2" |
| 184 | + ] |
| 185 | + assert np.isclose(max(x), expected_max_x) |
| 186 | + computed_sqrt_2_div_2 = np.sqrt(2) / 2 |
| 187 | + assert np.isclose(computed_sqrt_2_div_2, expected_sqrt_2_div_2) |
| 188 | + expected_max_abs_diff = loaded_data["expected_outcomes"]["test_Geodesics_format"][ |
| 189 | + "expected_max_abs_diff" |
| 190 | + ] |
| 191 | + computed_max_abs_diff = max(abs(gf - x)) |
| 192 | + assert np.allclose(computed_max_abs_diff, expected_max_abs_diff) |
0 commit comments