Skip to content

Commit 2e5d5a7

Browse files
author
Clovis Lambert
committed
creating test file for spatial average operator
1 parent bda71a4 commit 2e5d5a7

1 file changed

Lines changed: 359 additions & 0 deletions

File tree

Lines changed: 359 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,359 @@
1+
"""Tests for spatial_average3d module."""
2+
3+
import pytest
4+
import numpy as np
5+
6+
# from fluidsim.operators.spatial_average3d import SpatialAverage
7+
from ../spatial_average3d import SpatialAverage
8+
9+
10+
# Mock operator for testing (without full fluidsim dependency)
11+
class MockOperator:
12+
"""Minimal operator for testing spatial averaging."""
13+
14+
def __init__(self, nx, ny, nz, Lx, Ly, Lz):
15+
self.nx = nx
16+
self.ny = ny
17+
self.nz = nz
18+
self.Lx = Lx
19+
self.Ly = Ly
20+
self.Lz = Lz
21+
self.delta = Lx / nx # Assume uniform spacing
22+
23+
# Create a simple uniform grid
24+
x1d = np.linspace(0, Lx, nx, endpoint=False)
25+
y1d = np.linspace(0, Ly, ny, endpoint=False)
26+
z1d = np.linspace(-Lz/2, Lz/2, nz, endpoint=False)
27+
28+
self.Z, self.Y, self.X = np.meshgrid(z1d, y1d, x1d, indexing='ij')
29+
self.shapeX_loc = self.X.shape
30+
31+
def get_XYZ_loc(self):
32+
"""Return local coordinates (no MPI in tests)."""
33+
return self.X, self.Y, self.Z
34+
35+
36+
@pytest.fixture(scope="module")
37+
def mock_oper():
38+
"""Create a small 3D operator for testing."""
39+
return MockOperator(nx=8, ny=8, nz=8, Lx=2.0, Ly=2.0, Lz=2.0)
40+
41+
42+
@pytest.fixture(scope="module")
43+
def spatial_avg(mock_oper):
44+
"""Create SpatialAverage instance."""
45+
return SpatialAverage(mock_oper, nr=10, nrh=8, nz=8)
46+
47+
48+
# ---------------------------------------------------------------------------
49+
# Initialization tests
50+
# ---------------------------------------------------------------------------
51+
52+
53+
def test_initialization(spatial_avg, mock_oper):
54+
"""Test that spatial average initializes correctly."""
55+
assert spatial_avg.nr == 10
56+
assert spatial_avg.nrh == 8
57+
assert spatial_avg.nz == 8
58+
assert spatial_avg.X.shape == mock_oper.X.shape
59+
assert hasattr(spatial_avg, 'r')
60+
assert hasattr(spatial_avg, 'rho')
61+
assert hasattr(spatial_avg, 'phi')
62+
63+
64+
def test_coordinate_shapes(spatial_avg, mock_oper):
65+
"""Test that computed coordinates have correct shapes."""
66+
shape = mock_oper.X.shape
67+
assert spatial_avg.r.shape == shape
68+
assert spatial_avg.rho.shape == shape
69+
assert spatial_avg.phi.shape == shape
70+
71+
72+
def test_bins_positive(spatial_avg):
73+
"""Test that bin centers are positive and sorted."""
74+
assert np.all(spatial_avg.r_centers > 0)
75+
assert np.all(np.diff(spatial_avg.r_centers) > 0) # monotonically increasing
76+
assert np.all(spatial_avg.rho_centers >= 0)
77+
assert np.all(np.diff(spatial_avg.rho_centers) > 0)
78+
79+
80+
# ---------------------------------------------------------------------------
81+
# Radial average tests
82+
# ---------------------------------------------------------------------------
83+
84+
85+
def test_radial_average_constant_field(spatial_avg, allclose):
86+
"""Radial average of a constant field should be constant."""
87+
field = np.ones_like(spatial_avg.X) * 5.0
88+
r_centers, field_avg = spatial_avg.compute_radial_average(field)
89+
90+
# All bins should have approximately the same value
91+
assert allclose(field_avg, 5.0, rtol=1e-10)
92+
93+
94+
def test_radial_average_radial_field(spatial_avg, allclose):
95+
"""Test radial average of f(r) = r."""
96+
field = spatial_avg.r.copy()
97+
r_centers, field_avg = spatial_avg.compute_radial_average(field)
98+
99+
# Average of r in each radial shell should equal the shell radius
100+
assert allclose(field_avg, r_centers, rtol=0.1)
101+
102+
103+
def test_radial_average_quadratic_field(spatial_avg, allclose):
104+
"""Test radial average of f(r) = r^2."""
105+
field = spatial_avg.r**2
106+
r_centers, field_avg = spatial_avg.compute_radial_average(field)
107+
108+
# Average of r^2 in each shell should equal r_center^2
109+
expected = r_centers**2
110+
assert allclose(field_avg, expected, rtol=0.15)
111+
112+
113+
def test_radial_average_vector_field(spatial_avg):
114+
"""Test radial average of a 3D vector field."""
115+
shape = spatial_avg.X.shape
116+
vx = np.ones(shape)
117+
vy = np.ones(shape) * 2
118+
vz = np.ones(shape) * 3
119+
vector_field = np.array([vx, vy, vz])
120+
121+
r_centers, v_avg = spatial_avg.compute_radial_average(vector_field)
122+
123+
assert v_avg.shape == (3, spatial_avg.nr)
124+
assert np.allclose(v_avg[0], 1.0, rtol=1e-10)
125+
assert np.allclose(v_avg[1], 2.0, rtol=1e-10)
126+
assert np.allclose(v_avg[2], 3.0, rtol=1e-10)
127+
128+
129+
def test_radial_average_with_std(spatial_avg):
130+
"""Test that standard deviation is returned when requested."""
131+
field = np.random.randn(*spatial_avg.X.shape)
132+
r_centers, field_avg, field_std = spatial_avg.compute_radial_average(
133+
field, return_std=True
134+
)
135+
136+
assert field_avg.shape == (spatial_avg.nr,)
137+
assert field_std.shape == (spatial_avg.nr,)
138+
assert np.all(field_std >= 0) # Std must be non-negative
139+
140+
141+
def test_radial_average_zero_field(spatial_avg, allclose):
142+
"""Radial average of zero field should be zero."""
143+
field = np.zeros_like(spatial_avg.X)
144+
r_centers, field_avg = spatial_avg.compute_radial_average(field)
145+
146+
assert allclose(field_avg, 0.0, atol=1e-15)
147+
148+
149+
# ---------------------------------------------------------------------------
150+
# Azimuthal average tests
151+
# ---------------------------------------------------------------------------
152+
153+
154+
def test_azimuthal_average_constant_field(spatial_avg, allclose):
155+
"""Azimuthal average of a constant field should be constant."""
156+
field = np.ones_like(spatial_avg.X) * 7.0
157+
rho_centers, z_centers, field_avg = spatial_avg.compute_azimuthal_average(field)
158+
159+
assert allclose(field_avg, 7.0, rtol=1e-10)
160+
161+
162+
def test_azimuthal_average_z_dependent(spatial_avg, allclose):
163+
"""Test azimuthal average of f(z) = z."""
164+
field = spatial_avg.Z.copy()
165+
rho_centers, z_centers, field_avg = spatial_avg.compute_azimuthal_average(field)
166+
167+
# For each z bin, average should equal z_center
168+
# Average over rho (each row should be same)
169+
avg_over_rho = np.mean(field_avg, axis=0)
170+
assert allclose(avg_over_rho, z_centers, rtol=0.15)
171+
172+
173+
def test_azimuthal_average_rho_dependent(spatial_avg, allclose):
174+
"""Test azimuthal average of f(rho) = rho."""
175+
field = spatial_avg.rho.copy()
176+
rho_centers, z_centers, field_avg = spatial_avg.compute_azimuthal_average(field)
177+
178+
# For each rho bin, average should equal rho_center
179+
# Average over z (each column should be same)
180+
avg_over_z = np.mean(field_avg, axis=1)
181+
assert allclose(avg_over_z, rho_centers, rtol=0.15)
182+
183+
184+
def test_azimuthal_average_vector_field(spatial_avg):
185+
"""Test azimuthal average of a 3D vector field."""
186+
shape = spatial_avg.X.shape
187+
vx = np.ones(shape) * 1.5
188+
vy = np.ones(shape) * 2.5
189+
vz = np.ones(shape) * 3.5
190+
vector_field = np.array([vx, vy, vz])
191+
192+
rho_centers, z_centers, v_avg = spatial_avg.compute_azimuthal_average(vector_field)
193+
194+
assert v_avg.shape == (3, spatial_avg.nrh, spatial_avg.nz)
195+
assert np.allclose(v_avg[0], 1.5, rtol=1e-10)
196+
assert np.allclose(v_avg[1], 2.5, rtol=1e-10)
197+
assert np.allclose(v_avg[2], 3.5, rtol=1e-10)
198+
199+
200+
def test_azimuthal_average_with_std(spatial_avg):
201+
"""Test that standard deviation is returned when requested."""
202+
field = np.random.randn(*spatial_avg.X.shape)
203+
rho_centers, z_centers, field_avg, field_std = spatial_avg.compute_azimuthal_average(
204+
field, return_std=True
205+
)
206+
207+
assert field_avg.shape == (spatial_avg.nrh, spatial_avg.nz)
208+
assert field_std.shape == (spatial_avg.nrh, spatial_avg.nz)
209+
assert np.all(field_std >= 0)
210+
211+
212+
def test_azimuthal_average_zero_field(spatial_avg, allclose):
213+
"""Azimuthal average of zero field should be zero."""
214+
field = np.zeros_like(spatial_avg.X)
215+
rho_centers, z_centers, field_avg = spatial_avg.compute_azimuthal_average(field)
216+
217+
assert allclose(field_avg, 0.0, atol=1e-15)
218+
219+
220+
# ---------------------------------------------------------------------------
221+
# Physics tests: sin(phi) weighting
222+
# ---------------------------------------------------------------------------
223+
224+
225+
def test_radial_average_sin_phi_weighting(spatial_avg):
226+
"""Test that sin(phi) weighting is correctly applied."""
227+
# Field = 1 everywhere
228+
field = np.ones_like(spatial_avg.X)
229+
230+
# Compute radial average (uses sin(phi) weights)
231+
r_centers, field_avg = spatial_avg.compute_radial_average(field)
232+
233+
# For a constant field, average should be 1 regardless of weighting
234+
assert np.allclose(field_avg, 1.0, rtol=1e-10)
235+
236+
# Now test with field = phi (should give average phi in each shell)
237+
field_phi = spatial_avg.phi.copy()
238+
r_centers, phi_avg = spatial_avg.compute_radial_average(field_phi)
239+
240+
# Weighted average of phi over sphere should be close to pi/2
241+
# (integral of phi*sin(phi) from 0 to pi gives pi/2)
242+
overall_avg_phi = np.average(phi_avg, weights=r_centers**2) # weight by shell volume
243+
assert np.allclose(overall_avg_phi, np.pi/2, rtol=0.1)
244+
245+
246+
# ---------------------------------------------------------------------------
247+
# Volume weights tests
248+
# ---------------------------------------------------------------------------
249+
250+
251+
def test_compute_volume_weights_shape(spatial_avg, mock_oper):
252+
"""Test that volume weights have correct shape."""
253+
weights = spatial_avg.compute_volume_weights()
254+
assert weights.shape == mock_oper.X.shape
255+
256+
257+
def test_compute_volume_weights_uniform(spatial_avg, mock_oper, allclose):
258+
"""Test that volume weights are uniform for uniform grid."""
259+
weights = spatial_avg.compute_volume_weights()
260+
expected_weight = mock_oper.delta**3
261+
assert allclose(weights, expected_weight, rtol=1e-12)
262+
263+
264+
def test_compute_volume_weights_sum(spatial_avg, mock_oper, allclose):
265+
"""Test that sum of volume weights equals total domain volume."""
266+
weights = spatial_avg.compute_volume_weights()
267+
total_volume = np.sum(weights)
268+
expected_volume = mock_oper.Lx * mock_oper.Ly * mock_oper.Lz
269+
assert allclose(total_volume, expected_volume, rtol=1e-10)
270+
271+
272+
# ---------------------------------------------------------------------------
273+
# Edge cases
274+
# ---------------------------------------------------------------------------
275+
276+
277+
def test_radial_average_single_bin(mock_oper):
278+
"""Test with only one radial bin."""
279+
spatial_avg = SpatialAverage(mock_oper, nr=1, nrh=8, nz=8)
280+
field = np.ones_like(spatial_avg.X) * 3.14
281+
r_centers, field_avg = spatial_avg.compute_radial_average(field)
282+
283+
assert len(field_avg) == 1
284+
assert np.allclose(field_avg[0], 3.14, rtol=1e-10)
285+
286+
287+
def test_azimuthal_average_single_bin(mock_oper):
288+
"""Test with only one azimuthal bin."""
289+
spatial_avg = SpatialAverage(mock_oper, nr=10, nrh=1, nz=1)
290+
field = np.ones_like(spatial_avg.X) * 2.71
291+
rho_centers, z_centers, field_avg = spatial_avg.compute_azimuthal_average(field)
292+
293+
assert field_avg.shape == (1, 1)
294+
assert np.allclose(field_avg[0, 0], 2.71, rtol=1e-10)
295+
296+
297+
def test_radial_average_large_nr(mock_oper):
298+
"""Test with more bins than grid points (some bins will be empty)."""
299+
spatial_avg = SpatialAverage(mock_oper, nr=100, nrh=8, nz=8)
300+
field = np.ones_like(spatial_avg.X)
301+
r_centers, field_avg = spatial_avg.compute_radial_average(field)
302+
303+
# Non-empty bins should have value close to 1
304+
# Empty bins should be 0 (as set by the code)
305+
assert np.all((field_avg == 0) | (np.abs(field_avg - 1.0) < 0.1))
306+
307+
308+
# ---------------------------------------------------------------------------
309+
# Consistency tests
310+
# ---------------------------------------------------------------------------
311+
312+
313+
def test_radial_azimuthal_consistency(spatial_avg, allclose):
314+
"""Test that radial and azimuthal averages are consistent for spherically symmetric fields."""
315+
# Spherically symmetric field: f(r) = r
316+
field = spatial_avg.r.copy()
317+
318+
# Radial average
319+
r_centers, field_avg_radial = spatial_avg.compute_radial_average(field)
320+
321+
# Azimuthal average
322+
rho_centers, z_centers, field_avg_azim = spatial_avg.compute_azimuthal_average(field)
323+
324+
# For spherically symmetric field, azimuthal average should depend only on r = sqrt(rho^2 + z^2)
325+
# Compute r for each (rho, z) bin
326+
RHO, Z = np.meshgrid(rho_centers, z_centers, indexing='ij')
327+
r_azim = np.sqrt(RHO**2 + Z**2)
328+
329+
# The field value should be close to r at each (rho, z)
330+
# (with some discretization error)
331+
assert allclose(field_avg_azim, r_azim, rtol=0.2)
332+
333+
334+
def test_linearity_radial_average(spatial_avg, allclose):
335+
"""Test linearity: avg(a*f1 + b*f2) = a*avg(f1) + b*avg(f2)."""
336+
field1 = np.random.randn(*spatial_avg.X.shape)
337+
field2 = np.random.randn(*spatial_avg.X.shape)
338+
a, b = 2.5, -1.3
339+
340+
r_centers, avg1 = spatial_avg.compute_radial_average(field1)
341+
_, avg2 = spatial_avg.compute_radial_average(field2)
342+
_, avg_combined = spatial_avg.compute_radial_average(a * field1 + b * field2)
343+
344+
expected = a * avg1 + b * avg2
345+
assert allclose(avg_combined, expected, rtol=1e-10)
346+
347+
348+
def test_linearity_azimuthal_average(spatial_avg, allclose):
349+
"""Test linearity for azimuthal average."""
350+
field1 = np.random.randn(*spatial_avg.X.shape)
351+
field2 = np.random.randn(*spatial_avg.X.shape)
352+
a, b = 3.2, 0.7
353+
354+
rho_centers, z_centers, avg1 = spatial_avg.compute_azimuthal_average(field1)
355+
_, _, avg2 = spatial_avg.compute_azimuthal_average(field2)
356+
_, _, avg_combined = spatial_avg.compute_azimuthal_average(a * field1 + b * field2)
357+
358+
expected = a * avg1 + b * avg2
359+
assert allclose(avg_combined, expected, rtol=1e-10)

0 commit comments

Comments
 (0)