Skip to content

Commit d49fcca

Browse files
committed
coord_system3d: testing
1 parent ccd5051 commit d49fcca

File tree

2 files changed

+218
-36
lines changed

2 files changed

+218
-36
lines changed

fluidsim/operators/coord_system3d.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ def compute_spherical_components(self, vx, vy, vz):
5050
"""Convert (vx, vy, vz) in spherical coordinates (vr, vt, vp)"""
5151
vr = self.compute_radial_component(vx, vy, vz)
5252
vt = (-self.y * vx + self.x * vy) / self.rh_not0
53-
vp = (
54-
(self.z * (self.x * vx + self.y * vy) - self.rh**2 * vz)
55-
/ (self.r_not0 * self.rh_not0),
53+
vp = (self.z * (self.x * vx + self.y * vy) - self.rh**2 * vz) / (
54+
self.r_not0 * self.rh_not0
5655
)
5756
return vr, vt, vp

fluidsim/operators/test/test_coord_system3d.py

Lines changed: 216 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,54 +3,237 @@
33

44
from fluidsim.operators.coord_system3d import CoordSystem3DConverter
55

6-
shape = (4, 4, 4)
6+
# Use a 3D grid of points in Cartesian coordinates.
7+
# Avoid x=y=0 (the z-axis) to keep cylindrical/spherical angles well-defined.
8+
_n = 4
9+
_x1d = np.linspace(1.0, 2.0, _n)
10+
_y1d = np.linspace(0.5, 1.5, _n)
11+
_z1d = np.linspace(-1.0, 1.0, _n)
12+
_z, _y, _x = np.meshgrid(_z1d, _y1d, _x1d, indexing="ij")
13+
shape = _x.shape
714

815

916
@pytest.fixture(scope="module")
1017
def converter():
11-
# TODO: fix this
12-
x = np.zeros(shape)
13-
y = np.zeros(shape)
14-
z = np.zeros(shape)
18+
return CoordSystem3DConverter(_x, _y, _z)
1519

16-
return CoordSystem3DConverter(x, y, z)
20+
21+
@pytest.fixture(scope="module")
22+
def r_h():
23+
"""Horizontal (cylindrical) radius sqrt(x^2 + y^2)."""
24+
return np.sqrt(_x**2 + _y**2)
25+
26+
27+
@pytest.fixture(scope="module")
28+
def r_sph():
29+
"""Spherical radius sqrt(x^2 + y^2 + z^2)."""
30+
return np.sqrt(_x**2 + _y**2 + _z**2)
31+
32+
33+
# ---------------------------------------------------------------------------
34+
# compute_r_theta
35+
# ---------------------------------------------------------------------------
36+
37+
38+
def test_compute_r_theta_range(converter):
39+
"""r_theta must lie in [-pi, pi]."""
40+
r_theta = converter.compute_r_theta()
41+
assert np.all(r_theta >= -np.pi)
42+
assert np.all(r_theta <= np.pi)
43+
44+
45+
def test_compute_r_theta_values(converter, allclose):
46+
"""r_theta should equal arctan2(y, x)."""
47+
r_theta = converter.compute_r_theta()
48+
expected = np.arctan2(_y, _x)
49+
assert allclose(r_theta, expected)
1750

1851

19-
# TODO: add more values for vector_kind
20-
@pytest.mark.parametrize("vector_kind", ["pure-radial", "pure-rh"])
21-
def test_coord_system_converter(vector_kind, converter, allclose):
52+
def test_compute_r_theta_origin():
53+
"""r_theta must be 0 when x = y = 0 (on the z-axis)."""
54+
x = np.zeros((3,))
55+
y = np.zeros((3,))
56+
z = np.array([1.0, 0.0, -1.0])
57+
conv = CoordSystem3DConverter(x, y, z)
58+
r_theta = conv.compute_r_theta()
59+
assert np.all(r_theta == 0.0)
60+
61+
62+
# ---------------------------------------------------------------------------
63+
# compute_cylindrical_components — pure-radial vector
64+
# ---------------------------------------------------------------------------
65+
# A pure-radial (horizontal) vector points in the direction of (x, y, 0),
66+
# i.e. vx = x/r_h, vy = y/r_h, vz = 0.
67+
# In cylindrical coordinates this should give vh = 1, vt = 0, vz = 0.
68+
69+
70+
@pytest.mark.parametrize(
71+
"vector_kind",
72+
["pure-radial-h", "pure-azimuthal", "pure-vertical", "pure-spherical-radial"],
73+
)
74+
def test_compute_cylindrical_components(
75+
vector_kind, converter, r_h, r_sph, allclose
76+
):
2277
match vector_kind:
23-
case "pure-radial":
24-
# TODO: fix this
25-
vx = np.zeros(shape)
26-
vy = np.zeros(shape)
78+
case "pure-radial-h":
79+
# Unit vector in the horizontal radial direction
80+
vx = _x / r_h
81+
vy = _y / r_h
2782
vz = np.zeros(shape)
28-
case "pure-rh":
29-
# TODO: fix this
83+
vh_exp = np.ones(shape)
84+
vt_exp = np.zeros(shape)
85+
vz_exp = np.zeros(shape)
86+
87+
case "pure-azimuthal":
88+
# Unit vector in the azimuthal direction: (-y, x, 0) / r_h
89+
vx = -_y / r_h
90+
vy = _x / r_h
91+
vz = np.zeros(shape)
92+
vh_exp = np.zeros(shape)
93+
vt_exp = np.ones(shape)
94+
vz_exp = np.zeros(shape)
95+
96+
case "pure-vertical":
97+
# Unit vector along z
3098
vx = np.zeros(shape)
3199
vy = np.zeros(shape)
32-
vz = np.zeros(shape)
100+
vz = np.ones(shape)
101+
vh_exp = np.zeros(shape)
102+
vt_exp = np.zeros(shape)
103+
vz_exp = np.ones(shape)
104+
105+
case "pure-spherical-radial":
106+
# Unit vector in the spherical radial direction: (x, y, z) / r_sph
107+
# Cylindrical decomposition: vh = r_h/r_sph, vt = 0, vz = z/r_sph
108+
vx = _x / r_sph
109+
vy = _y / r_sph
110+
vz = _z / r_sph
111+
vh_exp = r_h / r_sph
112+
vt_exp = np.zeros(shape)
113+
vz_exp = _z / r_sph
114+
33115
case _:
34-
raise ValueError
116+
raise ValueError(f"Unknown vector_kind: {vector_kind}")
117+
118+
vh, vt, vz_out = converter.compute_cylindrical_components(vx, vy, vz)
119+
assert allclose(vh, vh_exp), f"vh mismatch for {vector_kind}"
120+
assert allclose(vt, vt_exp), f"vt mismatch for {vector_kind}"
121+
assert allclose(vz_out, vz_exp), f"vz mismatch for {vector_kind}"
35122

36-
# TODO: call all the methods
37-
# example
38-
vh, vt, vz = converter.compute_cylindrical_components(vx, vy, vz)
39123

40-
# TODO: check the results with allclose
124+
# ---------------------------------------------------------------------------
125+
# compute_cylindrical_components — linearity / inverse
126+
# ---------------------------------------------------------------------------
127+
128+
129+
def test_cylindrical_preserves_norm(converter, allclose):
130+
"""Cylindrical conversion is a rotation: it must preserve the vector norm."""
131+
rng = np.random.default_rng(0)
132+
vx = rng.standard_normal(shape)
133+
vy = rng.standard_normal(shape)
134+
vz = rng.standard_normal(shape)
135+
136+
norm2_cart = vx**2 + vy**2 + vz**2
137+
vh, vt, vz_out = converter.compute_cylindrical_components(vx, vy, vz)
138+
norm2_cyl = vh**2 + vt**2 + vz_out**2
139+
140+
assert allclose(norm2_cyl, norm2_cart)
141+
142+
143+
# ---------------------------------------------------------------------------
144+
# compute_radial_component
145+
# ---------------------------------------------------------------------------
146+
147+
148+
def test_compute_radial_component_pure_radial(converter, r_sph, allclose):
149+
"""A pure horizontal-radial unit vector should have radial component 1."""
150+
vx = _x / r_sph
151+
vy = _y / r_sph
152+
vz = _z / r_sph
153+
vr = converter.compute_radial_component(vx, vy, vz)
154+
assert allclose(vr, np.ones(shape))
155+
156+
157+
def test_compute_radial_component_pure_azimuthal(converter, r_h, allclose):
158+
"""A pure azimuthal unit vector is perpendicular to r_h → radial component 0."""
159+
vx = -_y / r_h
160+
vy = _x / r_h
161+
vz = np.zeros(shape)
162+
vr = converter.compute_radial_component(vx, vy, vz)
163+
assert allclose(vr, np.zeros(shape))
164+
165+
166+
# ---------------------------------------------------------------------------
167+
# compute_spherical_components
168+
# ---------------------------------------------------------------------------
169+
170+
171+
@pytest.mark.parametrize(
172+
"vector_kind",
173+
["pure-spherical-radial", "pure-azimuthal", "pure-polar"],
174+
)
175+
def test_compute_spherical_components(
176+
vector_kind, converter, r_h, r_sph, allclose
177+
):
41178
match vector_kind:
42-
case "pure-radial":
43-
# TODO: fix this
44-
# bad example
45-
assert allclose(vx, vy)
46-
case "pure-rh":
47-
# TODO: fix this
48-
pass
179+
case "pure-spherical-radial":
180+
# Unit vector along spherical r: (x, y, z)/r_sph
181+
vx = _x / r_sph
182+
vy = _y / r_sph
183+
vz = _z / r_sph
184+
vr_exp = np.ones(shape)
185+
vt_exp = np.zeros(shape) # azimuthal
186+
vp_exp = np.zeros(shape) # polar
187+
188+
case "pure-azimuthal":
189+
# Unit vector along azimuthal phi: (-y, x, 0)/r_h
190+
vx = -_y / r_h
191+
vy = _x / r_h
192+
vz = np.zeros(shape)
193+
vr_exp = np.zeros(shape)
194+
vt_exp = np.ones(shape)
195+
vp_exp = np.zeros(shape)
196+
197+
case "pure-polar":
198+
# Unit vector along polar theta (e_theta): (x*z, y*z, -r_h^2) / (r_sph * r_h)
199+
vx = _x * _z / (r_sph * r_h)
200+
vy = _y * _z / (r_sph * r_h)
201+
vz = -(r_h**2) / (r_sph * r_h)
202+
vr_exp = np.zeros(shape)
203+
vt_exp = np.zeros(shape)
204+
vp_exp = np.ones(shape)
205+
49206
case _:
50-
raise ValueError
207+
raise ValueError(f"Unknown vector_kind: {vector_kind}")
51208

209+
vr, vt, vp = converter.compute_spherical_components(vx, vy, vz)
210+
assert allclose(vr, vr_exp), f"vr mismatch for {vector_kind}"
211+
assert allclose(vt, vt_exp), f"vt mismatch for {vector_kind}"
212+
assert allclose(vp, vp_exp), f"vp mismatch for {vector_kind}"
52213

53-
def test_compute_r_theta(converter, allclose):
54-
r_theta = converter.compute_r_theta()
55-
# TODO: fix this
56-
assert allclose(r_theta, r_theta)
214+
215+
def test_spherical_preserves_norm(converter, allclose):
216+
"""Spherical conversion is a rotation: it must preserve the vector norm."""
217+
rng = np.random.default_rng(1)
218+
vx = rng.standard_normal(shape)
219+
vy = rng.standard_normal(shape)
220+
vz = rng.standard_normal(shape)
221+
222+
norm2_cart = vx**2 + vy**2 + vz**2
223+
vr, vt, vp = converter.compute_spherical_components(vx, vy, vz)
224+
norm2_sph = vr**2 + vt**2 + vp**2
225+
226+
assert allclose(norm2_sph, norm2_cart)
227+
228+
229+
def test_spherical_radial_equals_radial_component(converter, allclose):
230+
"""The spherical vr component must equal compute_radial_component."""
231+
rng = np.random.default_rng(2)
232+
vx = rng.standard_normal(shape)
233+
vy = rng.standard_normal(shape)
234+
vz = rng.standard_normal(shape)
235+
236+
vr_sph, _, _ = converter.compute_spherical_components(vx, vy, vz)
237+
vr_direct = converter.compute_radial_component(vx, vy, vz)
238+
239+
assert allclose(vr_sph, vr_direct)

0 commit comments

Comments
 (0)