-
Notifications
You must be signed in to change notification settings - Fork 626
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
automated DFT decimation for adjoint sources #1753
Conversation
Codecov Report
@@ Coverage Diff @@
## master #1753 +/- ##
==========================================
+ Coverage 74.43% 74.50% +0.06%
==========================================
Files 13 13
Lines 4589 4600 +11
==========================================
+ Hits 3416 3427 +11
Misses 1173 1173
|
When defining bandwidth, we typically look at the power spectral density (PSD), not the real and imaginary parts. |
Plotting the magnitude of the DTFT of the Nuttall window shows the side lobes more clearly as well as some of its other features including an (unexpected) discontinuity. In this specific example, it looks like the frequency response decays by roughly five orders of magnitude from its peak before reaching some kind of (slowly-decaying) floor. For comparison, the Nuttall spectrum in the scipy example decays by roughly 10 orders of magnitude from its peak value before reaching a constant floor. The spectrum also does not contain a discontinuity. plt.figure()
dtft = nuttall_dtft(frqs,fcen)
plt.semilogy(frqs,np.abs(dtft),'bo-')
plt.xlabel('frequency')
plt.ylabel('DTFT of Nuttall window (magnitude)')
plt.savefig('nuttall_dtft_abs.png',dpi=150,bbox_inches='tight') |
The scipy example is in dB (multiplied by 20, not 10) which, again, is PSD. That's why the rolloff differs by a factor of 2. |
You'll just want to add Basically, handle |
The scipy plot looks odd because the amplitude is not decaying outside of the center lobe — probably that is because they are using Try plotting it just from 0.99 to 1.01, but sample the points 10x more finely (and plot with The fact that scipy doesn't provide the exact DTFT means we have to implement that part on our own anyway, so we should implement our own time-domain version also to ensure that they are consistent. |
The Nutall window W(ω) (centered at ω=0) has a complicated formula, but we only care about its asymptotic behavior (for large |ω|), and that should approximately be a simple power law (proportional to #/ω^n for some exponent n and some coefficient #, both of which can be calculated analytically). This asymptotic power law should be accurate enough for computing the bandwidth. Mathematica (available online via WolframAlpha) should be able to do this asymptotic expansion for you. Since we expect it to be a polynomial in x = 1/ω, probably you can give Mathematica W(1/x) and tell it to Taylor expand around x=0. |
Don't we already know its asymptotic behavior? We specify the basis width of each window to be the smallest Δω. Why not just use that value to determine the bandwidth? Specifically, as long as the Nyquist rate is greater than ω_max+Δω/2, we should be good. |
You can also just get the asymptotics numerically to decent accuracy pretty easily:
Given this, now you just need to work out the rescaling of the asymptotic when you change the bandwidth to Δω — I'm guessing it's something like #(Δω/ω)^p, but you should double-check. |
You need to make sure that this formula correctly predicts the behavior when you change the bandwidth Δf = 1/(NΔt). In your tests above, you have N=7980 and dt=0.0125. So, if we want So hopefully |
720f541
to
6fc1b00
Compare
This seems to be finally working: the frequency bandwidth of the Nuttall window used in the adjoint sources is computed analytically by fitting to an asymptotic power law. Similar to the I verified that the fitting procedure for import numpy as np
import matplotlib
matplotlib.use('agg')
import matplotlib.pyplot as plt
res = 50
dt = 0.5/res
fcen = 0
df = 4.0
nfrqs = 250
frqs = np.linspace(fcen-0.5*df,fcen+0.5*df,nfrqs)
T = np.max(np.abs(1 / np.diff(frqs)))
N = np.rint(T / dt)
fwidth = 1/(N*dt)
def sinc(f, f0):
num = np.where(f == f0, N+1,
(1 - np.exp(1j * (N + 1) * (2 * np.pi) *
(f - f0) * dt)))
den = np.where(f == f0, 1,
(1 - np.exp(1j * (2 * np.pi) * (f - f0) * dt)))
return num / den
def cos_window_fd(a, f, f0):
df = 1 / (N * dt)
cos_sum = a[0] * sinc(f, f0)
for k in range(1, len(a)):
cos_sum += (-1)**k * a[k] / 2 * sinc(f, f0 - k * df) + (-1)**k * a[k] / 2 * sinc(f, f0 + k * df)
return cos_sum
def nuttall_dtft(f, f0):
a = [0.355768, 0.4873960, 0.144232, 0.012604]
return cos_window_fd(a, f, f0)
if __name__ == '__main__':
plt.figure()
plt.subplot(1,2,1)
dtft = nuttall_dtft(frqs,fcen)
coeff1 = ((df/2)**3)*np.abs(dtft[-1])
print("coeff1:, {}".format(coeff1))
plt.loglog(frqs,np.abs(dtft),'b-',label='|W(f)|')
plt.loglog(frqs,coeff1/np.power(frqs,3),'k--',label='{:.6e}/f$^3$'.format(coeff1))
plt.xlabel('frequency, f')
plt.ylabel('DTFT of Nuttall window, |W(f)|')
plt.title('fit to C/f$^p$')
plt.legend(loc='lower left')
coeff2 = coeff1 * 1/fwidth**3
print("coeff2:, {}".format(coeff2))
plt.subplot(1,2,2)
plt.loglog(frqs,np.abs(dtft),'b-',label='|W(f)|')
plt.loglog(frqs,coeff2*np.power(fwidth/frqs,3),'k--',label='{:.6e}(Δf/f)$^3$'.format(coeff2))
plt.xlabel('frequency, f')
plt.ylabel('DTFT of Nuttall window, |W(f)|')
plt.title('fit to C*(Δf/f)$^p$, Δf={:.4f}'.format(fwidth))
plt.legend(loc='lower left')
plt.savefig('nuttall_dtft.png',dpi=150,bbox_inches='tight') |
Note that coefficient C in |
You should also check that this gives the same answer (to many digits) as setting the decimation factor to 1. |
c1498ae
to
66d55e2
Compare
After switching the fit to the function I think therefore that this PR is ready to be merged. |
@@ -213,7 +208,7 @@ def test_adjoint_solver_DFT_fields(self): | |||
adj_scale = (dp[None,:]@adjsol_grad).flatten() | |||
fd_grad = S12_perturbed-S12_unperturbed | |||
print("Directional derivative -- adjoint solver: {}, FD: {}".format(adj_scale,fd_grad)) | |||
tol = 0.04 if mp.is_single_precision() else 0.005 | |||
tol = 0.0461 if mp.is_single_precision() else 0.005 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the one place where the tolerance needed to be slightly adjusted.
Results from running
automatic DFT decimation
no DFT decimation
automatic DFT decimation
no DFT decimation
edit: these results were generated with Meep compiled using single-precision floating point. |
* remove tol parameter from get_fwidth and add new set_fwidth function * add fwidth parameter to custom_src_time class * compute bandwidth of Nuttall DTFT window function using asymptotic power law * fixes and docs * add set_fwidth and get_fwidth functions to custom_py_src_time class * adjust tolerances of failing unit tests * remove fwidth from formula for fitting coefficient * revert changes to tolerances in unit tests * slightly increase tolerance for single-precision case of DFT fields test
Closes #1751.
This is an incomplete attempt to add automated DFT decimation for the adjoint sources. It follows the outline in #1751 by removing the
tol
parameter ofget_fwidth
and adding a new functionset_fwidth(double fw)
to thesrc_time
class and its two variantsgaussian_src_time
andcustom_src_time
.The piece that has yet to be added involves modifying the Python adjoint source
FilteredSource
(which is a wrapper aroundCustomSource
) to call its member functionset_fwidth
for each frequency in its listfrequencies
after theCustomSource
instance has been created:meep/python/adjoint/filter_source.py
Lines 57 to 61 in e2739e4
The problem though is that the bandwidth of the Nuttall window-function implementation of
python/adjoint/filter_source.py
does not seem to be well defined. To demonstrate this, here is a plot of the DTFT of an actual Nuttall window (real and imaginary parts) using arbitrary but realistic values:(Note: the imaginary part is several orders of magnitude larger than the real part.)
One possible definition for the bandwidth of this Nuttall window is the smallest interval in which its magnitude is non-zero but this may be too conservative.
For comparison, the Nuttall window from the scipy signal package has well-defined sidelobes which obviously makes the definition of the bandwidth unambiguous.