Open dgary50 opened 9 months ago
Here is the code to create the ionospheric gravity wave model. The density Ne is returned. Here is my presentation, with more details of the model: Ionospheric_Refraction.pptx
dtor = np.pi/180.
def refraction_model(t=0, T=40, gamma=1):
''' Based on Koval et al. (2018), Journal of Geophysical Research: Space Physics,
123, 8940–8950. https://doi.org/10.1029/2018JA025584.
Creates a 1001 x 1001 pixel density model of the disturbed
ionosphere, with vertical resolution 0.5 km, horizontal resolution 1 km.
Inputs:
t The time (seconds) at which to calculate the model.
Calling the routine with changing time t allows the waves
in the model to propagate.
T The period of the gravity wave disturbance (minutes)
gamma A knob to adjust the magnitude of the disturbance.
Many other parameters are hard-coded in the first section below, but could be
converted to setable parameters on calling the routine, if desired.
'''
# Chosen parameter values for the problem
lambda_x = 300 # [km] horizontal wavelength of gravity wave
A = 135*dtor # Azimuth of gravity wave travel
dip = 61.5*dtor # Dip angle of local magnetic field
u_parallel = 7e-3 # [km/s] component of neutral wind velocity along geomagnetic field
U = 0 # Background wind velocity
N_max = 1.24e12 # [m^-3] Maximum density of F-layer
z_max = 300 # [km] Height of peak F-layer density
H = 50 # [km] Scale height for F-layer
T_BV = 12 # Brunt-Vaisala period of buoyancy oscillation
B_dec = 0 ovro-eovsa/ovro-lwa-solar#12*dtor # Magnetic declination (direction to magnetic pole)
# Calculated quantities
omega = 2*np.pi/(T*60.) # Angular frequency of gravity wave (from period T)
kx = 2*np.pi/lambda_x # horizontal component of gravity wave number (from lambda_x)
c = omega/kx # Phase velocity of gravity wave (should be omega/k?)
omega_B = 2*np.pi/(T_BV*60) # Brunt-Vaisala frequency (from T_BV)
z = np.linspace(0, 500, 1001) # Height range of ionosphere (resolution is 1/2 km)
x = np.linspace(0, 1000, 1001) # Horizontal range (resolution is 1 km)
kz2 = (omega_B/(U-c))**2 - kx**2 - 1/(4*H**2)
kz = np.sqrt(kz2) # Vertical wavenumber of gravity wave
# k_parallel is the component of the wave number of the gravity wave along B. It has a vertical
# component k_parallelv, and a horizontal component k_parallelh.
slope = np.arctan(kz/kx)
print('Altitude of trough is:', 90 - slope/dtor, 'degrees')
k_parallelv = np.sqrt(kx**2 + kz2) * np.cos(slope - dip)*np.cos((np.pi - A) + B_dec)
k_parallelh = np.sqrt(kx**2 + kz2) * np.cos(slope - dip)*np.cos((np.pi - A) - B_dec)
# These are all a function of z (height)
Ne0 = N_max*np.exp(0.5*(1-(z-z_max)/H - np.exp(-(z-z_max)/H))) # Chapman density function
dNe0dz = Ne0/(2*H) * (np.exp(-(z-z_max)/H) - 1) # Derivative of Ne0
Nep = 1j*u_parallel/omega*(dNe0dz*np.sin(dip) - 1j*k_parallelv*Ne0) # Density perturbation
# Calculate 2D density
Ne = np.zeros((1001,1001), np.float64)
for i,xi in enumerate(x):
Ne[:,i] = Ne0 + gamma*np.abs(Nep)*np.cos(omega*t - kx*xi - kz*z)
return Ne
I have modified the rd_mfits function a bit to make it more general such that it can read the compressed fitsfiles produced by ndfits as well and am attaching it here.
def rd_mfits(fname,reverse=True):
''' Given a multifrequency file name, return the frequencies, images, and header.
If reverse is True (default), the order of images and frequencies are reversed
to range from high to low (since it is more natural to start at high frequencies
for determining shifts.
'''
hdu=fits.open(fname)
try:
num_hdu=len(hdu)
if num_hdu==2:
imgs = hdu[0].data
h = hdu[0].header
f_recs = hdu[1].data
elif num_hdu==3 and hdu[1].name=='COMPRESSED_IMAGE':
imgs = hdu[1].data
h = hdu[1].header
f_recs = hdu[2].data
finally:
hdu.close()
freqs = [f[0] / 1e6 for f in f_recs]
freqs = np.array(freqs)
imgs = imgs.squeeze()
if reverse:
#print('Frequency order will be high to low')
freqs = freqs[::-1]
imgs = imgs[::-1]
return freqs, imgs, h
For our record, @surajit4444mondal @peijin94 could you both post your updates on the methods you use here?
(from the dev notes)
refraction correction
The thresh needs to be low enough to include all the corona weak emission, and be less influenced by the on disk emission.
So we are using fixed value of 0.1 T_b(quiet Sun), empirical expression from Zhang et al 2022
def thresh_func(freq): # freq in Hz
return 1.1e6 * (1-1.8e4* freq**(-0.6))
thresh = thresh_func*0.1
Using morphological operation to achieve a relatively robust way to get the coronal region while reducing the influence of artifacts to the maximum extend.
The resulting correction is OK-ish
But in the correction displacement, there is a feature.
In the freq-displacement plot, we see the overall frequency-dependent trend looks like refraction, yes
But the three jump near 43MHz 61MHz and 80MHz, it is obviously not refraction.
Before
After
The source position jump is visible by eye:
podman pull https://hub.docker.com/r/astronrd/linc
More test:
Quiet Sun with an active region
when it is very faint:
When it is too bright…..
offset with time (x and y):
blue: x[46MHz]-x[76Mhz] , orange: y[46MHz]-y[76Mhz]
without and with fitting:
fitting to x = x0+ a (1/f^2) and y = y0+ b(1/f^2)
x0 and y0:
Indeed there is frequency independent shift… (x0 y0)
Refraction correction implemented in PRs #50, #54, #65, #67, #69, #72. Also implemented in the realtime pipeline with interpolation in ovro-lwa-solar-ops PRs https://github.com/ovro-eovsa/ovro-lwa-solar-ops/pull/16 and https://github.com/ovro-eovsa/ovro-lwa-solar-ops/pull/17.
New feature request: divide the refraction into a constant and a variable component and at least correct the constant one determined from the whole day.
Depending on the number of good shift measurements and their temporal behavior, fitting linear or quadratic terms may be appropriate, too.
Depending on the number of good shift measurements and there temporal behavior, fitting linear or quadratic terms may be appropriate, too.
This has already been implemented as a keyword (using scipy's interp1d function). See https://github.com/ovro-eovsa/ovro-lwa-solar/blob/main/ovrolwasolar/refraction_correction.py#L277. linear interpolation is used as the default. However, how to decide on the exact interpolation method remains an open question (which has not been thought out yet).
Here is the code (updated 2023-12-11) to correct an fch file and write out a new fits file result. This version corrects a critical error from before, and also creates and writes out new mfs files. Note that the refraction 1/f^2 fit is actually done using the mfs files, so both are needed. To use it, create a new folder to store the results, cd to that folder, and then use the command: import mfits2mfs as m refraction = m.mfits2mfs(filelist) where filelist is the full list of files (expected to be all of those for a given date, although any list of files will do). The list should be of the fch files, but the mfs files of the same name are also needed. If you give it a single filename, it still needs to be a list, i.e. [filename]. The refraction dictionary returned has keys 'time', 'refraction', 'offset'. The later two are n x 2 arrays that are basically the fit parameters for x and y returned by the linear fit to 1/f^2, where f is in MHz. Therefore, the 'refraction' values seem high, but to get them for, say, 50 MHz, just divide by 50^2. Then they will be the refraction in arcmin for 50 MHz.
It is helpful to visualize the result for a given file by m.check_as_movie(filename). This is very slow from NJ, so a quicker check is with m.check_as_contour(filename). With luck, when called with the name of one of the corrected files, the image will be stationary with frequency (although not necessarily centered).