# Copyright (C) 2012 VT SuperDARN Lab
# Full license can be found in LICENSE.txt
"""
*********************
**Module**: pydarn.radar.radFov
*********************
This module handles generating field-of-view projctions
**Classes**:
* :class:`fov`: field of view position
**Functions**:
* :func:`pydarn.radar.radFov.slantRange`: Calculate slant range
* :func:`pydarn.radar.radFov.calcAzOffBore`: Calculate off-array-normal azimuth
* :func:`pydarn.radar.radFov.calcFieldPnt`: Calculate field point projection
Based on Mike Ruohoniemi's GEOPACK
Based on R.J. Barnes radar.pro
"""
# *************************************************************
[docs]class fov(object):
""" This class calculates and stores field-of-view coordinates.
Provide the input-set [nbeams, ngates, bmsep, recrise] or a SITE object. Parameters from the input-set
will always take precedence over parameters from the SITE object.Make sure to provide frang and rsep,
the default values are not always applicable. The full projection gives the coordinates at each corner
of each gate, in the following order: looking in the beam direction, lower-left, lower-right,
upper-right, upper-left.
**Args**:
* **site**: site structure for a given radar and date-time
* **frang**: first range gate position [km] (defaults to 180 km) (scalar or ndarray(nbeams))
* **rsep**: range gate separation [km] (defaults to 45 km) (scalar or ndarray(nbeams))
* **nbeams**: number of beams (use site information if not provided)
* **ngates**: number of gates (use site information if not provided)
* **bmsep**: beam separation [degree] (use site information if not provided)
* **siteLat**: geographic latitude of radar [degree] (use site information if not provided)
* **siteLon**: geographic longitude of radar [degree] (use site information if not provided)
* **siteAlt**: altitude of radar site [m] (use site information if not provided)
* **siteBore**: radar boresite [degree] (use site information if not provided)
* **recrise**: receiver rise time [us] (use site information if not provided) (scalar or ndarray(nbeams))
* **elevation**: elevation angle [degree] (if not provided, is evaluated using 'model') (scalar or ndarray(ngates) or ndarray(nbeams,ngates))
* **altitude**: altitude [km] (if not provided, set to 300 km) (scalar or ndarray(ngates) or ndarray(nbeams,ngates))
* **model**:
* **'IS'**: for ionopsheric scatter projection model (default)
* **'GS'**: for ground scatter projection model
* **None**: if you are really confident in your elevation or altitude values
* ... more to come
* **coords**: 'geo', 'mag'
"""
def __init__(self, \
frang=180.0, rsep=45.0, site=None, \
nbeams=None, ngates=None, bmsep=None, recrise=None, \
siteLat=None, siteLon=None, siteBore=None, siteAlt=None, \
elevation=None, altitude=300., \
model='IS', coords='geo'):
# Get fov
from numpy import ndarray, array, arange, zeros, nan
import models.aacgm as aacgm
# Test that we have enough input arguments to work with
if not site and None in [nbeams, ngates, bmsep, recrise, siteLat, siteLon, siteBore, siteAlt]:
print 'calcFov: must provide either a site object or [nbeams, ngates, bmsep, recrise, siteLat, siteLon, siteBore, siteAlt].'
return
# Then assign variables from the site object if necessary
if site:
if not nbeams: nbeams = site.maxbeam
if not ngates: ngates = site.maxgate
if not bmsep: bmsep = site.bmsep
if not recrise: recrise = site.recrise
if not siteLat: siteLat = site.geolat
if not siteLon: siteLon = site.geolon
if not siteAlt: siteAlt = site.alt
if not siteBore: siteBore = site.boresite
# Some type checking. Look out for arrays
# If frang, rsep or recrise are arrays, then they should be of shape (nbeams,)
# Set a flag if any of frang, rsep or recrise is an array
isParamArray = False
if isinstance(frang, ndarray):
isParamArray = True
if len(frang) != nbeams:
print 'getFov: frang must be of a scalar or ndarray(nbeams). Using first element: {}'.format(frang[0])
frang = frang[0] * ones(nbeams+1)
# Array is adjusted to add on extra beam edge by copying the last element
else: frang = np.append(frang, frang[-1])
else: frang = array([frang])
if isinstance(rsep, ndarray):
isParamArray = True
if len(rsep) != nbeams:
print 'getFov: rsep must be of a scalar or ndarray(nbeams). Using first element: {}'.format(rsep[0])
rsep = rsep[0] * ones(nbeams+1)
# Array is adjusted to add on extra beam edge by copying the last element
else: rsep = np.append(rsep, rsep[-1])
else: rsep = array([rsep])
if isinstance(recrise, ndarray):
isParamArray = True
if len(recrise) != nbeams:
print 'getFov: recrise must be of a scalar or ndarray(nbeams). Using first element: {}'.format(recrise[0])
recrise = recrise[0] * ones(nbeams+1)
# Array is adjusted to add on extra beam edge by copying the last element
else: recrise = np.append(recrise, recrise[-1])
else: recrise = array([recrise])
# If altitude or elevation are arrays, then they should be of shape (nbeams,ngates)
if isinstance(altitude, ndarray):
if altitude.ndim == 1:
if altitude.size != ngates:
print 'getFov: altitude must be of a scalar or ndarray(ngates) or ndarray(nbeans,ngates). Using first element: {}'.format(altitude[0])
altitude = altitude[0] * ones((nbeams+1, ngates+1))
# Array is adjusted to add on extra beam/gate edge by copying the last element and replicating the whole array as many times as beams
else: altitude = np.resize( np.append(altitude, altitude[-1]), (nbeams+1,ngates+1) )
elif altitude.ndim == 2:
if altitude.shape != (nbeams, ngates):
print 'getFov: altitude must be of a scalar or ndarray(ngates) or ndarray(nbeans,ngates). Using first element: {}'.format(altitude[0])
altitude = altitude[0] * ones((nbeams+1, ngates+1))
# Array is adjusted to add on extra beam/gate edge by copying the last row and column
else:
altitude = np.append(altitude, altitude[-1,:].reshape(1,ngates), axis=0)
altitude = np.append(altitude, altitude[:,-1].reshape(nbeams,1), axis=1)
else:
print 'getFov: altitude must be of a scalar or ndarray(ngates) or ndarray(nbeans,ngates). Using first element: {}'.format(altitude[0])
altitude = altitude[0] * ones((nbeams+1, ngates+1))
if isinstance(elevation, ndarray):
if elevation.ndim == 1:
if elevation.size != ngates:
print 'getFov: elevation must be of a scalar or ndarray(ngates) or ndarray(nbeans,ngates). Using first element: {}'.format(elevation[0])
elevation = elevation[0] * ones((nbeams+1, ngates+1))
# Array is adjusted to add on extra beam/gate edge by copying the last element and replicating the whole array as many times as beams
else: elevation = np.resize( np.append(elevation, elevation[-1]), (nbeams+1,ngates+1) )
elif elevation.ndim == 2:
if elevation.shape != (nbeams, ngates):
print 'getFov: elevation must be of a scalar or ndarray(ngates) or ndarray(nbeans,ngates). Using first element: {}'.format(elevation[0])
elevation = elevation[0] * ones((nbeams+1, ngates+1))
# Array is adjusted to add on extra beam/gate edge by copying the last row and column
else:
elevation = np.append(elevation, elevation[-1,:].reshape(1,ngates), axis=0)
elevation = np.append(elevation, elevation[:,-1].reshape(nbeams,1), axis=1)
else:
print 'getFov: elevation must be of a scalar or ndarray(ngates) or ndarray(nbeans,ngates). Using first element: {}'.format(elevation[0])
elevation = elevation[0] * ones((nbeams+1, ngates+1))
# Generate beam/gate arrays
beams = arange(nbeams+1)
gates = arange(ngates+1)
# Create output arrays
slantRangeFull = zeros((nbeams+1, ngates+1), dtype='float')
latFull = zeros((nbeams+1, ngates+1), dtype='float')
lonFull = zeros((nbeams+1, ngates+1), dtype='float')
slantRangeCenter = zeros((nbeams+1, ngates+1), dtype='float')
latCenter = zeros((nbeams+1, ngates+1), dtype='float')
lonCenter = zeros((nbeams+1, ngates+1), dtype='float')
# Calculate deviation from boresight for center of beam
bOffCenter = bmsep * (beams - nbeams/2.0)
# Calculate deviation from boresight for edge of beam
bOffEdge = bmsep * (beams - nbeams/2.0 - 0.5)
# Iterates through beams
for ib in beams:
# if none of frang, rsep or recrise are arrays, then only execute this for the first loop, otherwise, repeat for every beam
if (~isParamArray and ib == 0) or isParamArray:
# Calculate center slant range
sRangCenter = slantRange(frang[ib], rsep[ib], recrise[ib], gates, center=True)
# Calculate edges slant range
sRangEdge = slantRange(frang[ib], rsep[ib], recrise[ib], gates, center=False)
# Save into output arrays
slantRangeCenter[ib, :-1] = sRangCenter[:-1]
slantRangeFull[ib,:] = sRangEdge
# Calculate coordinates for Edge and Center of the current beam
for ig in gates:
# This is a bit redundant, but I could not think of any other way to deal with the array-or-not-array issue
if not isinstance(altitude, ndarray) and not isinstance(elevation, ndarray):
tElev = elevation
tAlt = altitude
elif isinstance(altitude, ndarray) and not isinstance(elevation, ndarray):
tElev = elevation
tAlt = altitude[ib,ig]
elif isinstance(elevation, ndarray) and not isinstance(altitude, ndarray):
tElev = elevation[ib,ig]
tAlt = altitude
else:
tElev = elevation[ib,ig]
tAlt = altitude[ib,ig]
if model == 'GS':
if (~isParamArray and ib == 0) or isParamArray:
slantRangeCenter[ib,ig] = gsMapSlantRange(sRangCenter[ig],altitude=None,elevation=None)
slantRangeFull[ib,ig] = gsMapSlantRange(sRangEdge[ig],altitude=None,elevation=None)
sRangCenter[ig] = slantRangeCenter[ib,ig]
sRangEdge[ig] = slantRangeFull[ib,ig]
if (sRangCenter[ig] != -1) and (sRangEdge[ig] != -1):
# Then calculate projections
latC, lonC = calcFieldPnt(siteLat, siteLon, siteAlt*1e-3, siteBore, bOffCenter[ib], sRangCenter[ig], \
elevation=tElev, altitude=tAlt, model=model)
latE, lonE = calcFieldPnt(siteLat, siteLon, siteAlt*1e-3, siteBore, bOffEdge[ib], sRangEdge[ig], \
elevation=tElev, altitude=tAlt, model=model)
if(coords == 'mag'):
latC, lonC, _ = aacgm.aacgmlib.aacgmConv(latC,lonC,0.,0)
latE, lonE, _ = aacgm.aacgmlib.aacgmConv(latE,lonE,0.,0)
else:
latC, lonC = nan, nan
latE, lonE = nan, nan
# Save into output arrays
latCenter[ib, ig] = latC
lonCenter[ib, ig] = lonC
latFull[ib, ig] = latE
lonFull[ib, ig] = lonE
# Output is...
self.latCenter= latCenter[:-1,:-1]
self.lonCenter = lonCenter[:-1,:-1]
self.slantRCenter = slantRangeCenter[:-1,:-1]
self.latFull = latFull
self.lonFull = lonFull
self.slantRFull = slantRangeFull
self.beams = beams[:-1]
self.gates = gates[:-1]
self.coords = coords
# *************************************************************
def __str__(self):
from numpy import shape
outstring = 'latCenter: {} \
\nlonCenter: {} \
\nlatFull: {} \
\nlonFull: {} \
\nslantRCenter: {} \
\nslantRFull: {} \
\nbeams: {} \
\ngates: {} \
\ncoords: {}'.format(shape(self.latCenter), \
shape(self.lonCenter), \
shape(self.latFull), \
shape(self.lonFull), \
shape(self.slantRCenter), \
shape(self.slantRFull), \
shape(self.beams), \
shape(self.gates), \
self.coords)
return outstring
# *************************************************************
# *************************************************************
[docs]def calcFieldPnt(tGeoLat, tGeoLon, tAlt, boreSight, boreOffset, slantRange, \
elevation=None, altitude=None, model=None, coords='geo'):
"""
Calculate coordinates of field point given the radar coordinates and boresight,
the pointing direction deviation from boresight and elevation angle, and the
field point slant range and altitude. Either the elevation or the altitude must
be provided. If none is provided, the altitude is set to 300 km and the elevation
evaluated to accomodate altitude and range.
**INPUTS**:
* **tGeoLat**: transmitter latitude [degree, N]
* **tGeoLon**: transmitter longitude [degree, E]
* **tAlt**: transmitter altitude [km]
* **boreSight**: boresight azimuth [degree, E]
* **boreOffset**: offset from boresight [degree]
* **slantRange**: slant range [km]
* **elevation**: elevation angle [degree] (estimated if None)
* **altitude**: altitude [km] (default 300 km)
* **model**:
* **'IS'**: for ionopsheric scatter projection model
* **'GS'**: for ground scatter projection model
* **None**: if you are really confident in your elevation or altitude data
* ... more to come
* **coords**: 'geo' (more to come)
"""
from math import radians, degrees, sin, cos, asin, atan, sqrt, pi
from utils import Re, geoPack
# Make sure we have enough input stuff
# if (not model) and (not elevation or not altitude): model = 'IS'
# Now let's get to work
# Classic Ionospheric/Ground scatter projection model
if model in ['IS','GS']:
# Make sure you have altitude, because these 2 projection models rely on it
if not elevation and not altitude:
# Set default altitude to 300 km
altitude = 300.0
elif elevation and not altitude:
# If you have elevation but not altitude, then you calculate altitude, and elevation will be adjusted anyway
altitude = sqrt( Re**2 + slantRange**2 + 2. * slantRange * Re * sin( radians(elevation) ) ) - Re
# Now you should have altitude (and maybe elevation too, but it won't be used in the rest of the algorithm)
# Adjust altitude so that it makes sense with common scatter distribution
xAlt = altitude
if model == 'IS':
if altitude > 150. and slantRange <= 600:
xAlt = 115.
elif altitude > 150. and slantRange > 600. and slantRange <= 800.:
xAlt = 115. + ( slantRange - 600. ) / 200. * ( altitude - 115. )
elif model == 'GS':
if altitude > 150. and slantRange <= 300:
xAlt = 115.
elif altitude > 150. and slantRange > 300. and slantRange <= 500.:
xAlt = 115. + ( slantRange - 300. ) / 200. * ( altitude - 115. )
if slantRange < 150.: xAlt = slantRange / 150. * 115.
# To start, set Earth radius below field point to Earth radius at radar
(lat,lon,tRe) = geoPack.geodToGeoc(tGeoLat, tGeoLon)
RePos = tRe
# Iterate until the altitude corresponding to the calculated elevation matches the desired altitude
n = 0L # safety counter
while True:
# pointing elevation (spherical Earth value) [degree]
tel = degrees( asin( ((RePos+xAlt)**2 - (tRe+tAlt)**2 - slantRange**2) / (2. * (tRe+tAlt) * slantRange) ) )
# estimate off-array-normal azimuth (because it varies slightly with elevation) [degree]
bOff = calcAzOffBore(tel, boreOffset)
# pointing azimuth
taz = boreSight + bOff
# calculate position of field point
dictOut = geoPack.calcDistPnt(tGeoLat, tGeoLon, tAlt, dist=slantRange, el=tel, az=taz)
# Update Earth radius
RePos = dictOut['distRe']
# stop if the altitude is what we want it to be (or close enough)
n += 1L
if abs(xAlt - dictOut['distAlt']) <= 0.5 or n > 2:
return dictOut['distLat'], dictOut['distLon']
break
# No projection model (i.e., the elevation or altitude is so good that it gives you the proper projection by simple geometric considerations)
elif not model:
# Using no models simply means tracing based on trustworthy elevation or altitude
if not altitude:
altitude = sqrt( Re**2 + slantRange**2 + 2. * slantRange * Re * sin( radians(elevation) ) ) - Re
if not elevation:
if(slantRange < altitude): altitude = slantRange - 10
elevation = degrees( asin( ((Re+altitude)**2 - (Re+tAlt)**2 - slantRange**2) / (2. * (Re+tAlt) * slantRange) ) )
# The tracing is done by calcDistPnt
dict = geoPack.calcDistPnt(tGeoLat, tGeoLon, tAlt, dist=slantRange, el=elevation, az=boreSight+boreOffset)
return dict['distLat'], dict['distLon']
# *************************************************************
# *************************************************************
[docs]def slantRange(frang, rsep, recrise, range_gate, center=True):
"""Calculate slant range
**INPUTS**:
* **frang**: first range gate position [km]
* **rsep**: range gate separation [km]
* **recrise**: receiver rise time [us]
* **range_gate**: range gate number(s)
* **center**: wether or not to compute the slant range in the center of the gate rather than at the edge
**OUTPUT**:
* **srang**: slant range [km]
"""
# Lag to first range gate [us]
lagfr = frang * 2./0.3
# Sample separation [us]
smsep = rsep * 2./0.3
# Range offset if calculating slant range at center of the gate
range_offset = -0.5*rsep if not center else 0.0
# Slant range [km]
srang = ( lagfr - recrise + range_gate * smsep ) * 0.3/2. + range_offset
return srang
# *************************************************************
# *************************************************************
[docs]def calcAzOffBore(elevation, boreOffset0):
"""
Calculate off-boresight azimuth as a function of elevation angle and zero-elevation off-boresight azimuth.
See Milan et al. [1997] for more details on how this works.
**INPUTS**:
* **elevation**: elevation angle [degree]
* **boreOffset0**: zero-elevation off-boresight azimuth [degree]
**OUTPUT**:
* **boreOffset**: off-boresight azimuth [degree]
"""
from math import radians, degrees, cos, sin, atan, pi, sqrt
if ( cos(radians(boreOffset0))**2 - sin(radians(elevation))**2 ) < 0:
if boreOffset0 >= 0: boreOffset = pi/2.
else: boreOffset = -pi/2.
else:
tan_bOff = sqrt( sin(radians(boreOffset0))**2 / ( cos(radians(boreOffset0))**2 - sin(radians(elevation))**2 ) )
if boreOffset0 >= 0: boreOffset = atan( tan_bOff )
else: boreOffset = -atan( tan_bOff )
return degrees(boreOffset)
[docs]def gsMapSlantRange(slantRange,altitude=None,elevation=None):
"""
Calculate the ground scatter mapped slant range. See Bristow et al. [1994] for more details.
**INPUTS**:
* **slantRange**: normal slant range [km]
* **altitude**: altitude [km] (defaults to 300 km)
* **elevation**: elevation angle [degree]
**OUTPUT**:
* **gsSlantRange**: ground scatter mapped slant range [km] (typically slightly less than 0.5*slantRange.
Will return -1 if (slantRange**2/4. - altitude**2 >= 0). This occurs when the scatter is too close and
this model breaks down.
"""
from math import radians, degrees, sin, cos, asin, atan, sqrt, pi
from utils import Re, geoPack
# Make sure you have altitude, because these 2 projection models rely on it
if not elevation and not altitude:
# Set default altitude to 300 km
altitude = 300.0
elif elevation and not altitude:
# If you have elevation but not altitude, then you calculate altitude, and elevation will be adjusted anyway
altitude = sqrt( Re**2 + slantRange**2 + 2. * slantRange * Re * sin( radians(elevation) ) ) - Re
if (slantRange**2)/4. - altitude**2 >= 0:
gsSlantRange = Re * asin(sqrt(slantRange**2/4. - altitude**2)/Re) #From Bristow et al. [1994]
else:
gsSlantRange = -1
return gsSlantRange