Skip to content

Commit 08e2e19

Browse files
authored
fix: apply chain rule in StructuredGrid2D element gradient
fix: apply chain rule in StructuredGrid2D element gradient
2 parents 53897b4 + 3d43f5c commit 08e2e19

2 files changed

Lines changed: 43 additions & 0 deletions

File tree

packages/loop_common/src/loop_common/supports/_2d_structured_grid.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,8 @@ def get_element_gradient_for_location(
460460
T[:, 1, 2] = 1 - local_coords[:, 0]
461461
T[:, 1, 3] = local_coords[:, 0]
462462

463+
T[:, 0, :] /= self.step_vector[None, 0]
464+
T[:, 1, :] /= self.step_vector[None, 1]
463465
return vertices, T, elements, inside
464466

465467
def get_element_for_location(

tests/unit/interpolator/test_2d_discrete_support.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,44 @@ def test_structured_grid2d_vtk_assigns_quad_cell_types():
8080
triangulated = vtk_grid.triangulate()
8181

8282
assert triangulated.n_cells == grid.n_elements * 2
83+
84+
85+
def test_evaluate_gradient_2d_world_units():
86+
"""
87+
Gradient must be returned in world units, independent of the cell size.
88+
89+
Regression test for #289: get_element_gradient_for_location returned the
90+
shape-function derivatives with respect to local (0-1) cell coordinates,
91+
so gradients were inflated by step_vector on each axis.
92+
"""
93+
grid = StructuredGrid2D(
94+
origin=np.zeros(2), nsteps=np.array([10, 10]), step_vector=np.array([2.5, 0.5])
95+
)
96+
# f(x, y) = x and f(x, y) = y have unit gradients regardless of cell size
97+
gradient_x = np.mean(grid.evaluate_gradient(grid.barycentre, grid.nodes[:, 0]), axis=0)
98+
gradient_y = np.mean(grid.evaluate_gradient(grid.barycentre, grid.nodes[:, 1]), axis=0)
99+
assert np.allclose(gradient_x, np.array([1.0, 0.0]))
100+
assert np.allclose(gradient_y, np.array([0.0, 1.0]))
101+
102+
103+
def test_fdi_2d_gradient_resolution_independent():
104+
"""
105+
The interpolated gradient magnitude must not depend on nelements (#289).
106+
"""
107+
from LoopStructural.geometry import BoundingBox
108+
from LoopStructural.interpolators import InterpolatorFactory
109+
110+
sqrt2 = np.sqrt(2.0)
111+
bbox = BoundingBox(dimensions=2, origin=np.array([0.0, 0.0]), maximum=np.array([100.0, 100.0]))
112+
points = np.random.default_rng(0).uniform(10, 90, size=(60, 2))
113+
for nelements in (1e3, 4e3):
114+
interpolator = InterpolatorFactory.create_interpolator(
115+
interpolatortype="FDI", boundingbox=bbox, nelements=nelements
116+
)
117+
interpolator.set_value_constraints(
118+
np.column_stack([points, (points[:, 0] + points[:, 1]) / sqrt2])
119+
)
120+
interpolator.setup_interpolator()
121+
interpolator.solve_system(solver="cg")
122+
gradient_norm = np.linalg.norm(interpolator.evaluate_gradient(np.array([[50.0, 50.0]]))[0])
123+
assert np.isclose(gradient_norm, 1.0, atol=0.05)

0 commit comments

Comments
 (0)