Skip to content

Commit e076c78

Browse files
committed
smoothn test
1 parent b4caf07 commit e076c78

File tree

2 files changed

+228
-39
lines changed

2 files changed

+228
-39
lines changed

openpiv/smoothn.py

Lines changed: 60 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -445,35 +445,37 @@ def RobustWeights(r, I, h, wstr):
445445

446446
## Initial Guess with weighted/missing data
447447
# function z = InitialGuess(y,I)
448-
def InitialGuess(y, I):
449-
# -- nearest neighbor interpolation (in case of missing values)
450-
if np.any(~I):
451-
try:
452-
from scipy.ndimage.morphology import distance_transform_edt
448+
def InitialGuess(y, z0):
449+
"""
450+
Compute initial guess for the smoothed array.
453451
454-
# if license('test','image_toolbox')
455-
# [z,L] = bwdist(I);
456-
L = distance_transform_edt(1 - I)
457-
z = y
458-
z[~I] = y[L[~I]]
459-
except:
460-
# If BWDIST does not exist, NaN values are all replaced with the
461-
# same scalar. The initial guess is not optimal and a warning
462-
# message thus appears.
463-
z = y
464-
z[~I] = mean(y[I])
452+
Parameters
453+
----------
454+
y : ndarray
455+
The input array to be smoothed.
456+
z0 : ndarray or None
457+
Initial guess for the smoothed array. If None, y is used.
458+
459+
Returns
460+
-------
461+
z : ndarray
462+
The initial guess for the smoothed array.
463+
"""
464+
# If z0 is provided and has the right size, use it
465+
if z0 is not None:
466+
if z0.shape == y.shape:
467+
return z0
468+
else:
469+
# Wrong size, ignore z0
470+
pass
471+
472+
# Otherwise, use y as the initial guess
473+
if isinstance(y, np.ma.MaskedArray):
474+
# For masked arrays, preserve the mask
475+
z = y.copy()
465476
else:
466-
z = y
467-
# coarse fast smoothing
468-
z = dctND(z, f=dct)
469-
k = array(z.shape)
470-
m = ceil(k / 10) + 1
471-
d = []
472-
for i in range(len(k)):
473-
d.append(arange(m[i], k[i]))
474-
d = np.array(d).astype(int)
475-
z[d] = 0.0
476-
z = dctND(z, f=idct)
477+
z = y.copy()
478+
477479
return z
478480
# -- coarse fast smoothing using one-tenth of the DCT coefficients
479481
# siz = z.shape;
@@ -506,24 +508,44 @@ def dctND(data, f=dct):
506508

