Skip to content

Commit 506064d

Browse files
CopilotSierd
andauthored
Fix ustarn calculation: initialization and FFT shear formula bugs (#265)
* Initial plan * Fix ustars0 and ustarn0 initialization bug in wind.py Fixed bug where ustars0 and ustarn0 were incorrectly set to ustar magnitude instead of their respective directional components ustars and ustarn. Co-authored-by: Sierd <[email protected]> * input files for debugging * Fix missing division in dtauy FFT shear calculation The dtauy_t formula in the FFT shear method was missing the division by sc_kv(0., 2.*sqrt(2.)*sigma), causing incorrect y-direction shear stress perturbations. This resulted in non-zero ustarn values even when the bed had no y-direction variability. The formula now matches the structure of dtaux_t calculation. Co-authored-by: Sierd <[email protected]> * Fix frequency array alignment in FFT shear calculation The kx and ky frequency arrays were misaligned with the FFT output. The code was creating frequency arrays with fftfreq(n+1)[1:] which skips the DC component, but the FFT includes all frequencies. This caused incorrect mapping between frequencies and FFT coefficients, leading to wrong shear perturbations. Fixed by using fftfreq(n) which properly matches the FFT output dimensions and includes the DC component at the correct index. Co-authored-by: Sierd <[email protected]> * Handle division by zero at DC component in FFT shear The previous fix included the DC component (kx=0, ky=0) in the frequency arrays, which caused division by zero errors in the shear calculations. Fixed by: - Using safe division with np.where to replace zeros with 1.0 temporarily - Explicitly setting DC component of perturbations to 0 after calculation - Applying same fix to filter_highfrequencies function The DC component represents the mean value and doesn't contribute to perturbations, so setting it to zero is physically correct. Co-authored-by: Sierd <[email protected]> * Use masked computation to avoid invalid values in FFT shear The previous fix still allowed invalid values when kx=0 (but ky≠0) because sigma depends on kx. When kx=0, sigma=0 which causes issues in Bessel function ratios. New approach: - Create a mask for valid computations: (k > 0) & (abs(kx) > 0) - Initialize perturbation arrays with zeros - Only compute perturbations for valid frequency combinations - All invalid combinations (kx=0 or k=0) automatically get zero perturbation This is physically correct: perturbations at kx=0 (pure y-direction frequencies) should be zero for the x-direction flow formulation. Co-authored-by: Sierd <[email protected]> * Simplify FFT shear edge case handling Simplified the masked computation approach to a cleaner implementation: - Use np.where for safe division (replace zeros with 1.0 temporarily) - Compute formulas normally with safe arrays - Apply invalid_mask at the end to zero out problematic regions This achieves the same result with much simpler, more readable code. Co-authored-by: Sierd <[email protected]> * Update shear.py reverted the suggested division in dtauy_t calculation * deleted wind tests * shear patch --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]> Co-authored-by: Sierd <[email protected]> Co-authored-by: Sierd <[email protected]>
1 parent 40c68b9 commit 506064d

File tree

2 files changed

+53
-17
lines changed

2 files changed

+53
-17
lines changed

aeolis/shear.py

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -544,8 +544,8 @@ def compute_shear(self, u0, nfilter=(1., 2.)):
544544
return
545545

546546
ny, nx = gc['z'].shape
547-
kx, ky = np.meshgrid(2. * np.pi * np.fft.fftfreq(nx+1, gc['dx'])[1:],
548-
2. * np.pi * np.fft.fftfreq(ny+1, gc['dy'])[1:])
547+
kx, ky = np.meshgrid(2. * np.pi * np.fft.fftfreq(nx, gc['dx']),
548+
2. * np.pi * np.fft.fftfreq(ny, gc['dy']))
549549

550550
hs = np.fft.fft2(gc['z'])
551551
hs = self.filter_highfrequenies(kx, ky, hs, nfilter)
@@ -576,25 +576,58 @@ def compute_shear(self, u0, nfilter=(1., 2.)):
576576

577577
# Arrays in Fourier
578578
k = np.sqrt(kx**2 + ky**2)
579-
sigma = np.sqrt(1j * L * kx * z0new /l)
580579

581580

582581
time_start_perturbation = time.time()
583-
582+
583+
584584
# Shear stress perturbation
585-
586-
dtaux_t = hs * kx**2 / k * 2 / ul**2 * \
587-
(-1. + (2. * np.log(l/z0new) + k**2/kx**2) * sigma * \
588-
sc_kv(1., 2. * sigma) / sc_kv(0., 2. * sigma))
585+
# Use masked computation to avoid division by zero and invalid special-function calls.
586+
# Build boolean mask for valid Fourier modes where formula is defined.
587+
valid = (k != 0) & (kx != 0)
588+
589+
# Pre-allocate zero arrays for Fourier-domain shear perturbations
590+
dtaux_t = np.zeros_like(hs, dtype=complex)
591+
dtauy_t = np.zeros_like(hs, dtype=complex)
592+
593+
if np.any(valid):
594+
# Extract valid-mode arrays
595+
k_v = k[valid]
596+
kx_v = kx[valid]
597+
ky_v = ky[valid]
598+
hs_v = hs[valid]
599+
600+
# z0new can be scalar or array; index accordingly
601+
if np.size(z0new) == 1:
602+
z0_v = z0new
603+
else:
604+
z0_v = z0new[valid]
589605

590-
591-
dtauy_t = hs * kx * ky / k * 2 / ul**2 * \
592-
2. * np.sqrt(2.) * sigma * sc_kv(1., 2. * np.sqrt(2.) * sigma)
606+
# compute sigma on valid modes
607+
sigma_v = np.sqrt(1j * L * kx_v * z0_v / l)
593608

594-
609+
# Evaluate Bessel K functions on valid arguments only
610+
kv0 = sc_kv(0., 2. * sigma_v)
611+
kv1 = sc_kv(1., 2. * sigma_v)
612+
613+
# main x-direction perturbation (vectorized on valid indices)
614+
term_x = -1. + (2. * np.log(l / z0_v) + (k_v**2) / (kx_v**2)) * sigma_v * (kv1 / kv0)
615+
dtaux_v = hs_v * (kx_v**2) / k_v * 2. / ul**2 * term_x
616+
617+
# y-direction perturbation (also vectorized)
618+
kv1_y = sc_kv(1., 2. * np.sqrt(2.) * sigma_v)
619+
dtauy_v = hs_v * (kx_v * ky_v) / k_v * 2. / ul**2 * 2. * np.sqrt(2.) * sigma_v * (kv1_y)
620+
621+
# store back into full arrays (other entries remain zero)
622+
dtaux_t[valid] = dtaux_v
623+
dtauy_t[valid] = dtauy_v
624+
625+
# invalid modes remain 0 (physically reasonable for k=0 or kx=0)
595626
gc['dtaux'] = np.real(np.fft.ifft2(dtaux_t))
596627
gc['dtauy'] = np.real(np.fft.ifft2(dtauy_t))
597628

629+
630+
598631

599632
def separation_shear(self, hsep):
600633
'''Reduces the computed wind shear perturbation below the
@@ -668,8 +701,11 @@ def filter_highfrequenies(self, kx, ky, hs, nfilter=(1, 2)):
668701
if nfilter is not None:
669702
n1 = np.min(nfilter)
670703
n2 = np.max(nfilter)
671-
px = 2 * np.pi / self.cgrid['dx'] / np.abs(kx)
672-
py = 2 * np.pi / self.cgrid['dy'] / np.abs(ky)
704+
# Avoid division by zero at DC component (kx=0, ky=0)
705+
kx_safe = np.where(kx == 0, 1.0, kx)
706+
ky_safe = np.where(ky == 0, 1.0, ky)
707+
px = 2 * np.pi / self.cgrid['dx'] / np.abs(kx_safe)
708+
py = 2 * np.pi / self.cgrid['dy'] / np.abs(ky_safe)
673709
s1 = n1 / np.log(1. / .01 - 1.)
674710
s2 = -n2 / np.log(1. / .99 - 1.)
675711
f1 = 1. / (1. + np.exp(-(px + n1 - n2) / s1))
@@ -882,4 +918,4 @@ def interpolate(self, x, y, z, xi, yi, z0):
882918

883919

884920

885-
921+

aeolis/wind.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,8 @@ def interpolate(s, p, t):
148148
s = velocity_stress(s,p)
149149

150150
s['ustar0'] = s['ustar'].copy()
151-
s['ustars0'] = s['ustar'].copy()
152-
s['ustarn0'] = s['ustar'].copy()
151+
s['ustars0'] = s['ustars'].copy()
152+
s['ustarn0'] = s['ustarn'].copy()
153153

154154
s['tau0'] = s['tau'].copy()
155155
s['taus0'] = s['taus'].copy()

0 commit comments

Comments
 (0)