"""
This module contains the satellite class and functions to calculate the
along-track and along-range averaging parameters. Main methode and
definitions based on Lamer et al (2020) and Schirmacher et al. (2023).
Two pre-defined satellites are available: EarthCARE and CloudSat.
**EarthCARE**
- frequency: Kollias et al. (2014), Table 1
- velocity: Kollias et al. (2014), Eq 4
- antenna diameter: Kollias et al. (2014), Table 1
- altitude: Kollias et al. (2014), Table 1
- pulse length: Kollias et al. (2014), Table 1
- along track resolution: Kollias et al. (2014), Table 1
- range resolution: Kollias et al. (2014), Table 1
- ifov_factor: Kollias et al. (2022), Table 1
- ifov_scale: based on Tanelli et al. (2008) and as long nothing is reported it is 1 s
- detection limit: Kollias et al. (2014), Table 1
- pules repetition frequency (PRF): Kollias et al. (2022), Table 1
- noise_ze: Kollias et al. (2014), Table 1
- ze_bins: Hogan et al. (2005),
- ze_std: Hogan et al. (2005),
- ze_std_background:
- vm_bins_broad: Kollias et al. (2022), Figure 7
- vm_std_broad: Kollias et al. (2022), Figure 7
- vm_std_broad_background: Kollias et al. (2022)
**CloudSat**
- frequency: Kollias et al. (2014), Table 1
- velocity: Kollias et al. (2014), Eq 4
- antenna diameter: Kollias et al. (2014), Table 1
- altitude: Kollias et al. (2014), Table 1
- pulse length: Kollias et al. (2014), Table 1
- along track resolution: Kollias et al. (2014), Table 1
- range resolution: Kollias et al. (2014), Table 1
- ifov_factor: Kollias et al. (2022), Table 1 for the Arctic Schirmacher et al. (2023)
- ifov_scale: Tanelli et al. (2008), "integration accurecy of the pulse is 0.968 s"
- detection limit: Kollias et al. (2014), Table 1
- pules repetition frequency (PRF): Kollias et al. (2014, 2022), Table 1
- noise_ze:
- ze_bins: Hogan et al. (2005),
- ze_std: Hogan et al. (2005),
- ze_std_background:
- vm_bins_broad: not used
- vm_std_broad: not used
- vm_std_broad_background: not used
References
----------
Hogan et al. (2005) : https://doi.org/10.1175/JTECH1768.1
Kollias et al. (2014) : https://doi.org/10.1175/JTECH-D-11-00202.1
Kollias et al. (2022) : https://doi.org/10.3389/frsen.2022.860284
Lamer et al. (2020) : https://doi.org/10.5194/amt-13-2363-2020
Schirmacher et al. (2023) : https://doi.org/10.5194/egusphere-2023-636
Tanelli et al. (2008) : https://doi.org/10.1109/TGRS.2008.2002030
"""
from dataclasses import dataclass
from pathlib import Path
from typing import List, Union, Optional
import numpy as np
from orbital_radar.helpers import db2li
from orbital_radar.readers.rangewf import read_range_weighting_function
SPEED_OF_LIGHT = 299792458.0 # unit: m s-1
RADARS_PREDEFINED = {
"earthcare": {
"name": "EarthCARE",
"frequency": 94.05e9,
"velocity": 7600,
"antenna_diameter": 2.5,
"altitude": 400000,
"pulse_length": 500,
"along_track_resolution": 500,
"range_resolution": 100,
"ifov_factor": 74.5,
"ifov_scale": 1,
"detection_limit": -37,
"nyquist_velocity": 5.7,
"pulse_repetition_frequency": 7150,
"noise_ze": -37.01,
"ze_bins": [-37, -25, -13],
"ze_std": [0.5, 0.3, 0.2],
"ze_std_background": 0.2176,
"vm_bins_broad": [
-37,
-34,
-31,
-28,
-25,
-22,
-19,
-16,
-13,
-10,
-7,
-4,
],
"vm_std_broad": [
3.27,
3.12,
2.83,
2.35,
1.63,
1.09,
0.76,
0.59,
0.52,
0.49,
0.48,
0.47,
],
"vm_std_broad_background": 1.09,
},
"cloudsat": {
"name": "CloudSat",
"frequency": 94.05e9,
"velocity": 6800,
"antenna_diameter": 1.85,
"altitude": 705000,
"pulse_length": 480,
"along_track_resolution": 1093,
"range_resolution": 240,
"ifov_factor": 67,
"ifov_scale": 0.968,
"detection_limit": -27,
"nyquist_velocity": 5.7,
"pulse_repetition_frequency": 7150,
"noise_ze": -27.0,
"ze_bins": [
-37,
-34,
-31,
-28,
-25,
-22,
-19,
-16,
-13,
-10,
-7,
-4,
],
"ze_std": [
9.24,
4.77,
2.54,
1.41,
0.85,
0.56,
0.42,
0.35,
0.32,
0.3,
0.29,
0.28,
],
"ze_std_background": 0.2176,
"vm_bins_broad": [],
"vm_std_broad": [],
"vm_std_broad_background": np.nan,
},
}
[docs]@dataclass
class RadarSpec:
"""
This class contains the satellite parameters.
Units of radar specification
----------------------------
- frequency: radar frequency [Hz]
- velocity: satellite velocity [m s-1]
- antenna diameter: radar antenna diameter [m]
- altitude: satellite altitude [m]
- pulse length: radar pulse length [m]
- along track resolution: radar along track resolution [m]
- range resolution: radar range resolution [m]
- detection limit: radar detection limit [dBZ]
- noise_ze: radar noise floor [dBZ]
- ze_bins: radar Ze lookup table [dBZ]
- ze_std: radar standard deviation lookup table [dBZ]
- ze_std_background: radar standard deviation background [dBZ]
- vm_bins_broad: radar reflectivity bin of vm_std_broad [dBZ]
- vm_std_broad: Doppler velocity broadening due to platform motion [m s-1]
- vm_std_broad_background: radar standard deviation background [m s-1]
- nyquist velocity: radar nyquist velocity [m s-1]
- pulse repetition frequency: radar pulse repetition frequency [Hz]
"""
name: str
frequency: float
velocity: int
antenna_diameter: float
altitude: int
pulse_length: int
along_track_resolution: int
range_resolution: int
ifov_factor: float
ifov_scale: float
detection_limit: float
noise_ze: float
ze_bins: Union[List[float], np.ndarray]
ze_std: Union[List[float], np.ndarray]
ze_std_background: float
vm_bins_broad: Union[List[float], np.ndarray]
vm_std_broad: Union[List[float], np.ndarray]
vm_std_broad_background: float
nyquist_velocity: float = np.nan
pulse_repetition_frequency: float = np.nan
[docs]class RadarBeam:
"""
This class manages the satellite specifications from pre-defined or user-
specified space-borne radars. It also contains transformation functions
for along-track and along-range averaging.
"""
def __init__(
self,
file_earthcare=None,
sat_name=None,
nyquist_from_prf=False,
**sat_params,
):
"""
Initializes the satellite parameters and calculates along-track and
along-range weighting functions, and the velocity error due to
satellite velocity.
The function requires along-track and along-range bins.
The following parameters will be derived for later use in the simulator
- instantaneous field of view
- normalized along-track weighting function
- along track resolution
- normalized along-range weighting function
- range resolution
- satellite velocity error
Parameters
----------
file_earthcare : str
path to file containing EarthCARE CPR weighting function. This
file is used if the satellite name is 'earthcare'.
satellite_name : str
name of the satellite, e.g. 'earthcare' or 'cloudsat'
nqv_from_prf : bool
if True, the Nyquist velocity is calculated from the pulse
repetition frequency. If False, the Nyquist velocity must be given
as a parameter. Default is False.
**sat_params: keyword arguments to overwrite the predefined satellite
"""
# check if either sat_name or sat_params is given
if sat_name is None and sat_params is None:
raise ValueError("Either sat_name or sat_params must be given")
# check if sat_name is valid
if sat_name is not None and sat_name not in RADARS_PREDEFINED.keys():
raise ValueError(
f"Unknown satellite name: {sat_name}. "
f"Valid names are: {RADARS_PREDEFINED.keys()}"
)
# check if sat_params are valid
for key in sat_params.keys():
if key not in RADARS_PREDEFINED["earthcare"].keys():
raise ValueError(f"Unknown parameter: {key}")
# check if all keys are given if no satellite name is given
if sat_name is None and sat_params is not None:
for key in RADARS_PREDEFINED["earthcare"].keys():
if key not in sat_params.keys():
raise ValueError(f"Parameter {key} missing")
# check if file to EarthCARE CPR weighting function exists
if sat_name == "earthcare" and file_earthcare is not None:
if not Path(file_earthcare).exists():
raise ValueError(
f"EarthCARE CPR weighting function file does not exist: "
f"{file_earthcare}"
)
# warn if file for EarthCARE CPR weighting function is not given
if sat_name == "earthcare" and file_earthcare is None:
print(
"Warning: EarthCARE CPR weighting function file is not given. "
"Gaussian range weighting function will be used instead."
)
# warn that earthcare weighting function is not used
if sat_name != "earthcare" and file_earthcare is not None:
print(
"Warning: EarthCARE CPR weighting function file is not used "
"because satellite name is not 'earthcare'"
)
# set satellite parameters from pre-defined satellites
if sat_name is not None:
# update pre-defined satellite parameters with sat_params
radar_predefined = RADARS_PREDEFINED[sat_name].copy()
radar_predefined.update(sat_params)
self.spec = RadarSpec(**radar_predefined)
# set satellite parameters from user-specified satellite
else:
self.spec = RadarSpec(**sat_params)
# convert lookup tables to numpy arrays
self.spec.ze_bins = np.array(self.spec.ze_bins)
self.spec.ze_std = np.array(self.spec.ze_std)
self.spec.vm_bins_broad = np.array(self.spec.vm_bins_broad)
self.spec.vm_std_broad = np.array(self.spec.vm_std_broad)
self.sat_name = sat_name
# add range weighting function file
self.file_earthcare = file_earthcare
# initialize along-track and along-range averaging parameters
self.atrack_bins = np.array([])
self.atrack_weights = np.array([])
self.range_weights = np.array([])
self.range_bins = np.array([])
# initialize derived parameters
self.wavelength = np.nan
self.ifov = np.nan
self.theta_along = np.nan
self.velocity_error = np.nan
# calculate derived parameters
self.calculate_wavelength()
# calculate Nyquist velocity from pulse repetition frequency
if nyquist_from_prf:
print(
"Nyquist velocity is calculated from pulse repetition frequency."
)
self.calculate_nyquist_velocity()
else:
print("Nyquist velocity parameter is used instead of pulse "
"repition frequency.")
# show summary of satellite parameters
self.params
@property
def params(self):
"""Prints a summary of the satellite parameters"""
print(
f"Satellite: {self.spec.name}\n"
f"Frequency: {self.spec.frequency*1e-9} GHz\n"
f"Velocity: {self.spec.velocity} m s-1\n"
f"Antenna diameter: {self.spec.antenna_diameter} m\n"
f"Altitude: {self.spec.altitude} m\n"
f"Pulse length: {self.spec.pulse_length} m\n"
f"Horizontal resolution: {self.spec.along_track_resolution} m\n"
f"Vertical resolution: {self.spec.range_resolution} m\n"
f"Nyquist velocity: {np.round(self.spec.nyquist_velocity, 2)} m s-1\n"
f"Pulse repetition frequency: {np.round(self.spec.pulse_repetition_frequency, 0)} Hz\n"
)
[docs] def calculate_wavelength(self):
"""
Calculates the radar wavelength from the radar frequency.
Units
-----
- frequency: radar frequency [Hz]
- wavelength: radar wavelength [m]
- speed of light: speed of light [m s-1]
"""
self.wavelength = SPEED_OF_LIGHT / self.spec.frequency
[docs] def calculate_nyquist_velocity(self):
"""
Calculates the Nyquist velocity from the pulse repetition frequency
and the radar wavelength.
"""
self.spec.nyquist_velocity = (
self.wavelength * self.spec.pulse_repetition_frequency / 4
)
[docs] def calculate_ifov(self):
"""
Calculates the instantaneous field of view (IFOV) from the along-track
averaging parameters.
"""
# constant for ifov calculation
self.theta_along = (
self.spec.ifov_factor * self.wavelength
) / self.spec.antenna_diameter
# instantaneous field of view
self.ifov = (
self.spec.altitude
* np.tan(np.pi * self.theta_along / 180)
* self.spec.ifov_scale
)
[docs] def create_along_track_grid(self, along_track_coords):
"""
Creates the along-track grid.
The along-track grid is defined from -ifov/2 to ifov/2. The spacing
is defined by the along-track resolution. The outermost along-track
bins relative to the line of size always lie within the IFOV.
If the along-track grid is not equidistant, the along-track weighting
function cannot be calculated.
Parameters
----------
along_track_coords : array
along-track coordinates of the ground-based radar [m]
"""
assert len(np.unique(np.diff(along_track_coords))) == 1, (
"Along-track grid is not equidistant. "
"Along-track weighting function cannot be calculated."
)
# grid with size of ifov centered around zero
step = np.diff(along_track_coords)[0]
self.atrack_bins = np.append(
np.arange(-step, -self.ifov / 2, -step)[::-1],
np.arange(0, self.ifov / 2, step),
)
[docs] def create_along_range_grid(self, range_coords):
"""
Creates range grid at which range weighting function is evaluated.
The range grid is defined from -pulse_length to pulse_length. The
spacing is defined by the range resolution of the ground-based radar.
If the range grid is not equidistant, the range weighting function
cannot be calculated.
Parameters
----------
range_coords : array
range coordinates of the ground-based radar [m]
"""
assert len(np.unique(np.diff(range_coords))) == 1, (
"Range grid is not equidistant. "
"Range weighting function cannot be calculated."
)
# grid with size of two pulse lengths centered around zero
step = np.diff(range_coords)[0]
self.range_bins = np.arange(
-self.spec.pulse_length,
self.spec.pulse_length + step,
step,
)
[docs] def calculate_along_track(self, along_track_coords):
"""
Calculates along-track averaging parameters.
Parameters
----------
along_track_coords : array
along-track coordinates of the ground-based radar [m]
"""
# instantaneous field of view
self.calculate_ifov()
# calculate along-track grid
self.create_along_track_grid(along_track_coords=along_track_coords)
# along-track weighting function
w_at = np.exp(
-2 * np.log(2) * (self.atrack_bins / (self.ifov / 2)) ** 2
)
self.atrack_weights = w_at / np.sum(w_at) # normalization
assert (
np.sum(self.atrack_weights) - 1 < 1e-10
), "Along-track weighting function is not normalized"
[docs] def calculate_velocity_error(self):
"""
Calculates the velocity error due to satellite velocity.
"""
# velocity error due to satellite velocity
self.velocity_error = (
self.spec.velocity / self.spec.altitude
) * self.atrack_bins
[docs] def calculate_along_range(self, range_coords):
"""
Calculates along-range averaging parameters.
Parameters
----------
range_coords : array
range coordinates of the ground-based radar [m]
"""
self.create_along_range_grid(range_coords=range_coords)
# range weighting function
if self.sat_name == "earthcare" and self.file_earthcare is not None:
self.range_weights = (
self.normalized_range_weighting_function_earthcare()
)
else:
self.range_weights = (
self.normalized_range_weighting_function_default(
pulse_length=self.spec.pulse_length,
range_bins=self.range_bins,
)
)
[docs] def normalized_range_weighting_function_earthcare(self):
"""
Prepares EarthCARE range weighting function for along-range averaging.
The high-resolution weighting function is interpolated to the
range resolution of the ground-based radar.
Returns
-------
range_weights : array
normalized range weighting function
"""
ds_wf = read_range_weighting_function(self.file_earthcare)
# linearize the weighting function
da_wf = db2li(ds_wf["response"])
# convert from tau factor to range and set height as dimension
da_wf["height"] = da_wf["tau_factor"] * self.spec.pulse_length
da_wf = da_wf.swap_dims({"tau_factor": "height"})
# interpolate to range grid of ground-based radar
da_wf = da_wf.interp(height=self.range_bins, method="linear")
da_wf = da_wf.fillna(0)
# normalize the linear weighting function
da_wf /= da_wf.sum()
range_weights = da_wf.values
return range_weights
[docs] @staticmethod
def normalized_range_weighting_function_default(pulse_length, range_bins):
"""
Defines the range weighting function for the along-range averaging.
"""
# calculate along-range weighting function
w_const = -(np.pi**2) / (2.0 * np.log(2) * pulse_length**2)
range_weights = np.exp(w_const * range_bins**2)
range_weights = range_weights / np.sum(range_weights) # normalization
return range_weights
[docs] def calculate_weighting_functions(self, along_track_coords, range_coords):
"""
Calculates the along-track and along-range weighting functions.
Parameters
----------
along_track_coords : array
along-track coordinates of the ground-based radar [m]
range_coords : array
range coordinates of the ground-based radar [m]
"""
# calculate along-track averaging parameters
self.calculate_along_track(along_track_coords=along_track_coords)
# calculate velocity error due to satellite velocity
self.calculate_velocity_error()
# calculate along-range averaging parameters
self.calculate_along_range(range_coords=range_coords)