507509
def peaks(n):
508510
"""
509-
Mimic basic of matlab peaks fn
510-
"""
511-
xp = arange(n)
512-
[x, y] = meshgrid(xp, xp)
511+
Mimic basic of matlab peaks fn
512+
513+
Parameters
514+
----------
515+
n : int or array_like
516+
If int, size of the output array. If array, find peaks in this array.
517+
518+
Returns
519+
-------
520+
z : ndarray or list
521+
If n is int, returns a 2D array with peaks.
522+
If n is array, returns indices of peaks in the array.
523+
"""
524+
# If n is an array, find peaks in it
525+
if isinstance(n, np.ndarray):
526+
# Find local maxima
527+
indices = []
528+
for i in range(1, len(n)-1):
529+
if n[i] > n[i-1] and n[i] > n[i+1]:
530+
indices.append(i)
531+
return indices
532+
533+
# Otherwise, generate a 2D peaks function
534+
xp = np.arange(n)
535+
x, y = np.meshgrid(xp, xp)
513536
z = np.zeros_like(x).astype(float)
514537
for i in range(n // 5):
515-
x0 = random() * n
516-
y0 = random() * n
517-
sdx = random() * n / 4.0
538+
x0 = np.random.random() * n
539+
y0 = np.random.random() * n
540+
sdx = np.random.random() * n / 4.0
518541
sdy = sdx
519-
c = random() * 2 - 1.0
520-
f = exp(
542+
c = np.random.random() * 2 - 1.0
543+
f = np.exp(
521544
-(((x - x0) / sdx) ** 2)
522545
- ((y - y0) / sdy) ** 2
523546
- (((x - x0) / sdx)) * ((y - y0) / sdy) * c
524547
)
525-
# f /= f.sum()
526-
f *= random()
548+
f *= np.random.random()
527549
z += f
528550
return z
529551

openpiv/test/test_smoothn.py

Lines changed: 168 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
gcv,
1111
RobustWeights,
1212
warning,
13-
dctND
13+
dctND,
14+
InitialGuess,
15+
peaks
1416
)
1517

1618
def test_smoothn_basic():
@@ -442,3 +444,168 @@ def test_warning_function():
442444
output = captured_output.getvalue()
443445
assert "Warning type" in output
444446
assert "Warning message" in output
447+
448+
def test_smoothn_with_negative_weights():
449+
"""Test smoothn with negative weights (should raise a warning)"""
450+
# Create a noisy 1D signal
451+
x = np.linspace(0, 10, 100)
452+
y_true = np.sin(x)
453+
noise = np.random.normal(0, 0.1, x.size)
454+
y_noisy = y_true + noise
455+
456+
# Create weights with some negative values
457+
W = np.ones_like(y_noisy)
458+
W[40:60] = -1.0 # Negative weights in the middle
459+
460+
# The function should raise a ValueError with negative weights
461+
try:
462+
y_smooth, _, _, Wtot = smoothn(y_noisy, W=W)
463+
# If we get here, the test should fail
464+
assert False, "smoothn should raise ValueError with negative weights"
465+
except ValueError as e:
466+
# Check that the error message is correct
467+
assert "Weights must all be >=0" in str(e)
468+
469+
# Now try with zero weights instead
470+
W[40:60] = 0.0
471+
y_smooth, _, _, Wtot = smoothn(y_noisy, W=W)
472+
473+
# Check that zero weights were preserved
474+
assert np.all(Wtot[40:60] == 0)
475+
476+
# Check that the smoothed signal is still reasonable
477+
assert np.mean((y_smooth - y_true)**2) < np.mean((y_noisy - y_true)**2)
478+
479+
def test_initial_guess_function():
480+
"""Test the InitialGuess function"""
481+
# Create a simple test case
482+
y = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
483+
484+
# Test with default z0=None
485+
z = InitialGuess(y, None)
486+
assert np.array_equal(z, y)
487+
488+
# Test with provided z0
489+
z0 = np.array([0.5, 1.5, 2.5, 3.5, 4.5])
490+
z = InitialGuess(y, z0)
491+
assert np.array_equal(z, z0)
492+
493+
# Test with z0 of wrong size
494+
z0_wrong_size = np.array([0.5, 1.5, 2.5])
495+
z = InitialGuess(y, z0_wrong_size)
496+
assert np.array_equal(z, y)
497+
498+
# Test with masked array
499+
mask = np.zeros_like(y, dtype=bool)
500+
mask[2] = True
501+
y_masked = ma.array(y, mask=mask)
502+
z = InitialGuess(y_masked, None)
503+
assert isinstance(z, ma.MaskedArray)
504+
assert np.array_equal(z.mask, mask)
505+
506+
def test_peaks_function():
507+
"""Test the peaks function"""
508+
# Create a signal with peaks
509+
x = np.linspace(0, 10, 100)
510+
y = np.sin(x) + 0.5 * np.sin(2*x)
511+
512+
# Find peaks
513+
idx = peaks(y)
514+
515+
# Check that peaks were found
516+
assert len(idx) > 0
517+
518+
# Check that the identified points are actually peaks
519+
for i in idx:
520+
if i > 0 and i < len(y) - 1:
521+
assert y[i] > y[i-1] and y[i] > y[i+1]
522+
523+
def test_smoothn_3d():
524+
"""Test smoothn with 3D data"""
525+
# Create a noisy 3D signal
526+
x, y, z = np.meshgrid(
527+
np.linspace(0, 1, 10),
528+
np.linspace(0, 1, 10),
529+
np.linspace(0, 1, 10)
530+
)
531+
data_true = np.sin(2*np.pi*x) * np.cos(2*np.pi*y) * np.sin(2*np.pi*z)
532+
noise = np.random.normal(0, 0.1, data_true.shape)
533+
data_noisy = data_true + noise
534+
535+
# Apply smoothn
536+
data_smooth, s, exitflag, _ = smoothn(data_noisy)
537+
538+
# Check that the smoothed signal is closer to the true signal than the noisy one
539+
assert np.mean((data_smooth - data_true)**2) < np.mean((data_noisy - data_true)**2)
540+
541+
# Check that s is positive (smoothing parameter)
542+
assert s > 0
543+
544+
# Check that exitflag is 1 (convergence)
545+
assert exitflag == 1
546+
547+
def test_smoothn_with_verbose():
548+
"""Test smoothn with verbose output"""
549+
# Create a noisy 1D signal
550+
x = np.linspace(0, 10, 100)
551+
y_true = np.sin(x)
552+
noise = np.random.normal(0, 0.1, x.size)
553+
y_noisy = y_true + noise
554+
555+
# Capture stdout to check for verbose output
556+
import io
557+
import sys
558+
captured_output = io.StringIO()
559+
sys.stdout = captured_output
560+
561+
# Apply smoothn with verbose=True
562+
y_smooth, _, _, _ = smoothn(y_noisy, verbose=True)
563+
564+
# Restore stdout
565+
sys.stdout = sys.__stdout__
566+
567+
# Check that verbose output was produced
568+
output = captured_output.getvalue()
569+
assert "tol" in output.lower() or "nit" in output.lower()
570+
571+
# Check that the smoothed signal is reasonable
572+
assert np.mean((y_smooth - y_true)**2) < np.mean((y_noisy - y_true)**2)
573+
574+
def test_smoothn_with_max_iter():
575+
"""Test smoothn with maximum iterations"""
576+
# Create a noisy 1D signal
577+
x = np.linspace(0, 10, 100)
578+
y_true = np.sin(x)
579+
noise = np.random.normal(0, 0.1, x.size)
580+
y_noisy = y_true + noise
581+
582+
# Apply smoothn with very low MaxIter
583+
y_smooth, _, exitflag, _ = smoothn(y_noisy, isrobust=True, MaxIter=1)
584+
585+
# Check that exitflag is 0 (max iterations reached)
586+
assert exitflag == 0
587+
588+
# Check that the smoothed signal is still reasonable
589+
assert np.mean((y_smooth - y_true)**2) < np.mean((y_noisy - y_true)**2)
590+
591+
def test_smoothn_with_tolerance():
592+
"""Test smoothn with different tolerance values"""
593+
# Create a noisy 1D signal
594+
x = np.linspace(0, 10, 100)
595+
y_true = np.sin(x)
596+
noise = np.random.normal(0, 0.1, x.size)
597+
y_noisy = y_true + noise
598+
599+
# Apply smoothn with high tolerance (should converge quickly)
600+
y_smooth_high_tol, _, exitflag1, _ = smoothn(y_noisy, isrobust=True, TolZ=0.5)
601+
602+
# Apply smoothn with low tolerance (should take more iterations)
603+
y_smooth_low_tol, _, exitflag2, _ = smoothn(y_noisy, isrobust=True, TolZ=1e-6)
604+
605+
# Both should converge
606+
assert exitflag1 == 1
607+
assert exitflag2 == 1
608+
609+
# Check that both smoothed signals are reasonable
610+
assert np.mean((y_smooth_high_tol - y_true)**2) < np.mean((y_noisy - y_true)**2)
611+
assert np.mean((y_smooth_low_tol - y_true)**2) < np.mean((y_noisy - y_true)**2)

0 commit comments

Comments
 (0)