"""
pyrad.correct.sunlib
====================
Library to deal with sun measurements
.. autosummary::
:toctree: generated/
sun_position_pysolar
sun_position_mfr
equation_of_time
hour_angle
solar_declination
refraction_correction
gas_att_sun
gauss_fit
retrieval_result
sun_power
ptoa_to_sf
solar_flux_lookup
scanning_losses
"""
import datetime
from copy import deepcopy
from warnings import warn
from numpy import pi, sin, cos, arcsin, arccos, sqrt, floor
from numpy.linalg import LinAlgError
import numpy as np
from scipy.special import erf
try:
import pysolar
_PYSOLAR_AVAILABLE = True
except ImportError:
_PYSOLAR_AVAILABLE = False
[docs]def sun_position_pysolar(dt, lat, lon, elevation=0.):
"""
obtains the sun position in antenna coordinates using the pysolar
library.
Parameters
----------
dt : datetime object
the time when to look for the sun
lat, lon : float
latitude and longitude of the sensor in degrees
Returns
-------
el, az : float
elevation and azimuth angles of the sun respect to the sensor in
degrees
"""
# Make the date time zone aware
dt_aux = deepcopy(dt)
dt_aux = dt_aux.replace(tzinfo=datetime.timezone.utc)
az, el = pysolar.solar.get_position(lat, lon, dt_aux, elevation=elevation)
return (el, az)
[docs]def sun_position_mfr(dt, lat_deg, lon_deg, refraction=True):
"""
Calculate the sun position for the given time (dt) at the given position
(lat, lon).
Parameters
----------
dt : datetime object
the time when to look for the sun
lat_deg, lon_deg: floats
latitude and longitude in degrees
refraction : boolean
whether to correct for refraction or not
Returns
-------
elev_sun, azim_sun : floats
elevation and azimuth angles of the sun respect to the sensor in
degrees
"""
lat = lat_deg*pi/180.
lon = lon_deg*pi/180.
secs_since_midnight = (
(dt - dt.replace(
hour=0, minute=0, second=0, microsecond=0)).total_seconds())
htime = secs_since_midnight/3600.
dayjul = (
(dt - dt.replace(
month=1, day=1, hour=0, minute=0, second=0, microsecond=0)).days +
1)
eqt = equation_of_time(dayjul) # [h]
hang = hour_angle(htime, lon, eqt) # [rad]
sdec = solar_declination(dayjul, htime) # [rad]
elev_sun = (
arcsin(sin(lat)*sin(sdec)+cos(lat)*cos(sdec)*cos(hang))*180./pi)
azim_sun = (
arccos((sin(lat)*cos(sdec)*cos(hang)-cos(lat)*sin(sdec)) /
cos(elev_sun*pi/180.))*180./pi)
if hang < 0:
azim_sun = 180. - azim_sun # morning
else:
azim_sun = 180. + azim_sun # afternoon
if refraction:
elev_sun += refraction_correction(elev_sun)
return (elev_sun, azim_sun)
def equation_of_time(dayjul):
"""
Computes the solar hour for a given julian day.
Parameters
----------
dayjul : double
julian date
Returns
-------
eqt : float
hour
"""
temp_cos = [-0.00720, 0.0528, 0.0012]
temp_sin = [0.12290, 0.1565, 0.0041]
omega = 2.*pi/365.2425 # earth mean angular orbital velocity [rad/day]
eqt = 0.
for ii in range(3):
z = dayjul*omega*(ii+1)
eqt += (temp_cos[ii]*cos(z)+temp_sin[ii]*sin(z))
return -eqt # [h]
def hour_angle(htime, lon, eqt):
"""
Computes the solar angle at a particular time.
Parameters
----------
htime : double
time in seconds since midnight
lon : float
longitude in degrees
eqt : float
solar time
Returns
-------
angle : float
the solar angle in radiants
"""
return (htime+12./pi*lon+eqt-12.)*pi/12. # [rad]
def solar_declination(dayjul, htime):
"""
Computes the solar declination.
Parameters
----------
dayjul : double
julian date
htime : double
time in seconds since midnight
Returns
-------
angle : float
the solar declination in radiants
"""
omega = 2.*pi/365.2425 # earth mean angular orbital velocity [rad/day]
correction = [0., 3., 6., 9., 12., 13., 12., 10., 10., 8., 6., 3., 1.,
-2., -3., -4., -5., -5., -3., -3., -1., -1., 0., 0., 1., 3.,
6.]
z = dayjul * omega
x = 0.33281 - 22.984 * cos(z) - 0.3499 * cos(2.*z) - 0.1398 * cos(3. * z)
y = 3.7872 * sin(z) + 0.03205 * sin(2. * z) + 0.07187 * sin(3. * z)
fortnight = int(floor(dayjul/15)) + 1
day_fortnight = dayjul-(fortnight-1)*15
corr1 = (
(correction[fortnight]+day_fortnight/15. *
(correction[fortnight+1]-correction[fortnight]))/60.)
delta1 = x + y + corr1
z = (dayjul+1) * omega
x = 0.33281 - 22.984 * cos(z) - 0.3499 * cos(2.*z) - 0.1398 * cos(3. * z)
y = 3.7872 * sin(z) + 0.03205 * sin(2. * z) + 0.07187 * sin(3. * z)
fortnight = int(floor(dayjul+1)/15) + 1
day_fortnight = (dayjul+1)-(fortnight-1)*15
corr2 = (
(correction[fortnight]+day_fortnight/15. *
(correction[fortnight+1]-correction[fortnight]))/60.)
delta2 = x + y + corr2
return (delta1+(delta2-delta1)*htime/24.)*pi/180. # [rad]
def refraction_correction(es_deg):
"""
Computes the correction that has to be applied to the sun elevation angle
to account for refraction
Parameters
----------
es_deg : float
sun elevation in degrees
Returns
-------
refr : float
the correction due to refraction in degrees
References
----------
Holleman & Huuskonen, 2013: analytical formulas for refraction of
radiowaves from exoatmospheric sources, radio science, vol. 48, 226-231
"""
if es_deg < -0.77:
return 0.0
es_rad = es_deg*pi/180.
k = 5./4. # effective earth radius factor (typically 4/3)
n = 313. # surface refractivity
no = n*1e-6 + 1.
refr = (
((k-1.)/(2.*k-1.)*cos(es_rad) *
(sqrt((sin(es_rad))**2.+(4.*k-2.)/(k-1.)*(no-1.)) -
sin(es_rad)))*180./pi)
return refr
def gas_att_sun(es_deg, attg):
"""
Computes the attenuation suffered by the sun signal through the atmosphere
Parameters
----------
es_deg : float
sun elevation in degrees
attg : float
1-way gas attenuation in dB/km
Returns
-------
gas_att_sun : float
the sun attenuation in dB
"""
r43 = 4./3.*6371 # effective earth radius [km]
z0 = 8.4 # equivalent height of the atmosphere [km]
return attg*(r43*sqrt((sin(es_deg*pi/180.))**2.+2.*z0/r43+(z0/r43)**2) -
r43*sin(es_deg*pi/180.))
def gauss_fit(az_data, az_ref, el_data, el_ref, sunhits, npar, degree=True,
do_elcorr=True):
"""
estimates a gaussian fit of sun hits data
Parameters
----------
az_data, el_data : float array
azimuth and elevation radar data
az_ref, el_ref : float array
azimuth and elevation sun data
sunhits : float array
sun hits data
npar : int
number of parameters of the fit
degree : boolean
boolean indicating whether the data is in degree or radians
do_elcorr : boolean
indicates whether azimuth data is corrected so that azimuth
differences are invalid with elevation
Returns
-------
par : 1D float array
the fit parameters
alpha: 2D float array
the matrix used in the fit
beta: 1D float array
the vector used in the fit
"""
nhits = len(az_data)
el_corr = 1.
if do_elcorr:
if degree:
el_corr = np.ma.cos(el_data*np.pi/180.)
else:
el_corr = np.ma.cos(el_data)
basis = np.ma.zeros((npar, nhits))
basis[0, :] = 1.
basis[1, :] = (az_data-az_ref)*el_corr
basis[2, :] = el_data-el_ref
if npar == 5:
basis[3, :] = basis[1, :]*basis[1, :]
basis[4, :] = basis[2, :]*basis[2, :]
alpha = np.ma.zeros((npar, npar))
beta = np.ma.zeros(npar)
for hit in range(nhits):
for ipar in range(npar):
for jpar in range(npar):
alpha[jpar, ipar] += basis[jpar, hit] * basis[ipar, hit]
beta[ipar] += sunhits[hit] * basis[ipar, hit]
try:
alphainv = np.linalg.inv(alpha)
par = np.ma.dot(alphainv, beta)
return par, alpha, beta
except LinAlgError:
warn('Unable to perform Guassian fit of sun hits data')
return None, None, None
def retrieval_result(sunhits, alpha, beta, par, npar):
"""
computes the physical parameters of the sun retrieval from the results of
a Gaussian fit.
Parameters
----------
sunhits : float array
sun hits data
alpha: 2D float array
the matrix used in the fit
beta: 1D float array
the vector used in the fit
par : 1D float array
the fit parameters
npar : int
number of parameters of the fit
Returns
-------
val, val_std : float
retrieved value and its standard deviation
az_bias, el_bias : float
retrieved azimuth and elevation antenna bias respect to the sun
position
az_width, el_width : float
retrieved azimuth and elevation antenna widths
"""
nhits = len(sunhits)
val = (
par[0]-0.25*np.ma.power(par[1], 2.)/par[3] -
0.25*np.ma.power(par[2], 2.)/par[4])
az_bias = -0.5*par[1]/par[3]
el_bias = -0.5*par[2]/par[4]
coeff = -40.*np.ma.log10(2.)
az_width = np.ma.sqrt(coeff/par[3])
el_width = np.ma.sqrt(coeff/par[4])
val_std = np.ma.sum(np.ma.power(sunhits, 2.))-2.*np.ma.sum(par*beta)
for ipar in range(npar):
for jpar in range(npar):
val_std += par[ipar]*par[jpar]*alpha[ipar, jpar]
val_std = np.ma.sqrt(val_std/(nhits-npar))
return val, val_std, az_bias, el_bias, az_width, el_width
[docs]def sun_power(solar_flux, pulse_width, wavelen, antenna_gain, angle_step,
beamwidth, coeff_band=1.2):
"""
computes the theoretical sun power detected at the antenna [dBm] as it
would be without atmospheric attenuation (sun power at top of the
atmosphere) for a given solar flux and radar characteristics
Parameters
----------
solar_flux : float array
the solar fluxes measured at 10.7 cm wavelength [10e-22 W/(m2 Hz)]
pulse_width : float
pulse width [s]
wavelen : float
radar wavelength [m]
antenna_gain : float
the antenna gain [dB]
angle_step : float
integration angle [deg]
beamwidth : float
3 dB-beamwidth [deg]
coeff_band : float
multiplicative coefficient applied to the inverse of the pulse width
to get the effective bandwidth
Returns
-------
pwr_det : float array
the detected power
References
----------
Altube P., J. Bech, O. Argemi, T. Rigo, 2015: Quality Control of Antenna
Alignment and Receiver Calibration Using the Sun: Adaptation to Midrange
Weather Radar Observations at Low Elevation Angles
"""
g = np.power(10., 0.1*antenna_gain)
b = coeff_band*1./pulse_width # receiver bandwidth [Hz]
aeff = g*wavelen**2./(4.*np.pi) # effective area of the antenna [m2]
# solar flux at given wavelength
s0 = solar_flux_lookup(solar_flux, wavelen)
ptoa = 10.*np.log10(0.5*b*aeff*s0*1e-19) # sun power at TOA [dBm]
# losses due to antenna beam width and scanning
la = scanning_losses(angle_step, beamwidth)
return ptoa-la
[docs]def ptoa_to_sf(ptoa, pulse_width, wavelen, antenna_gain, coeff_band=1.2):
"""
Converts the sun power at the top of the atmosphere (in dBm) into solar
flux.
Parameters
----------
ptoa : float
sun power at the top of the amosphere. It already takes into account
the correction for antenna polarization
pulse_width : float
pulse width [s]
wavelen : float
radar wavelength [m]
antenna_gain : float
the antenna gain [dB]
coeff_band : float
multiplicative coefficient applied to the inverse of the pulse width
to get the effective bandwidth
Returns
-------
s0 : float
solar flux [10e-22 W/(m2 Hz)]
References
----------
Altube P., J. Bech, O. Argemi, T. Rigo, 2015: Quality Control of Antenna
Alignment and Receiver Calibration Using the Sun: Adaptation to Midrange
Weather Radar Observations at Low Elevation Angles
"""
g = np.power(10., 0.1*antenna_gain)
b = coeff_band*1./pulse_width # receiver bandwidth [Hz]
aeff = g*wavelen**2./(4.*np.pi) # effective area of the antenna [m2]
# solar flux in [10e-22 W/(m2 Hz)]
s0 = np.power(10., 0.1*ptoa)*1e19/(b*aeff)
return s0
[docs]def solar_flux_lookup(solar_flux, wavelen):
"""
Given the observed solar flux at 10.7 cm wavelength, returns the solar
flux at the given radar wavelength
Parameters
----------
solar_flux : float array
the solar fluxes measured at 10.7 cm wavelength [10e-22 W/(m2 Hz)]
wavelen : float
radar wavelength [m]
Returns
-------
s0 : float
the radar flux at the radar wavelength [10e-22 W/(m2 Hz)]
References
----------
Altube P., J. Bech, O. Argemi, T. Rigo, 2015: Quality Control of Antenna
Alignment and Receiver Calibration Using the Sun: Adaptation to Midrange
Weather Radar Observations at Low Elevation Angles
"""
# minimum flux
mfu = [1980., 495., 255., 170., 126., 102., 88., 76., 72., 68., 64., 61.,
58., 55., 54., 53., 52., 51., 50., 49., 48., 48., 47., 47., 47.,
46., 46., 45., 45., 45.]
# scale factor
sfa = [0.67, 0.68, 0.69, 0.70, 0.71, 0.73, 0.78, 0.84, 0.96, 1.00, 1.00,
0.98, 0.94, 0.90, 0.85, 0.80, 0.78, 0.77, 0.76, 0.75, 0.74, 0.73,
0.72, 0.71, 0.70, 0.69, 0.68, 0.67, 0.66, 0.65]
ind_w = int(wavelen*100.)-1 # table index
s0 = sfa[ind_w]*(solar_flux-64.)+mfu[ind_w] # solar flux at wavelen
return s0
[docs]def scanning_losses(angle_step, beamwidth):
"""
Given the antenna beam width and the integration angle, compute the
losses due to the fact that the sun is not a point target and the antenna
is scanning
Parameters
----------
angle_step : float
integration angle [deg]
beamwidth : float
3 dB-beamwidth [deg]
Returns
-------
la : float
The losses due to the scanning of the antenna [dB positive]
References
----------
Altube P., J. Bech, O. Argemi, T. Rigo, 2015: Quality Control of Antenna
Alignment and Receiver Calibration Using the Sun: Adaptation to Midrange
Weather Radar Observations at Low Elevation Angles
"""
delta_s = 0.57 # apparent diameter of radio sun [deg]
# sun convoluted antenna beamwidth look up table according to
# Altube et al. (2015) Table 2
delta_b = np.asarray(
[0.70, 0.75, 0.80, 0.85, 0.90, 0.95, 1.00, 1.10, 1.20, 1.30, 1.40,
1.50])
delta_c0 = np.asarray(
[0.78, 0.83, 0.87, 0.92, 0.96, 1.01, 1.06, 1.15, 1.25, 1.34, 1.44,
1.54])
if beamwidth < delta_b[0] or beamwidth > delta_b[-1]:
warn('Antenna beam width outside of range of valid antenna values ' +
'used to calculate sun convoluted beamwidth. The nominal ' +
'antenna beamwidth will be used instead.')
delta_c = beamwidth
else:
ind_c = np.where(delta_b <= beamwidth)[0][-1]
delta_c = (
delta_c0[ind_c]+(beamwidth-delta_b[ind_c]) *
(delta_c0[ind_c+1]-delta_c0[ind_c]) /
(delta_b[ind_c+1]-delta_b[ind_c]))
# losses due to scanning and antenna beamwidth
l0 = 1./np.log(2.)*beamwidth**2./delta_s**2.*(
1.-np.exp(-np.log(2.)*delta_s**2./beamwidth**2))
la = -10.*np.log10(
l0*np.sqrt(np.pi/(4.*np.log(2.)))*delta_c/angle_step *
erf(np.sqrt(np.log(2.))*angle_step/delta_c))
return la