apsis-rs/crates/rf/apsis-atmosphere/src/gaseous_path.rs

311 lines
12 KiB
Rust

//! Slant path attenuation through the atmosphere.
//!
//! The attenuation along a slant path is not simply the specific attenuation
//! divided by the sine of the elevation, because the atmosphere thins with
//! height and the path is curved. ITU-R Recommendation P.676 reduces the
//! problem to two equivalent heights, one for dry air and one for water
//! vapour, so that the zenith attenuation is the specific attenuation at the
//! surface multiplied by that height.
//!
//! The equivalent height of dry air depends on frequency through the shape
//! of the oxygen complex, and the equivalent height of water vapour depends
//! on frequency and on the vapour density. Both depend on the surface
//! pressure and temperature.
//!
//! Source: ITU-R Recommendation P.676-12, equations 30 through 38.
use crate::gaseous::{Atmosphere, specific_attenuation_dry_air, specific_attenuation_water_vapour};
/// The coefficients of the dry air equivalent height, table 3 of P.676-12.
///
/// Each entry is the coefficient and the line frequency in gigahertz.
const DRY_HEIGHT_TERMS: &[(f64, f64)] = &[
(0.159_7, 118.750_334),
(0.106_6, 368.498_246),
(0.132_5, 424.763_020),
(0.124_2, 487.249_273),
(0.093_8, 715.392_902),
(0.144_8, 773.839_490),
(0.137_4, 834.145_546),
];
/// The coefficients of the water vapour equivalent height, table 4 of
/// P.676-12.
///
/// Each entry is the line frequency in gigahertz, a coefficient, and a
/// width-like parameter.
const WET_HEIGHT_TERMS: &[(f64, f64, f64)] = &[
(22.235_08, 1.52, 2.56),
(183.310_087, 7.62, 10.2),
(325.152_888, 1.56, 2.7),
(380.197_353, 4.15, 5.7),
(439.150_807, 0.2, 0.91),
(448.001_085, 1.63, 2.46),
(474.689_092, 0.76, 2.22),
(488.490_108, 0.26, 2.49),
(556.935_985, 7.81, 10.0),
(620.700_87, 1.25, 2.35),
(752.033_113, 16.2, 20.0),
(916.171_582, 1.47, 2.58),
(970.315_022, 1.36, 2.44),
(987.926_764, 1.6, 1.86),
];
/// The equivalent height of dry air and of water vapour, in kilometers.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct EquivalentHeights {
/// The equivalent height of dry air, in kilometers.
pub dry_km: f64,
/// The equivalent height of water vapour, in kilometers.
pub wet_km: f64,
}
/// Returns the equivalent heights of dry air and water vapour.
///
/// The heights are the lengths that, multiplied by the surface specific
/// attenuation, give the zenith attenuation. The dry air height is limited
/// to a pressure dependent bound below 70 GHz, where the oxygen complex is
/// narrow enough that the height would otherwise be overestimated.
///
/// The frequency is in gigahertz.
///
/// 频率单位为吉赫。
///
/// Returns `None` when the frequency or atmosphere is not usable.
///
/// 频率或大气不可用时返回 `None`。
///
/// Source: ITU-R Recommendation P.676-12, equations 30 through 38.
pub fn equivalent_heights(frequency_ghz: f64, atmosphere: Atmosphere) -> Option<EquivalentHeights> {
if !frequency_ghz.is_finite() || frequency_ghz <= 0.0 || !atmosphere.is_valid() {
return None;
}
let temperature = atmosphere.temperature_k;
let rho = atmosphere.water_vapour_density;
let e = atmosphere.water_vapour_partial_pressure();
let rp = (atmosphere.pressure_hpa + e) / 1_013.25;
// Equation 31, the resonance term centred just below 60 GHz.
let term1 = 5.104_0 / (1.0 + 0.066 * rp.powf(-2.3))
* (-((frequency_ghz - 59.7) / (2.87 + 12.4 * (-7.9 * rp).exp())).powi(2)).exp();
// Equation 32, the residual contribution of the submillimetre lines.
let term2: f64 = DRY_HEIGHT_TERMS
.iter()
.map(|&(coefficient, line_frequency)| {
coefficient * (2.12 * rp).exp()
/ ((frequency_ghz - line_frequency).powi(2) + 0.025 * (2.2 * rp).exp())
})
.sum();
// Equation 33, the far wing correction.
let term3 = 0.011_4 * frequency_ghz / (1.0 + 0.14 * rp.powf(-2.6))
* (15.02 * frequency_ghz.powi(2) - 1_353.0 * frequency_ghz + 5.333e4)
/ (frequency_ghz.powi(3) - 151.3 * frequency_ghz.powi(2) + 9_629.0 * frequency_ghz
- 6_803.0);
let a = 0.783_2 + 0.007_09 * (temperature - 273.15);
// Equation 30, with the pressure dependent cap that applies below
// 70 GHz.
let dry = 6.1 * a / (1.0 + 0.17 * rp.powf(-1.1)) * (1.0 + term1 + term2 + term3);
let dry = if frequency_ghz < 70.0 {
dry.min(10.7 * rp.powf(0.3))
} else {
dry
};
// Equations 36 through 38 and 35b.
let a_wet = 1.929_8 - 0.041_66 * (temperature - 273.15) + 0.051_7 * rho;
let b_wet = 1.167_4 - 0.006_22 * (temperature - 273.15) + 0.006_3 * rho;
let sigma = 1.013 / (1.0 + (-8.6 * (rp - 0.57)).exp());
let wet = a_wet
+ b_wet
* WET_HEIGHT_TERMS
.iter()
.map(|&(line_frequency, coefficient, width)| {
coefficient * sigma / ((frequency_ghz - line_frequency).powi(2) + width * sigma)
})
.sum::<f64>();
Some(EquivalentHeights {
dry_km: dry,
wet_km: wet,
})
}
/// Returns the zenith attenuation of the atmosphere, in decibels.
///
/// This is the specific attenuation at the surface multiplied by the
/// equivalent height, summed over the two constituents.
///
/// The frequency is in gigahertz.
///
/// 频率单位为吉赫。
///
/// Returns `None` when the frequency or atmosphere is not usable.
///
/// 频率或大气不可用时返回 `None`。
pub fn zenith_attenuation(frequency_ghz: f64, atmosphere: Atmosphere) -> Option<f64> {
let heights = equivalent_heights(frequency_ghz, atmosphere)?;
let dry = specific_attenuation_dry_air(frequency_ghz, atmosphere)?;
let wet = specific_attenuation_water_vapour(frequency_ghz, atmosphere)?;
Some(dry * heights.dry_km + wet * heights.wet_km)
}
/// Returns the attenuation along a slant path at an elevation angle, in
/// decibels.
///
/// The zenith attenuation is divided by the sine of the elevation. The
/// plane parallel approximation this uses is the one the recommendation
/// applies for the approximate method; it is accurate above about five
/// degrees of elevation.
///
/// The elevation is in radians.
///
/// Returns `None` when the frequency or atmosphere is not usable, or when
/// the elevation is at or below the horizon.
pub fn slant_path_attenuation(
frequency_ghz: f64,
elevation: f64,
atmosphere: Atmosphere,
) -> Option<f64> {
if !elevation.is_finite() || elevation.sin() <= 0.0 {
return None;
}
Some(zenith_attenuation(frequency_ghz, atmosphere)? / elevation.sin())
}
/// The number of layer boundaries used by the exact slant path integration.
///
/// Recommendation P.676 defines the boundaries by
/// `delta_i = 1e-4 exp(i / 100)` for `i` from zero to 921, which gives 922
/// boundaries. The layers between them are thin near the surface and grow
/// with height, so the integration resolves the dense lower atmosphere where
/// the absorption is concentrated.
const EXACT_BOUNDARY_COUNT: usize = 922;
/// The Earth radius used by the ray trace, in kilometers.
///
/// The recommendation uses the mean radius, not the geopotential radius of
/// the standard atmosphere, because this value enters the geometry of the
/// ray rather than the definition of the height.
const RAY_TRACE_EARTH_RADIUS_KM: f64 = 6_371.0;
/// Returns the attenuation along a slant path by the exact method of
/// Recommendation P.676.
///
/// The exact method divides the atmosphere into a stack of layers whose
/// thickness grows exponentially with height, evaluates the specific
/// attenuation in each, and sums the contribution of each layer weighted by
/// the ray path length through it. Because the ray is refracted, the path
/// length is found by ray tracing with Snell's law applied to the ratio of
/// the refractive indices of adjacent layers, which the approximate method
/// ignores.
///
/// This is the form to use at low elevation, where the plane parallel
/// approximation that [`slant_path_attenuation`] uses is inaccurate. It is
/// far more expensive, so the approximate form remains available for the
/// high elevation case where it is adequate.
///
/// The frequency is in gigahertz, the elevation in radians, and the surface
/// atmosphere supplies the pressure, the water vapour density, and the
/// temperature at ground level. The surface water vapour density scales the
/// exponential vapour profile, so the atmosphere defines the whole stack.
///
/// Returns `None` when the frequency, elevation, or atmosphere is not
/// usable.
///
/// Source: ITU-R Recommendation P.676-12, equations 13 through 19, with the
/// layered atmosphere of ITU-R Recommendation P.835-6.
pub fn slant_path_attenuation_exact(
frequency_ghz: f64,
elevation: f64,
atmosphere: Atmosphere,
) -> Option<f64> {
if !frequency_ghz.is_finite() || frequency_ghz <= 0.0 || !atmosphere.is_valid() {
return None;
}
if !elevation.is_finite() || elevation <= 0.0 || elevation > core::f64::consts::FRAC_PI_2 {
return None;
}
// Build the stack of layer boundaries. Each entry holds the geometric
// height, the thickness, the profile values, and the refractive index.
let thickness_step = (1.0_f64 / 100.0).exp();
let mut heights = [0.0_f64; EXACT_BOUNDARY_COUNT];
let mut thicknesses = [0.0_f64; EXACT_BOUNDARY_COUNT];
let mut temperatures = [0.0_f64; EXACT_BOUNDARY_COUNT];
let mut pressures = [0.0_f64; EXACT_BOUNDARY_COUNT];
let mut densities = [0.0_f64; EXACT_BOUNDARY_COUNT];
let mut indices = [1.0_f64; EXACT_BOUNDARY_COUNT];
for index in 0..EXACT_BOUNDARY_COUNT {
let exponent = index as f64 / 100.0;
thicknesses[index] = 1.0e-4 * exponent.exp();
// Equation 15: the geometric height of the boundary.
heights[index] = 1.0e-4 * (exponent.exp() - 1.0) / (thickness_step - 1.0);
let temperature = crate::p835::standard_temperature(heights[index]);
let pressure = crate::p835::standard_pressure(heights[index]);
let density = crate::p835::standard_water_vapour_density(
heights[index],
atmosphere.water_vapour_density,
);
temperatures[index] = temperature;
pressures[index] = pressure;
densities[index] = density;
// The refractive index takes the total pressure, as the
// recommendation specifies, not the dry air pressure.
let vapour_pressure = density * temperature / 216.7;
indices[index] =
crate::p835::radio_refractive_index(pressure, vapour_pressure, temperature);
}
// The ray starts below the local horizon by the complement of the
// elevation, and is bent upward as it climbs.
let mut angle = core::f64::consts::FRAC_PI_2 - elevation;
let mut total = 0.0;
for index in 0..EXACT_BOUNDARY_COUNT {
let layer_atmosphere =
Atmosphere::new(pressures[index], densities[index], temperatures[index]);
let specific = crate::gaseous::specific_attenuation(frequency_ghz, layer_atmosphere)?;
// Equation 17: the path length through the layer.
let radius = RAY_TRACE_EARTH_RADIUS_KM + heights[index];
let delta = thicknesses[index];
let length = -radius * angle.cos()
+ 0.5
* (4.0 * radius * radius * angle.cos().powi(2)
+ 8.0 * radius * delta
+ 4.0 * delta * delta)
.sqrt();
total += length * specific;
// Equation 18a: the angle at which the ray leaves the layer.
let cosine_argument = ((-length * length - 2.0 * radius * delta - delta * delta)
/ (2.0 * length * radius + 2.0 * length * delta))
.clamp(-1.0, 1.0);
let leave_angle = core::f64::consts::PI - cosine_argument.acos();
// Equation 19a: Snell's law at the boundary, using the ratio of the
// refractive index here to the one above. The topmost boundary has
// no layer above it, so its ratio is one.
let next = (index + 1).min(EXACT_BOUNDARY_COUNT - 1);
let ratio = if next == index {
1.0
} else {
indices[index] / indices[next]
};
angle = (leave_angle.sin() * ratio).clamp(-1.0, 1.0).asin();
if !angle.is_finite() {
break;
}
}
Some(total)
}