44from fluidsim .operators .coord_system3d import CoordSystem3DConverter
55
66# Use a 3D grid of points in Cartesian coordinates.
7- # Avoid x=y=0 (the z-axis) to keep cylindrical/spherical angles well-defined.
7+ # warning: x=y=0 (the z-axis) is particular, cylindrical/spherical not well-defined.
88_n = 4
9- _x1d = np .linspace (1 .0 , 2 .0 , _n )
10- _y1d = np .linspace (0.5 , 1.5 , _n )
9+ _x1d = np .linspace (0 .0 , 1 .0 , _n )
10+ _y1d = np .linspace (0.0 , 1.0 , _n )
1111_z1d = np .linspace (- 1.0 , 1.0 , _n )
1212_z , _y , _x = np .meshgrid (_z1d , _y1d , _x1d , indexing = "ij" )
1313shape = _x .shape
14+ EPSILON = 1e-12
1415
1516
1617@pytest .fixture (scope = "module" )
@@ -25,9 +26,10 @@ def r_h():
2526
2627
2728@pytest .fixture (scope = "module" )
28- def r_sph ():
29+ def r_sph_not0 ():
2930 """Spherical radius sqrt(x^2 + y^2 + z^2)."""
30- return np .sqrt (_x ** 2 + _y ** 2 + _z ** 2 )
31+ r_sph = np .sqrt (_x ** 2 + _y ** 2 + _z ** 2 )
32+ return np .where (r_sph != 0 , r_sph , EPSILON )
3133
3234
3335# ---------------------------------------------------------------------------
@@ -72,25 +74,29 @@ def test_compute_r_theta_origin():
7274 ["pure-radial-h" , "pure-azimuthal" , "pure-vertical" , "pure-spherical-radial" ],
7375)
7476def test_compute_cylindrical_components (
75- vector_kind , converter , r_h , r_sph , allclose
77+ vector_kind , converter , r_h , r_sph_not0 , allclose
7678):
79+ r_h_not0 = np .where (r_h != 0 , r_h , EPSILON )
80+
7781 match vector_kind :
7882 case "pure-radial-h" :
7983 # Unit vector in the horizontal radial direction
80- vx = _x / r_h
81- vy = _y / r_h
84+ vx = _x / r_h_not0
85+ vy = _y / r_h_not0
8286 vz = np .zeros (shape )
8387 vh_exp = np .ones (shape )
88+ vh_exp [r_h == 0 ] = 0
8489 vt_exp = np .zeros (shape )
8590 vz_exp = np .zeros (shape )
8691
8792 case "pure-azimuthal" :
8893 # Unit vector in the azimuthal direction: (-y, x, 0) / r_h
89- vx = - _y / r_h
90- vy = _x / r_h
94+ vx = - _y / r_h_not0
95+ vy = _x / r_h_not0
9196 vz = np .zeros (shape )
9297 vh_exp = np .zeros (shape )
9398 vt_exp = np .ones (shape )
99+ vt_exp [r_h == 0 ] = 0
94100 vz_exp = np .zeros (shape )
95101
96102 case "pure-vertical" :
@@ -103,14 +109,14 @@ def test_compute_cylindrical_components(
103109 vz_exp = np .ones (shape )
104110
105111 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+ # Unit vector in the spherical radial direction: (x, y, z) / r_sph_not0
113+ # Cylindrical decomposition: vh = r_h/r_sph_not0 , vt = 0, vz = z/r_sph_not0
114+ vx = _x / r_sph_not0
115+ vy = _y / r_sph_not0
116+ vz = _z / r_sph_not0
117+ vh_exp = r_h / r_sph_not0
112118 vt_exp = np .zeros (shape )
113- vz_exp = _z / r_sph
119+ vz_exp = _z / r_sph_not0
114120
115121 case _:
116122 raise ValueError (f"Unknown vector_kind: { vector_kind } " )
@@ -126,12 +132,14 @@ def test_compute_cylindrical_components(
126132# ---------------------------------------------------------------------------
127133
128134
129- def test_cylindrical_preserves_norm (converter , allclose ):
135+ def test_cylindrical_preserves_norm (converter , r_h , allclose ):
130136 """Cylindrical conversion is a rotation: it must preserve the vector norm."""
131137 rng = np .random .default_rng (0 )
132138 vx = rng .standard_normal (shape )
133139 vy = rng .standard_normal (shape )
134140 vz = rng .standard_normal (shape )
141+ vx [r_h == 0 ] = 0
142+ vy [r_h == 0 ] = 0
135143
136144 norm2_cart = vx ** 2 + vy ** 2 + vz ** 2
137145 vh , vt , vz_out = converter .compute_cylindrical_components (vx , vy , vz )
@@ -145,19 +153,22 @@ def test_cylindrical_preserves_norm(converter, allclose):
145153# ---------------------------------------------------------------------------
146154
147155
148- def test_compute_radial_component_pure_radial (converter , r_sph , allclose ):
156+ def test_compute_radial_component_pure_radial (converter , r_sph_not0 , allclose ):
149157 """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
158+ vx = _x / r_sph_not0
159+ vy = _y / r_sph_not0
160+ vz = _z / r_sph_not0
153161 vr = converter .compute_radial_component (vx , vy , vz )
154162 assert allclose (vr , np .ones (shape ))
155163
156164
157165def test_compute_radial_component_pure_azimuthal (converter , r_h , allclose ):
158166 """A pure azimuthal unit vector is perpendicular to r_h → radial component 0."""
159- vx = - _y / r_h
160- vy = _x / r_h
167+
168+ r_h_not0 = np .where (r_h != 0 , r_h , EPSILON )
169+
170+ vx = - _y / r_h_not0
171+ vy = _x / r_h_not0
161172 vz = np .zeros (shape )
162173 vr = converter .compute_radial_component (vx , vy , vz )
163174 assert allclose (vr , np .zeros (shape ))
@@ -173,35 +184,39 @@ def test_compute_radial_component_pure_azimuthal(converter, r_h, allclose):
173184 ["pure-spherical-radial" , "pure-azimuthal" , "pure-polar" ],
174185)
175186def test_compute_spherical_components (
176- vector_kind , converter , r_h , r_sph , allclose
187+ vector_kind , converter , r_h , r_sph_not0 , allclose
177188):
189+ r_h_not0 = np .where (r_h != 0 , r_h , EPSILON )
190+
178191 match vector_kind :
179192 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
193+ # Unit vector along spherical r: (x, y, z)/r_sph_not0
194+ vx = _x / r_sph_not0
195+ vy = _y / r_sph_not0
196+ vz = _z / r_sph_not0
184197 vr_exp = np .ones (shape )
185198 vt_exp = np .zeros (shape ) # azimuthal
186199 vp_exp = np .zeros (shape ) # polar
187200
188201 case "pure-azimuthal" :
189202 # Unit vector along azimuthal phi: (-y, x, 0)/r_h
190- vx = - _y / r_h
191- vy = _x / r_h
203+ vx = - _y / r_h_not0
204+ vy = _x / r_h_not0
192205 vz = np .zeros (shape )
193206 vr_exp = np .zeros (shape )
194207 vt_exp = np .ones (shape )
208+ vt_exp [r_h == 0 ] = 0
195209 vp_exp = np .zeros (shape )
196210
197211 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 )
212+ # Unit vector along polar theta (e_theta): (x*z, y*z, -r_h^2) / (r_sph_not0 * r_h)
213+ vx = _x * _z / (r_sph_not0 * r_h_not0 )
214+ vy = _y * _z / (r_sph_not0 * r_h_not0 )
215+ vz = - (r_h ** 2 ) / (r_sph_not0 * r_h_not0 )
202216 vr_exp = np .zeros (shape )
203217 vt_exp = np .zeros (shape )
204218 vp_exp = np .ones (shape )
219+ vp_exp [r_h == 0 ] = 0
205220
206221 case _:
207222 raise ValueError (f"Unknown vector_kind: { vector_kind } " )
@@ -212,21 +227,24 @@ def test_compute_spherical_components(
212227 assert allclose (vp , vp_exp ), f"vp mismatch for { vector_kind } "
213228
214229
215- def test_spherical_preserves_norm (converter , allclose ):
230+ def test_spherical_preserves_norm (converter , r_h , allclose ):
216231 """Spherical conversion is a rotation: it must preserve the vector norm."""
217232 rng = np .random .default_rng (1 )
218233 vx = rng .standard_normal (shape )
219234 vy = rng .standard_normal (shape )
220235 vz = rng .standard_normal (shape )
221236
237+ vx [r_h == 0 ] = 0
238+ vy [r_h == 0 ] = 0
239+
222240 norm2_cart = vx ** 2 + vy ** 2 + vz ** 2
223241 vr , vt , vp = converter .compute_spherical_components (vx , vy , vz )
224242 norm2_sph = vr ** 2 + vt ** 2 + vp ** 2
225243
226244 assert allclose (norm2_sph , norm2_cart )
227245
228246
229- def test_spherical_radial_equals_radial_component (converter , allclose ):
247+ def test_spherical_radial_equals_radial_component (converter , r_h , allclose ):
230248 """The spherical vr component must equal compute_radial_component."""
231249 rng = np .random .default_rng (2 )
232250 vx = rng .standard_normal (shape )
0 commit comments