diff --git a/src/CelerisLab/body/jets.py b/src/CelerisLab/body/jets.py index ebd14e9..b32ffaa 100644 --- a/src/CelerisLab/body/jets.py +++ b/src/CelerisLab/body/jets.py @@ -156,6 +156,17 @@ def jet_link_velocity(jets, shape, jet_id): return (amplitude[jet_id][:, None]*shape).astype(np.float32) -def fine_hits_to_physical(hits_local, fine_origin, dx): - """Fine-local cell-index hits -> physical frame: origin + dx*(hit + 1/2).""" +# Two coarse-unit frames coexist (see FixedPatchStepper docs): the patch +# cell-edge frame puts coarse cell I's center at I + 1/2, while Simulation +# geometry uses the integer-node frame with that center at I. Jets are +# evaluated in the frame their region/field were written in; convert hits +# explicitly, never by relabelling. + +def fine_hits_to_edge_frame(hits_local, fine_origin, dx): + """Fine-local cell-index hits -> coarse cell-edge frame: origin + dx*(hit + 1/2).""" return np.asarray(fine_origin, np.float64) + dx*(np.asarray(hits_local, np.float64) + .5) + + +def edge_to_node_frame(points): + """Coarse cell-edge frame -> Simulation integer-node frame (shift by -1/2).""" + return np.asarray(points, np.float64) - .5 diff --git a/tests/unit/test_jets.py b/tests/unit/test_jets.py index 60a7263..4a56c1e 100644 --- a/tests/unit/test_jets.py +++ b/tests/unit/test_jets.py @@ -6,7 +6,8 @@ import numpy as np import pytest from CelerisLab.body.jets import (Jet, Parallel, Radial, Sector, Tube, - fine_hits_to_physical, jet_link_shapes) + edge_to_node_frame, fine_hits_to_edge_frame, + jet_link_shapes) from CelerisLab.body.objects import SurfaceActuation from CelerisLab.lbm.curved_links import CurvedLinkSoA, surface_link_velocity from .test_state_regressions import device, make_field, manager @@ -85,8 +86,36 @@ def test_selection_uses_true_hit_not_clipped_q(): def test_fine_half_cell_frame(): - np.testing.assert_allclose(fine_hits_to_physical([[0., 0.], [3., 1.5]], (4., 6.), .5), - [[4.25, 6.25], [5.75, 7.]], rtol=0, atol=0) + edge = fine_hits_to_edge_frame([[0., 0.], [3., 1.5]], (4., 6.), .5) + np.testing.assert_array_equal(edge, [[4.25, 6.25], [5.75, 7.]]) + np.testing.assert_array_equal(edge_to_node_frame(edge), [[3.75, 5.75], [5.25, 6.5]]) + + +def test_same_physical_hit_in_simulation_and_patch_frames(): + # One physical circle: Simulation node-frame centre C; patch edge-frame + # centre C + 1/2; fine-local centre (C + 1/2 - origin)/dx - 1/2 as in + # prepare_fine_boundary. Fine hits mapped edge -> node must lie on the + # Simulation circle, and one node-frame Jet selects the same arc on both. + from CelerisLab.body.geometry.circle import CircleGeometry + C, R, origin, dx = np.array([10.3, 9.6]), 2.15, np.array([4., 3.]), .5 + local = (C + .5 - origin)/dx - .5 + fine = CircleGeometry(*local, R/dx).build_cut_links(40, 40) + coarse = CircleGeometry(*C, R).build_cut_links(24, 20) + fine_hits = edge_to_node_frame(fine_hits_to_edge_frame( + [(l.hit_x, l.hit_y) for l in fine], origin, dx)) + coarse_hits = np.array([(l.hit_x, l.hit_y) for l in coarse]) + np.testing.assert_allclose(np.hypot(*(fine_hits - C).T), R, rtol=0, atol=1e-12) + np.testing.assert_allclose(np.hypot(*(coarse_hits - C).T), R, rtol=0, atol=1e-12) + jet = Jet(Sector(tuple(C), .4, .5, R + .01, R - .01), Radial(tuple(C), inward=False)) + for hits in (fine_hits, coarse_hits): + shape, jet_id = jet_link_shapes([jet], hits) + d = hits - C + angle = np.arctan2(d[:, 1], d[:, 0]) + assert np.array_equal(jet_id == 0, np.abs(angle - .4) < .5) + np.testing.assert_allclose(shape[jet_id == 0], d[jet_id == 0]/R, atol=1e-12) + # Relabelling instead of converting moves every hit off the circle. + wrong = fine_hits_to_edge_frame([(l.hit_x, l.hit_y) for l in fine], origin, dx) + assert np.abs(np.hypot(*(wrong - C).T) - R).max() > .3 def test_links_are_distinct_by_receiver_and_direction_and_overlap_rejected():