style(rust): apply cargo fmt across prop_grid_rs
This commit is contained in:
parent
efa3cc804d
commit
ed2563f4f8
17 changed files with 839 additions and 296 deletions
|
|
@ -76,14 +76,8 @@ pub const SUNRISE_TABLE: [f64; 12] = [
|
||||||
7.4, 7.3, 7.0, 6.7, 6.35, 6.25, 6.35, 6.65, 6.9, 7.1, 7.35, 7.45,
|
7.4, 7.3, 7.0, 6.7, 6.35, 6.25, 6.35, 6.65, 6.9, 7.1, 7.35, 7.45,
|
||||||
];
|
];
|
||||||
|
|
||||||
pub const HUMIDITY_BENEFICIAL_THRESHOLDS: &[(u8, i32)] = &[
|
pub const HUMIDITY_BENEFICIAL_THRESHOLDS: &[(u8, i32)] =
|
||||||
(4, 55),
|
&[(4, 55), (7, 70), (10, 82), (14, 90), (18, 95), (22, 88)];
|
||||||
(7, 70),
|
|
||||||
(10, 82),
|
|
||||||
(14, 90),
|
|
||||||
(18, 95),
|
|
||||||
(22, 88),
|
|
||||||
];
|
|
||||||
pub const HUMIDITY_BENEFICIAL_DEFAULT: i32 = 75;
|
pub const HUMIDITY_BENEFICIAL_DEFAULT: i32 = 75;
|
||||||
|
|
||||||
/// `(gradient_max, score_beneficial, score_harmful)`. First entry whose
|
/// `(gradient_max, score_beneficial, score_harmful)`. First entry whose
|
||||||
|
|
@ -160,73 +154,213 @@ const fn adj_from(values: &[(u8, i32)]) -> [i32; 12] {
|
||||||
// The seasonal_base tables are identical across many bands; pull out the
|
// The seasonal_base tables are identical across many bands; pull out the
|
||||||
// common shape once so the 22 band definitions stay scannable.
|
// common shape once so the 22 band definitions stay scannable.
|
||||||
const BASE_LOW_BAND: [i32; 12] = months([
|
const BASE_LOW_BAND: [i32; 12] = months([
|
||||||
(1, 38), (2, 40), (3, 22), (4, 55), (5, 68), (6, 90),
|
(1, 38),
|
||||||
(7, 95), (8, 75), (9, 78), (10, 88), (11, 78), (12, 25),
|
(2, 40),
|
||||||
|
(3, 22),
|
||||||
|
(4, 55),
|
||||||
|
(5, 68),
|
||||||
|
(6, 90),
|
||||||
|
(7, 95),
|
||||||
|
(8, 75),
|
||||||
|
(9, 78),
|
||||||
|
(10, 88),
|
||||||
|
(11, 78),
|
||||||
|
(12, 25),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const BASE_50: [i32; 12] = months([
|
const BASE_50: [i32; 12] = months([
|
||||||
(1, 30), (2, 32), (3, 25), (4, 50), (5, 70), (6, 95),
|
(1, 30),
|
||||||
(7, 95), (8, 70), (9, 65), (10, 70), (11, 60), (12, 25),
|
(2, 32),
|
||||||
|
(3, 25),
|
||||||
|
(4, 50),
|
||||||
|
(5, 70),
|
||||||
|
(6, 95),
|
||||||
|
(7, 95),
|
||||||
|
(8, 70),
|
||||||
|
(9, 65),
|
||||||
|
(10, 70),
|
||||||
|
(11, 60),
|
||||||
|
(12, 25),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const BASE_24G: [i32; 12] = months([
|
const BASE_24G: [i32; 12] = months([
|
||||||
(1, 88), (2, 84), (3, 68), (4, 62), (5, 51), (6, 34),
|
(1, 88),
|
||||||
(7, 18), (8, 18), (9, 48), (10, 68), (11, 96), (12, 88),
|
(2, 84),
|
||||||
|
(3, 68),
|
||||||
|
(4, 62),
|
||||||
|
(5, 51),
|
||||||
|
(6, 34),
|
||||||
|
(7, 18),
|
||||||
|
(8, 18),
|
||||||
|
(9, 48),
|
||||||
|
(10, 68),
|
||||||
|
(11, 96),
|
||||||
|
(12, 88),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const BASE_47G: [i32; 12] = months([
|
const BASE_47G: [i32; 12] = months([
|
||||||
(1, 90), (2, 88), (3, 78), (4, 68), (5, 55), (6, 38),
|
(1, 90),
|
||||||
(7, 22), (8, 22), (9, 48), (10, 74), (11, 96), (12, 90),
|
(2, 88),
|
||||||
|
(3, 78),
|
||||||
|
(4, 68),
|
||||||
|
(5, 55),
|
||||||
|
(6, 38),
|
||||||
|
(7, 22),
|
||||||
|
(8, 22),
|
||||||
|
(9, 48),
|
||||||
|
(10, 74),
|
||||||
|
(11, 96),
|
||||||
|
(12, 90),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const BASE_68G: [i32; 12] = months([
|
const BASE_68G: [i32; 12] = months([
|
||||||
(1, 90), (2, 88), (3, 78), (4, 65), (5, 50), (6, 32),
|
(1, 90),
|
||||||
(7, 18), (8, 18), (9, 44), (10, 70), (11, 92), (12, 90),
|
(2, 88),
|
||||||
|
(3, 78),
|
||||||
|
(4, 65),
|
||||||
|
(5, 50),
|
||||||
|
(6, 32),
|
||||||
|
(7, 18),
|
||||||
|
(8, 18),
|
||||||
|
(9, 44),
|
||||||
|
(10, 70),
|
||||||
|
(11, 92),
|
||||||
|
(12, 90),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const BASE_75G: [i32; 12] = months([
|
const BASE_75G: [i32; 12] = months([
|
||||||
(1, 90), (2, 90), (3, 80), (4, 68), (5, 55), (6, 38),
|
(1, 90),
|
||||||
(7, 22), (8, 22), (9, 48), (10, 74), (11, 96), (12, 90),
|
(2, 90),
|
||||||
|
(3, 80),
|
||||||
|
(4, 68),
|
||||||
|
(5, 55),
|
||||||
|
(6, 38),
|
||||||
|
(7, 22),
|
||||||
|
(8, 22),
|
||||||
|
(9, 48),
|
||||||
|
(10, 74),
|
||||||
|
(11, 96),
|
||||||
|
(12, 90),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const BASE_122G: [i32; 12] = months([
|
const BASE_122G: [i32; 12] = months([
|
||||||
(1, 92), (2, 90), (3, 78), (4, 62), (5, 45), (6, 28),
|
(1, 92),
|
||||||
(7, 15), (8, 15), (9, 38), (10, 68), (11, 92), (12, 92),
|
(2, 90),
|
||||||
|
(3, 78),
|
||||||
|
(4, 62),
|
||||||
|
(5, 45),
|
||||||
|
(6, 28),
|
||||||
|
(7, 15),
|
||||||
|
(8, 15),
|
||||||
|
(9, 38),
|
||||||
|
(10, 68),
|
||||||
|
(11, 92),
|
||||||
|
(12, 92),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const BASE_134G: [i32; 12] = months([
|
const BASE_134G: [i32; 12] = months([
|
||||||
(1, 92), (2, 90), (3, 78), (4, 65), (5, 48), (6, 30),
|
(1, 92),
|
||||||
(7, 18), (8, 18), (9, 42), (10, 70), (11, 92), (12, 92),
|
(2, 90),
|
||||||
|
(3, 78),
|
||||||
|
(4, 65),
|
||||||
|
(5, 48),
|
||||||
|
(6, 30),
|
||||||
|
(7, 18),
|
||||||
|
(8, 18),
|
||||||
|
(9, 42),
|
||||||
|
(10, 70),
|
||||||
|
(11, 92),
|
||||||
|
(12, 92),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const BASE_142G: [i32; 12] = months([
|
const BASE_142G: [i32; 12] = months([
|
||||||
(1, 92), (2, 90), (3, 78), (4, 65), (5, 47), (6, 28),
|
(1, 92),
|
||||||
(7, 16), (8, 16), (9, 40), (10, 68), (11, 92), (12, 92),
|
(2, 90),
|
||||||
|
(3, 78),
|
||||||
|
(4, 65),
|
||||||
|
(5, 47),
|
||||||
|
(6, 28),
|
||||||
|
(7, 16),
|
||||||
|
(8, 16),
|
||||||
|
(9, 40),
|
||||||
|
(10, 68),
|
||||||
|
(11, 92),
|
||||||
|
(12, 92),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const BASE_145G: [i32; 12] = months([
|
const BASE_145G: [i32; 12] = months([
|
||||||
(1, 92), (2, 90), (3, 78), (4, 64), (5, 46), (6, 27),
|
(1, 92),
|
||||||
(7, 15), (8, 15), (9, 38), (10, 66), (11, 92), (12, 92),
|
(2, 90),
|
||||||
|
(3, 78),
|
||||||
|
(4, 64),
|
||||||
|
(5, 46),
|
||||||
|
(6, 27),
|
||||||
|
(7, 15),
|
||||||
|
(8, 15),
|
||||||
|
(9, 38),
|
||||||
|
(10, 66),
|
||||||
|
(11, 92),
|
||||||
|
(12, 92),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const BASE_241G: [i32; 12] = months([
|
const BASE_241G: [i32; 12] = months([
|
||||||
(1, 95), (2, 92), (3, 75), (4, 55), (5, 35), (6, 15),
|
(1, 95),
|
||||||
(7, 8), (8, 8), (9, 30), (10, 65), (11, 95), (12, 95),
|
(2, 92),
|
||||||
|
(3, 75),
|
||||||
|
(4, 55),
|
||||||
|
(5, 35),
|
||||||
|
(6, 15),
|
||||||
|
(7, 8),
|
||||||
|
(8, 8),
|
||||||
|
(9, 30),
|
||||||
|
(10, 65),
|
||||||
|
(11, 95),
|
||||||
|
(12, 95),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const BASE_288G: [i32; 12] = months([
|
const BASE_288G: [i32; 12] = months([
|
||||||
(1, 95), (2, 92), (3, 75), (4, 55), (5, 35), (6, 14),
|
(1, 95),
|
||||||
(7, 7), (8, 7), (9, 28), (10, 64), (11, 95), (12, 95),
|
(2, 92),
|
||||||
|
(3, 75),
|
||||||
|
(4, 55),
|
||||||
|
(5, 35),
|
||||||
|
(6, 14),
|
||||||
|
(7, 7),
|
||||||
|
(8, 7),
|
||||||
|
(9, 28),
|
||||||
|
(10, 64),
|
||||||
|
(11, 95),
|
||||||
|
(12, 95),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const BASE_322G: [i32; 12] = months([
|
const BASE_322G: [i32; 12] = months([
|
||||||
(1, 96), (2, 92), (3, 74), (4, 52), (5, 32), (6, 12),
|
(1, 96),
|
||||||
(7, 6), (8, 6), (9, 26), (10, 62), (11, 96), (12, 96),
|
(2, 92),
|
||||||
|
(3, 74),
|
||||||
|
(4, 52),
|
||||||
|
(5, 32),
|
||||||
|
(6, 12),
|
||||||
|
(7, 6),
|
||||||
|
(8, 6),
|
||||||
|
(9, 26),
|
||||||
|
(10, 62),
|
||||||
|
(11, 96),
|
||||||
|
(12, 96),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const BASE_403G: [i32; 12] = months([
|
const BASE_403G: [i32; 12] = months([
|
||||||
(1, 96), (2, 92), (3, 74), (4, 52), (5, 32), (6, 12),
|
(1, 96),
|
||||||
(7, 6), (8, 6), (9, 26), (10, 62), (11, 96), (12, 96),
|
(2, 92),
|
||||||
|
(3, 74),
|
||||||
|
(4, 52),
|
||||||
|
(5, 32),
|
||||||
|
(6, 12),
|
||||||
|
(7, 6),
|
||||||
|
(8, 6),
|
||||||
|
(9, 26),
|
||||||
|
(10, 62),
|
||||||
|
(11, 96),
|
||||||
|
(12, 96),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ADJ_NONE: [i32; 12] = [0; 12];
|
const ADJ_NONE: [i32; 12] = [0; 12];
|
||||||
|
|
@ -242,187 +376,387 @@ fn build_bands() -> Vec<BandConfig> {
|
||||||
use HumidityEffect::*;
|
use HumidityEffect::*;
|
||||||
vec![
|
vec![
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 50, label: "50 MHz",
|
freq_mhz: 50,
|
||||||
o2_db_km: 0.0, h2o_coeff: 0.0, humidity_effect: Beneficial, humidity_penalty: 0.0,
|
label: "50 MHz",
|
||||||
rain_k: 0.0, rain_alpha: 1.0,
|
o2_db_km: 0.0,
|
||||||
seasonal_base: BASE_50, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.0,
|
||||||
typical_range_km: 400, extended_range_km: 1500, exceptional_range_km: 4000,
|
humidity_effect: Beneficial,
|
||||||
|
humidity_penalty: 0.0,
|
||||||
|
rain_k: 0.0,
|
||||||
|
rain_alpha: 1.0,
|
||||||
|
seasonal_base: BASE_50,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 400,
|
||||||
|
extended_range_km: 1500,
|
||||||
|
exceptional_range_km: 4000,
|
||||||
weights: None,
|
weights: None,
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 144, label: "144 MHz",
|
freq_mhz: 144,
|
||||||
o2_db_km: 0.0, h2o_coeff: 0.0, humidity_effect: Beneficial, humidity_penalty: 0.0,
|
label: "144 MHz",
|
||||||
rain_k: 0.0, rain_alpha: 1.0,
|
o2_db_km: 0.0,
|
||||||
seasonal_base: BASE_LOW_BAND, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.0,
|
||||||
typical_range_km: 350, extended_range_km: 900, exceptional_range_km: 2500,
|
humidity_effect: Beneficial,
|
||||||
|
humidity_penalty: 0.0,
|
||||||
|
rain_k: 0.0,
|
||||||
|
rain_alpha: 1.0,
|
||||||
|
seasonal_base: BASE_LOW_BAND,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 350,
|
||||||
|
extended_range_km: 900,
|
||||||
|
exceptional_range_km: 2500,
|
||||||
weights: None,
|
weights: None,
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 222, label: "222 MHz",
|
freq_mhz: 222,
|
||||||
o2_db_km: 0.0, h2o_coeff: 0.0, humidity_effect: Beneficial, humidity_penalty: 0.0,
|
label: "222 MHz",
|
||||||
rain_k: 0.0, rain_alpha: 1.0,
|
o2_db_km: 0.0,
|
||||||
seasonal_base: BASE_LOW_BAND, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.0,
|
||||||
typical_range_km: 320, extended_range_km: 800, exceptional_range_km: 2000,
|
humidity_effect: Beneficial,
|
||||||
weights: Some(Weights::new(0.1593, 0.0350, 0.1250, 0.1401, 0.0706, 0.1276, 0.0706, 0.0120, 0.0681, 0.1916)),
|
humidity_penalty: 0.0,
|
||||||
|
rain_k: 0.0,
|
||||||
|
rain_alpha: 1.0,
|
||||||
|
seasonal_base: BASE_LOW_BAND,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 320,
|
||||||
|
extended_range_km: 800,
|
||||||
|
exceptional_range_km: 2000,
|
||||||
|
weights: Some(Weights::new(
|
||||||
|
0.1593, 0.0350, 0.1250, 0.1401, 0.0706, 0.1276, 0.0706, 0.0120, 0.0681, 0.1916,
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 432, label: "432 MHz",
|
freq_mhz: 432,
|
||||||
o2_db_km: 0.0, h2o_coeff: 0.0, humidity_effect: Beneficial, humidity_penalty: 0.0,
|
label: "432 MHz",
|
||||||
rain_k: 0.0, rain_alpha: 1.0,
|
o2_db_km: 0.0,
|
||||||
seasonal_base: BASE_LOW_BAND, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.0,
|
||||||
typical_range_km: 300, extended_range_km: 700, exceptional_range_km: 1800,
|
humidity_effect: Beneficial,
|
||||||
weights: Some(Weights::new(0.2061, 0.0329, 0.1200, 0.1186, 0.0663, 0.1106, 0.0663, 0.0113, 0.0809, 0.1870)),
|
humidity_penalty: 0.0,
|
||||||
|
rain_k: 0.0,
|
||||||
|
rain_alpha: 1.0,
|
||||||
|
seasonal_base: BASE_LOW_BAND,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 300,
|
||||||
|
extended_range_km: 700,
|
||||||
|
exceptional_range_km: 1800,
|
||||||
|
weights: Some(Weights::new(
|
||||||
|
0.2061, 0.0329, 0.1200, 0.1186, 0.0663, 0.1106, 0.0663, 0.0113, 0.0809, 0.1870,
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 902, label: "902 MHz",
|
freq_mhz: 902,
|
||||||
o2_db_km: 0.006, h2o_coeff: 0.0, humidity_effect: Beneficial, humidity_penalty: 0.0,
|
label: "902 MHz",
|
||||||
rain_k: 0.000, rain_alpha: 1.0,
|
o2_db_km: 0.006,
|
||||||
seasonal_base: BASE_LOW_BAND, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.0,
|
||||||
typical_range_km: 400, extended_range_km: 800, exceptional_range_km: 1500,
|
humidity_effect: Beneficial,
|
||||||
weights: Some(Weights::new(0.2201, 0.0437, 0.1102, 0.0888, 0.0783, 0.1197, 0.0783, 0.0133, 0.0839, 0.1635)),
|
humidity_penalty: 0.0,
|
||||||
|
rain_k: 0.000,
|
||||||
|
rain_alpha: 1.0,
|
||||||
|
seasonal_base: BASE_LOW_BAND,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 400,
|
||||||
|
extended_range_km: 800,
|
||||||
|
exceptional_range_km: 1500,
|
||||||
|
weights: Some(Weights::new(
|
||||||
|
0.2201, 0.0437, 0.1102, 0.0888, 0.0783, 0.1197, 0.0783, 0.0133, 0.0839, 0.1635,
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 1_296, label: "1296 MHz",
|
freq_mhz: 1_296,
|
||||||
o2_db_km: 0.006, h2o_coeff: 0.0, humidity_effect: Beneficial, humidity_penalty: 0.0,
|
label: "1296 MHz",
|
||||||
rain_k: 0.000, rain_alpha: 1.0,
|
o2_db_km: 0.006,
|
||||||
seasonal_base: BASE_LOW_BAND, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.0,
|
||||||
typical_range_km: 350, extended_range_km: 700, exceptional_range_km: 1200,
|
humidity_effect: Beneficial,
|
||||||
weights: Some(Weights::new(0.2131, 0.0494, 0.0898, 0.1392, 0.0797, 0.1108, 0.0797, 0.0136, 0.0568, 0.1678)),
|
humidity_penalty: 0.0,
|
||||||
|
rain_k: 0.000,
|
||||||
|
rain_alpha: 1.0,
|
||||||
|
seasonal_base: BASE_LOW_BAND,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 350,
|
||||||
|
extended_range_km: 700,
|
||||||
|
exceptional_range_km: 1200,
|
||||||
|
weights: Some(Weights::new(
|
||||||
|
0.2131, 0.0494, 0.0898, 0.1392, 0.0797, 0.1108, 0.0797, 0.0136, 0.0568, 0.1678,
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 2_304, label: "2304 MHz",
|
freq_mhz: 2_304,
|
||||||
o2_db_km: 0.006, h2o_coeff: 0.0, humidity_effect: Beneficial, humidity_penalty: 0.0,
|
label: "2304 MHz",
|
||||||
rain_k: 0.001, rain_alpha: 1.15,
|
o2_db_km: 0.006,
|
||||||
seasonal_base: BASE_LOW_BAND, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.0,
|
||||||
typical_range_km: 300, extended_range_km: 600, exceptional_range_km: 1000,
|
humidity_effect: Beneficial,
|
||||||
weights: Some(Weights::new(0.1941, 0.0387, 0.1385, 0.1638, 0.0625, 0.0868, 0.0625, 0.0336, 0.0432, 0.1762)),
|
humidity_penalty: 0.0,
|
||||||
|
rain_k: 0.001,
|
||||||
|
rain_alpha: 1.15,
|
||||||
|
seasonal_base: BASE_LOW_BAND,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 300,
|
||||||
|
extended_range_km: 600,
|
||||||
|
exceptional_range_km: 1000,
|
||||||
|
weights: Some(Weights::new(
|
||||||
|
0.1941, 0.0387, 0.1385, 0.1638, 0.0625, 0.0868, 0.0625, 0.0336, 0.0432, 0.1762,
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 3_400, label: "3400 MHz",
|
freq_mhz: 3_400,
|
||||||
o2_db_km: 0.006, h2o_coeff: 0.0, humidity_effect: Beneficial, humidity_penalty: 0.0,
|
label: "3400 MHz",
|
||||||
rain_k: 0.002, rain_alpha: 1.20,
|
o2_db_km: 0.006,
|
||||||
seasonal_base: BASE_LOW_BAND, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.0,
|
||||||
typical_range_km: 250, extended_range_km: 550, exceptional_range_km: 900,
|
humidity_effect: Beneficial,
|
||||||
weights: Some(Weights::new(0.1996, 0.0398, 0.1251, 0.1310, 0.0642, 0.0893, 0.0642, 0.0489, 0.0568, 0.1811)),
|
humidity_penalty: 0.0,
|
||||||
|
rain_k: 0.002,
|
||||||
|
rain_alpha: 1.20,
|
||||||
|
seasonal_base: BASE_LOW_BAND,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 250,
|
||||||
|
extended_range_km: 550,
|
||||||
|
exceptional_range_km: 900,
|
||||||
|
weights: Some(Weights::new(
|
||||||
|
0.1996, 0.0398, 0.1251, 0.1310, 0.0642, 0.0893, 0.0642, 0.0489, 0.0568, 0.1811,
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 5_760, label: "5760 MHz",
|
freq_mhz: 5_760,
|
||||||
o2_db_km: 0.007, h2o_coeff: 0.0, humidity_effect: Beneficial, humidity_penalty: 0.0,
|
label: "5760 MHz",
|
||||||
rain_k: 0.005, rain_alpha: 1.25,
|
o2_db_km: 0.007,
|
||||||
seasonal_base: BASE_LOW_BAND, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.0,
|
||||||
typical_range_km: 220, extended_range_km: 500, exceptional_range_km: 1000,
|
humidity_effect: Beneficial,
|
||||||
weights: Some(Weights::new(0.1829, 0.0423, 0.1205, 0.0881, 0.0683, 0.0949, 0.0683, 0.0822, 0.0602, 0.1925)),
|
humidity_penalty: 0.0,
|
||||||
|
rain_k: 0.005,
|
||||||
|
rain_alpha: 1.25,
|
||||||
|
seasonal_base: BASE_LOW_BAND,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 220,
|
||||||
|
extended_range_km: 500,
|
||||||
|
exceptional_range_km: 1000,
|
||||||
|
weights: Some(Weights::new(
|
||||||
|
0.1829, 0.0423, 0.1205, 0.0881, 0.0683, 0.0949, 0.0683, 0.0822, 0.0602, 0.1925,
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 10_000, label: "10 GHz",
|
freq_mhz: 10_000,
|
||||||
o2_db_km: 0.007, h2o_coeff: 0.0, humidity_effect: Beneficial, humidity_penalty: 0.0,
|
label: "10 GHz",
|
||||||
rain_k: 0.010, rain_alpha: 1.28,
|
o2_db_km: 0.007,
|
||||||
seasonal_base: BASE_LOW_BAND, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.0,
|
||||||
typical_range_km: 200, extended_range_km: 500, exceptional_range_km: 1000,
|
humidity_effect: Beneficial,
|
||||||
|
humidity_penalty: 0.0,
|
||||||
|
rain_k: 0.010,
|
||||||
|
rain_alpha: 1.28,
|
||||||
|
seasonal_base: BASE_LOW_BAND,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 200,
|
||||||
|
extended_range_km: 500,
|
||||||
|
exceptional_range_km: 1000,
|
||||||
weights: None,
|
weights: None,
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 24_000, label: "24 GHz",
|
freq_mhz: 24_000,
|
||||||
o2_db_km: 0.02, h2o_coeff: 0.002, humidity_effect: Harmful, humidity_penalty: 1.6,
|
label: "24 GHz",
|
||||||
rain_k: 0.070, rain_alpha: 1.07,
|
o2_db_km: 0.02,
|
||||||
seasonal_base: BASE_24G, seasonal_adj: ADJ_24G,
|
h2o_coeff: 0.002,
|
||||||
typical_range_km: 100, extended_range_km: 250, exceptional_range_km: 500,
|
humidity_effect: Harmful,
|
||||||
weights: Some(Weights::new(0.1481, 0.0532, 0.0360, 0.1250, 0.0477, 0.0729, 0.0477, 0.2147, 0.1203, 0.1344)),
|
humidity_penalty: 1.6,
|
||||||
|
rain_k: 0.070,
|
||||||
|
rain_alpha: 1.07,
|
||||||
|
seasonal_base: BASE_24G,
|
||||||
|
seasonal_adj: ADJ_24G,
|
||||||
|
typical_range_km: 100,
|
||||||
|
extended_range_km: 250,
|
||||||
|
exceptional_range_km: 500,
|
||||||
|
weights: Some(Weights::new(
|
||||||
|
0.1481, 0.0532, 0.0360, 0.1250, 0.0477, 0.0729, 0.0477, 0.2147, 0.1203, 0.1344,
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 47_000, label: "47 GHz",
|
freq_mhz: 47_000,
|
||||||
o2_db_km: 0.04, h2o_coeff: 0.003, humidity_effect: Harmful, humidity_penalty: 1.0,
|
label: "47 GHz",
|
||||||
rain_k: 0.187, rain_alpha: 0.93,
|
o2_db_km: 0.04,
|
||||||
seasonal_base: BASE_47G, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.003,
|
||||||
typical_range_km: 70, extended_range_km: 150, exceptional_range_km: 300,
|
humidity_effect: Harmful,
|
||||||
|
humidity_penalty: 1.0,
|
||||||
|
rain_k: 0.187,
|
||||||
|
rain_alpha: 0.93,
|
||||||
|
seasonal_base: BASE_47G,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 70,
|
||||||
|
extended_range_km: 150,
|
||||||
|
exceptional_range_km: 300,
|
||||||
weights: None,
|
weights: None,
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 68_000, label: "68 GHz",
|
freq_mhz: 68_000,
|
||||||
o2_db_km: 0.90, h2o_coeff: 0.007, humidity_effect: Harmful, humidity_penalty: 1.4,
|
label: "68 GHz",
|
||||||
rain_k: 0.310, rain_alpha: 0.86,
|
o2_db_km: 0.90,
|
||||||
seasonal_base: BASE_68G, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.007,
|
||||||
typical_range_km: 40, extended_range_km: 80, exceptional_range_km: 150,
|
humidity_effect: Harmful,
|
||||||
|
humidity_penalty: 1.4,
|
||||||
|
rain_k: 0.310,
|
||||||
|
rain_alpha: 0.86,
|
||||||
|
seasonal_base: BASE_68G,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 40,
|
||||||
|
extended_range_km: 80,
|
||||||
|
exceptional_range_km: 150,
|
||||||
weights: None,
|
weights: None,
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 75_000, label: "75 GHz",
|
freq_mhz: 75_000,
|
||||||
o2_db_km: 0.012, h2o_coeff: 0.006, humidity_effect: Harmful, humidity_penalty: 1.2,
|
label: "75 GHz",
|
||||||
rain_k: 0.345, rain_alpha: 0.84,
|
o2_db_km: 0.012,
|
||||||
seasonal_base: BASE_75G, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.006,
|
||||||
typical_range_km: 50, extended_range_km: 120, exceptional_range_km: 250,
|
humidity_effect: Harmful,
|
||||||
|
humidity_penalty: 1.2,
|
||||||
|
rain_k: 0.345,
|
||||||
|
rain_alpha: 0.84,
|
||||||
|
seasonal_base: BASE_75G,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 50,
|
||||||
|
extended_range_km: 120,
|
||||||
|
exceptional_range_km: 250,
|
||||||
weights: None,
|
weights: None,
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 122_000, label: "122 GHz",
|
freq_mhz: 122_000,
|
||||||
o2_db_km: 0.80, h2o_coeff: 0.010, humidity_effect: Harmful, humidity_penalty: 1.0,
|
label: "122 GHz",
|
||||||
rain_k: 0.498, rain_alpha: 0.77,
|
o2_db_km: 0.80,
|
||||||
seasonal_base: BASE_122G, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.010,
|
||||||
typical_range_km: 30, extended_range_km: 80, exceptional_range_km: 140,
|
humidity_effect: Harmful,
|
||||||
|
humidity_penalty: 1.0,
|
||||||
|
rain_k: 0.498,
|
||||||
|
rain_alpha: 0.77,
|
||||||
|
seasonal_base: BASE_122G,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 30,
|
||||||
|
extended_range_km: 80,
|
||||||
|
exceptional_range_km: 140,
|
||||||
weights: None,
|
weights: None,
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 134_000, label: "134 GHz",
|
freq_mhz: 134_000,
|
||||||
o2_db_km: 0.08, h2o_coeff: 0.015, humidity_effect: Harmful, humidity_penalty: 1.3,
|
label: "134 GHz",
|
||||||
rain_k: 0.520, rain_alpha: 0.75,
|
o2_db_km: 0.08,
|
||||||
seasonal_base: BASE_134G, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.015,
|
||||||
typical_range_km: 40, extended_range_km: 100, exceptional_range_km: 160,
|
humidity_effect: Harmful,
|
||||||
|
humidity_penalty: 1.3,
|
||||||
|
rain_k: 0.520,
|
||||||
|
rain_alpha: 0.75,
|
||||||
|
seasonal_base: BASE_134G,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 40,
|
||||||
|
extended_range_km: 100,
|
||||||
|
exceptional_range_km: 160,
|
||||||
weights: None,
|
weights: None,
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 142_000, label: "142 GHz",
|
freq_mhz: 142_000,
|
||||||
o2_db_km: 0.05, h2o_coeff: 0.025, humidity_effect: Harmful, humidity_penalty: 1.4,
|
label: "142 GHz",
|
||||||
rain_k: 0.530, rain_alpha: 0.74,
|
o2_db_km: 0.05,
|
||||||
seasonal_base: BASE_142G, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.025,
|
||||||
typical_range_km: 35, extended_range_km: 80, exceptional_range_km: 160,
|
humidity_effect: Harmful,
|
||||||
|
humidity_penalty: 1.4,
|
||||||
|
rain_k: 0.530,
|
||||||
|
rain_alpha: 0.74,
|
||||||
|
seasonal_base: BASE_142G,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 35,
|
||||||
|
extended_range_km: 80,
|
||||||
|
exceptional_range_km: 160,
|
||||||
weights: None,
|
weights: None,
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 145_000, label: "145 GHz",
|
freq_mhz: 145_000,
|
||||||
o2_db_km: 0.06, h2o_coeff: 0.040, humidity_effect: Harmful, humidity_penalty: 1.5,
|
label: "145 GHz",
|
||||||
rain_k: 0.535, rain_alpha: 0.74,
|
o2_db_km: 0.06,
|
||||||
seasonal_base: BASE_145G, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.040,
|
||||||
typical_range_km: 35, extended_range_km: 80, exceptional_range_km: 150,
|
humidity_effect: Harmful,
|
||||||
|
humidity_penalty: 1.5,
|
||||||
|
rain_k: 0.535,
|
||||||
|
rain_alpha: 0.74,
|
||||||
|
seasonal_base: BASE_145G,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 35,
|
||||||
|
extended_range_km: 80,
|
||||||
|
exceptional_range_km: 150,
|
||||||
weights: None,
|
weights: None,
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 241_000, label: "241 GHz",
|
freq_mhz: 241_000,
|
||||||
o2_db_km: 0.08, h2o_coeff: 0.30, humidity_effect: Harmful, humidity_penalty: 3.0,
|
label: "241 GHz",
|
||||||
rain_k: 0.550, rain_alpha: 0.70,
|
o2_db_km: 0.08,
|
||||||
seasonal_base: BASE_241G, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.30,
|
||||||
typical_range_km: 10, extended_range_km: 50, exceptional_range_km: 115,
|
humidity_effect: Harmful,
|
||||||
|
humidity_penalty: 3.0,
|
||||||
|
rain_k: 0.550,
|
||||||
|
rain_alpha: 0.70,
|
||||||
|
seasonal_base: BASE_241G,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 10,
|
||||||
|
extended_range_km: 50,
|
||||||
|
exceptional_range_km: 115,
|
||||||
weights: None,
|
weights: None,
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 288_000, label: "288 GHz",
|
freq_mhz: 288_000,
|
||||||
o2_db_km: 0.10, h2o_coeff: 0.45, humidity_effect: Harmful, humidity_penalty: 3.5,
|
label: "288 GHz",
|
||||||
rain_k: 0.560, rain_alpha: 0.68,
|
o2_db_km: 0.10,
|
||||||
seasonal_base: BASE_288G, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.45,
|
||||||
typical_range_km: 5, extended_range_km: 15, exceptional_range_km: 50,
|
humidity_effect: Harmful,
|
||||||
|
humidity_penalty: 3.5,
|
||||||
|
rain_k: 0.560,
|
||||||
|
rain_alpha: 0.68,
|
||||||
|
seasonal_base: BASE_288G,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 5,
|
||||||
|
extended_range_km: 15,
|
||||||
|
exceptional_range_km: 50,
|
||||||
weights: None,
|
weights: None,
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 322_000, label: "322 GHz",
|
freq_mhz: 322_000,
|
||||||
o2_db_km: 0.12, h2o_coeff: 0.55, humidity_effect: Harmful, humidity_penalty: 4.0,
|
label: "322 GHz",
|
||||||
rain_k: 0.570, rain_alpha: 0.66,
|
o2_db_km: 0.12,
|
||||||
seasonal_base: BASE_322G, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.55,
|
||||||
typical_range_km: 5, extended_range_km: 15, exceptional_range_km: 40,
|
humidity_effect: Harmful,
|
||||||
|
humidity_penalty: 4.0,
|
||||||
|
rain_k: 0.570,
|
||||||
|
rain_alpha: 0.66,
|
||||||
|
seasonal_base: BASE_322G,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 5,
|
||||||
|
extended_range_km: 15,
|
||||||
|
exceptional_range_km: 40,
|
||||||
weights: None,
|
weights: None,
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 403_000, label: "403 GHz",
|
freq_mhz: 403_000,
|
||||||
o2_db_km: 0.15, h2o_coeff: 0.40, humidity_effect: Harmful, humidity_penalty: 3.0,
|
label: "403 GHz",
|
||||||
rain_k: 0.580, rain_alpha: 0.64,
|
o2_db_km: 0.15,
|
||||||
seasonal_base: BASE_403G, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.40,
|
||||||
typical_range_km: 3, extended_range_km: 10, exceptional_range_km: 30,
|
humidity_effect: Harmful,
|
||||||
|
humidity_penalty: 3.0,
|
||||||
|
rain_k: 0.580,
|
||||||
|
rain_alpha: 0.64,
|
||||||
|
seasonal_base: BASE_403G,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 3,
|
||||||
|
extended_range_km: 10,
|
||||||
|
exceptional_range_km: 30,
|
||||||
weights: None,
|
weights: None,
|
||||||
},
|
},
|
||||||
BandConfig {
|
BandConfig {
|
||||||
freq_mhz: 411_000, label: "411 GHz",
|
freq_mhz: 411_000,
|
||||||
o2_db_km: 0.15, h2o_coeff: 0.42, humidity_effect: Harmful, humidity_penalty: 3.2,
|
label: "411 GHz",
|
||||||
rain_k: 0.580, rain_alpha: 0.64,
|
o2_db_km: 0.15,
|
||||||
seasonal_base: BASE_403G, seasonal_adj: ADJ_NONE,
|
h2o_coeff: 0.42,
|
||||||
typical_range_km: 3, extended_range_km: 10, exceptional_range_km: 30,
|
humidity_effect: Harmful,
|
||||||
|
humidity_penalty: 3.2,
|
||||||
|
rain_k: 0.580,
|
||||||
|
rain_alpha: 0.64,
|
||||||
|
seasonal_base: BASE_403G,
|
||||||
|
seasonal_adj: ADJ_NONE,
|
||||||
|
typical_range_km: 3,
|
||||||
|
extended_range_km: 10,
|
||||||
|
exceptional_range_km: 30,
|
||||||
weights: None,
|
weights: None,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
@ -448,8 +782,16 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn default_weights_sum_to_one() {
|
fn default_weights_sum_to_one() {
|
||||||
let w = DEFAULT_WEIGHTS;
|
let w = DEFAULT_WEIGHTS;
|
||||||
let sum = w.humidity + w.time_of_day + w.td_depression + w.refractivity
|
let sum = w.humidity
|
||||||
+ w.sky + w.season + w.wind + w.rain + w.pressure + w.pwat;
|
+ w.time_of_day
|
||||||
|
+ w.td_depression
|
||||||
|
+ w.refractivity
|
||||||
|
+ w.sky
|
||||||
|
+ w.season
|
||||||
|
+ w.wind
|
||||||
|
+ w.rain
|
||||||
|
+ w.pressure
|
||||||
|
+ w.pwat;
|
||||||
// Elixir comment says "all 10 values sum to 1.0" — allow tiny FP slop.
|
// Elixir comment says "all 10 values sum to 1.0" — allow tiny FP slop.
|
||||||
assert!((sum - 1.0).abs() < 1e-9, "weights sum = {sum}");
|
assert!((sum - 1.0).abs() < 1e-9, "weights sum = {sum}");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
//! LOCKED allows N replicas with no coordination.
|
//! LOCKED allows N replicas with no coordination.
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use prop_grid_rs::{db, fetcher::HrrrClient, hrrr_points, telemetry};
|
use prop_grid_rs::{db, fetcher::HrrrClient, hrrr_points, telemetry};
|
||||||
|
|
@ -44,7 +44,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let points = task.points.clone();
|
let points = task.points.clone();
|
||||||
let started = Instant::now();
|
let started = Instant::now();
|
||||||
|
|
||||||
match hrrr_points::process_batch(&client, &pool, valid_time, &points, &tmp_dir).await {
|
match hrrr_points::process_batch(&client, &pool, valid_time, &points, &tmp_dir)
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(stats) => {
|
Ok(stats) => {
|
||||||
let elapsed = started.elapsed();
|
let elapsed = started.elapsed();
|
||||||
info!(
|
info!(
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use prop_grid_rs::{db, fetcher::HrrrClient, metrics, pipeline, telemetry};
|
use prop_grid_rs::{db, fetcher::HrrrClient, metrics, pipeline, telemetry};
|
||||||
|
|
@ -115,28 +115,26 @@ async fn worker_loop(
|
||||||
let _guard = metrics::InFlightGuard::new();
|
let _guard = metrics::InFlightGuard::new();
|
||||||
let started = Instant::now();
|
let started = Instant::now();
|
||||||
let result = match task.kind {
|
let result = match task.kind {
|
||||||
db::TaskKind::Forecast => {
|
db::TaskKind::Forecast => pipeline::run_chain_step(
|
||||||
pipeline::run_chain_step(
|
&client,
|
||||||
&client,
|
&scores_dir,
|
||||||
&scores_dir,
|
&pipeline::ChainStepInput {
|
||||||
&pipeline::ChainStepInput {
|
run_time: task.run_time,
|
||||||
run_time: task.run_time,
|
forecast_hour: task.forecast_hour as u8,
|
||||||
forecast_hour: task.forecast_hour as u8,
|
},
|
||||||
},
|
)
|
||||||
)
|
.await
|
||||||
.await
|
.map(ChainOutcome::Forecast),
|
||||||
.map(ChainOutcome::Forecast)
|
db::TaskKind::Analysis => pipeline::run_analysis_step(
|
||||||
}
|
&client,
|
||||||
db::TaskKind::Analysis => {
|
&pool,
|
||||||
pipeline::run_analysis_step(
|
&scores_dir,
|
||||||
&client,
|
&pipeline::AnalysisStepInput {
|
||||||
&pool,
|
run_time: task.run_time,
|
||||||
&scores_dir,
|
},
|
||||||
&pipeline::AnalysisStepInput { run_time: task.run_time },
|
)
|
||||||
)
|
.await
|
||||||
.await
|
.map(ChainOutcome::Analysis),
|
||||||
.map(ChainOutcome::Analysis)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
|
|
|
||||||
|
|
@ -368,7 +368,9 @@ pub async fn fail_hrrr_task(pool: &PgPool, task_id: Uuid, error: &str) -> Result
|
||||||
}
|
}
|
||||||
|
|
||||||
fn points_from_json(v: &serde_json::Value) -> Vec<(f64, f64)> {
|
fn points_from_json(v: &serde_json::Value) -> Vec<(f64, f64)> {
|
||||||
let Some(arr) = v.as_array() else { return Vec::new() };
|
let Some(arr) = v.as_array() else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
arr.iter()
|
arr.iter()
|
||||||
.filter_map(|p| {
|
.filter_map(|p| {
|
||||||
let lat = p.get("lat")?.as_f64()?;
|
let lat = p.get("lat")?.as_f64()?;
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,11 @@ fn run_wgrib2_lola(
|
||||||
grid_spec.lat_start, grid_spec.lat_count, grid_spec.lat_step
|
grid_spec.lat_start, grid_spec.lat_count, grid_spec.lat_step
|
||||||
);
|
);
|
||||||
|
|
||||||
let Output { status, stdout, stderr } = Command::new(wgrib2)
|
let Output {
|
||||||
|
status,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
} = Command::new(wgrib2)
|
||||||
.arg(grib_path)
|
.arg(grib_path)
|
||||||
.arg("-match")
|
.arg("-match")
|
||||||
.arg(match_pattern)
|
.arg(match_pattern)
|
||||||
|
|
@ -290,8 +294,14 @@ mod tests {
|
||||||
lat_step: 0.5,
|
lat_step: 0.5,
|
||||||
};
|
};
|
||||||
let msgs = vec![
|
let msgs = vec![
|
||||||
Message { var: "TMP".into(), level: "surface".into() },
|
Message {
|
||||||
Message { var: "DPT".into(), level: "surface".into() },
|
var: "TMP".into(),
|
||||||
|
level: "surface".into(),
|
||||||
|
},
|
||||||
|
Message {
|
||||||
|
var: "DPT".into(),
|
||||||
|
level: "surface".into(),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
for (msg_idx, _msg) in msgs.iter().enumerate() {
|
for (msg_idx, _msg) in msgs.iter().enumerate() {
|
||||||
|
|
@ -332,7 +342,10 @@ mod tests {
|
||||||
lat_count: 1,
|
lat_count: 1,
|
||||||
lat_step: 1.0,
|
lat_step: 1.0,
|
||||||
};
|
};
|
||||||
let msgs = vec![Message { var: "TMP".into(), level: "surface".into() }];
|
let msgs = vec![Message {
|
||||||
|
var: "TMP".into(),
|
||||||
|
level: "surface".into(),
|
||||||
|
}];
|
||||||
let mut buf = Vec::new();
|
let mut buf = Vec::new();
|
||||||
let len: u32 = 2 * 4;
|
let len: u32 = 2 * 4;
|
||||||
buf.extend_from_slice(&len.to_le_bytes());
|
buf.extend_from_slice(&len.to_le_bytes());
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,12 @@ pub fn detect_ducts(m: &[(f64, f64)]) -> Vec<Duct> {
|
||||||
if m2 < m1 {
|
if m2 < m1 {
|
||||||
// in a duct
|
// in a duct
|
||||||
current = Some(match current {
|
current = Some(match current {
|
||||||
None => RawDuct { base: h1, base_m_val: m1, top: h2, min_m_val: m2 },
|
None => RawDuct {
|
||||||
|
base: h1,
|
||||||
|
base_m_val: m1,
|
||||||
|
top: h2,
|
||||||
|
min_m_val: m2,
|
||||||
|
},
|
||||||
Some(d) => RawDuct {
|
Some(d) => RawDuct {
|
||||||
top: h2,
|
top: h2,
|
||||||
min_m_val: d.min_m_val.min(m2),
|
min_m_val: d.min_m_val.min(m2),
|
||||||
|
|
@ -146,7 +151,10 @@ pub fn analyze(profile: &NativeProfile) -> Analysis {
|
||||||
None => Some(f),
|
None => Some(f),
|
||||||
Some(a) => Some(a.min(f)),
|
Some(a) => Some(a.min(f)),
|
||||||
});
|
});
|
||||||
Analysis { ducts, best_duct_band_ghz: best }
|
Analysis {
|
||||||
|
ducts,
|
||||||
|
best_duct_band_ghz: best,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Minimum dM/dh across a profile (in M-units per km), mirroring the
|
/// Minimum dM/dh across a profile (in M-units per km), mirroring the
|
||||||
|
|
@ -169,15 +177,22 @@ pub fn min_m_gradient(profile: &NativeProfile) -> f64 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if min.is_finite() { min } else { 0.0 }
|
if min.is_finite() {
|
||||||
|
min
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maximum duct thickness across a detected set; `None` if empty.
|
/// Maximum duct thickness across a detected set; `None` if empty.
|
||||||
pub fn max_duct_thickness_m(ducts: &[Duct]) -> Option<f64> {
|
pub fn max_duct_thickness_m(ducts: &[Duct]) -> Option<f64> {
|
||||||
ducts.iter().map(|d| d.thickness_m).fold(None, |acc, t| match acc {
|
ducts
|
||||||
None => Some(t),
|
.iter()
|
||||||
Some(a) => Some(a.max(t)),
|
.map(|d| d.thickness_m)
|
||||||
})
|
.fold(None, |acc, t| match acc {
|
||||||
|
None => Some(t),
|
||||||
|
Some(a) => Some(a.max(t)),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -193,7 +208,12 @@ mod tests {
|
||||||
let pressure_pa: Vec<f64> = (0..10)
|
let pressure_pa: Vec<f64> = (0..10)
|
||||||
.map(|i| 101_325.0 * (1.0 - 0.000_022_6 * (i as f64 * 100.0)).powf(5.255))
|
.map(|i| 101_325.0 * (1.0 - 0.000_022_6 * (i as f64 * 100.0)).powf(5.255))
|
||||||
.collect();
|
.collect();
|
||||||
NativeProfile { heights_m: heights, temp_k, spfh, pressure_pa }
|
NativeProfile {
|
||||||
|
heights_m: heights,
|
||||||
|
temp_k,
|
||||||
|
spfh,
|
||||||
|
pressure_pa,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -212,8 +232,15 @@ mod tests {
|
||||||
let heights_m = vec![0.0, 50.0, 100.0, 150.0, 200.0, 400.0];
|
let heights_m = vec![0.0, 50.0, 100.0, 150.0, 200.0, 400.0];
|
||||||
let temp_k = vec![298.0, 297.5, 300.0, 302.0, 300.5, 295.0];
|
let temp_k = vec![298.0, 297.5, 300.0, 302.0, 300.5, 295.0];
|
||||||
let spfh = vec![0.018, 0.017, 0.012, 0.006, 0.003, 0.002];
|
let spfh = vec![0.018, 0.017, 0.012, 0.006, 0.003, 0.002];
|
||||||
let pressure_pa = vec![101_325.0, 100_720.0, 100_120.0, 99_520.0, 98_930.0, 96_580.0];
|
let pressure_pa = vec![
|
||||||
let p = NativeProfile { heights_m, temp_k, spfh, pressure_pa };
|
101_325.0, 100_720.0, 100_120.0, 99_520.0, 98_930.0, 96_580.0,
|
||||||
|
];
|
||||||
|
let p = NativeProfile {
|
||||||
|
heights_m,
|
||||||
|
temp_k,
|
||||||
|
spfh,
|
||||||
|
pressure_pa,
|
||||||
|
};
|
||||||
let a = analyze(&p);
|
let a = analyze(&p);
|
||||||
assert!(!a.ducts.is_empty(), "expected at least one duct");
|
assert!(!a.ducts.is_empty(), "expected at least one duct");
|
||||||
// The duct must have positive thickness and deficit.
|
// The duct must have positive thickness and deficit.
|
||||||
|
|
|
||||||
|
|
@ -60,11 +60,19 @@ pub async fn process_batch(
|
||||||
let prs_wanted = fetcher::pressure_messages_grid();
|
let prs_wanted = fetcher::pressure_messages_grid();
|
||||||
let sfc_pattern = format!(
|
let sfc_pattern = format!(
|
||||||
":({}):",
|
":({}):",
|
||||||
sfc_wanted.iter().map(|(v, _)| v.as_str()).collect::<Vec<_>>().join("|")
|
sfc_wanted
|
||||||
|
.iter()
|
||||||
|
.map(|(v, _)| v.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("|")
|
||||||
);
|
);
|
||||||
let prs_pattern = format!(
|
let prs_pattern = format!(
|
||||||
":({}):",
|
":({}):",
|
||||||
prs_wanted.iter().map(|(v, _)| v.as_str()).collect::<Vec<_>>().join("|")
|
prs_wanted
|
||||||
|
.iter()
|
||||||
|
.map(|(v, _)| v.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("|")
|
||||||
);
|
);
|
||||||
|
|
||||||
let sfc_fut = client.fetch_product_blob(date, hour, Product::Surface, 0, &sfc_wanted);
|
let sfc_fut = client.fetch_product_blob(date, hour, Product::Surface, 0, &sfc_wanted);
|
||||||
|
|
@ -99,10 +107,7 @@ pub async fn process_batch(
|
||||||
// produced nothing for its snapped key (e.g. off-CONUS).
|
// produced nothing for its snapped key (e.g. off-CONUS).
|
||||||
let mut inserted = 0u32;
|
let mut inserted = 0u32;
|
||||||
for &(lat, lon) in points {
|
for &(lat, lon) in points {
|
||||||
let key = (
|
let key = ((lat * 1000.0).round() as i32, (lon * 1000.0).round() as i32);
|
||||||
(lat * 1000.0).round() as i32,
|
|
||||||
(lon * 1000.0).round() as i32,
|
|
||||||
);
|
|
||||||
let mut cell: CellValues = sfc_grid.get(&key).cloned().unwrap_or_default();
|
let mut cell: CellValues = sfc_grid.get(&key).cloned().unwrap_or_default();
|
||||||
if let Some(prs) = prs_grid.get(&key) {
|
if let Some(prs) = prs_grid.get(&key) {
|
||||||
cell.extend(prs.iter().map(|(k, v)| (k.clone(), *v)));
|
cell.extend(prs.iter().map(|(k, v)| (k.clone(), *v)));
|
||||||
|
|
@ -136,7 +141,10 @@ async fn upsert_profile(
|
||||||
.get("DPT:2 m above ground")
|
.get("DPT:2 m above ground")
|
||||||
.copied()
|
.copied()
|
||||||
.map(|v| (v as f64) - 273.15);
|
.map(|v| (v as f64) - 273.15);
|
||||||
let surface_pressure_mb = cell.get("PRES:surface").copied().map(|v| (v as f64) / 100.0);
|
let surface_pressure_mb = cell
|
||||||
|
.get("PRES:surface")
|
||||||
|
.copied()
|
||||||
|
.map(|v| (v as f64) / 100.0);
|
||||||
let hpbl_m = cell.get("HPBL:surface").copied().map(|v| v as f64);
|
let hpbl_m = cell.get("HPBL:surface").copied().map(|v| v as f64);
|
||||||
let pwat_mm = cell
|
let pwat_mm = cell
|
||||||
.get("PWAT:entire atmosphere (considered as a single layer)")
|
.get("PWAT:entire atmosphere (considered as a single layer)")
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ pub mod nexrad;
|
||||||
pub mod pipeline;
|
pub mod pipeline;
|
||||||
pub mod profiles_file;
|
pub mod profiles_file;
|
||||||
pub mod region;
|
pub mod region;
|
||||||
pub mod scores_file;
|
|
||||||
pub mod scorer;
|
pub mod scorer;
|
||||||
|
pub mod scores_file;
|
||||||
pub mod sounding_params;
|
pub mod sounding_params;
|
||||||
pub mod telemetry;
|
pub mod telemetry;
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,7 @@
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use axum::{
|
use axum::{extract::State, http::StatusCode, response::IntoResponse, routing::get, Router};
|
||||||
extract::State,
|
|
||||||
http::StatusCode,
|
|
||||||
response::IntoResponse,
|
|
||||||
routing::get,
|
|
||||||
Router,
|
|
||||||
};
|
|
||||||
use prometheus::{
|
use prometheus::{
|
||||||
register_histogram, register_histogram_vec, register_int_counter_vec, register_int_gauge,
|
register_histogram, register_histogram_vec, register_int_counter_vec, register_int_gauge,
|
||||||
Encoder, Histogram, HistogramVec, IntCounterVec, IntGauge, TextEncoder,
|
Encoder, Histogram, HistogramVec, IntCounterVec, IntGauge, TextEncoder,
|
||||||
|
|
|
||||||
|
|
@ -109,11 +109,10 @@ pub async fn fetch_native_duct_grid(
|
||||||
// wgrib2 decode must run on the blocking pool — it forks a
|
// wgrib2 decode must run on the blocking pool — it forks a
|
||||||
// subprocess and the stdout read is synchronous IO.
|
// subprocess and the stdout read is synchronous IO.
|
||||||
let pattern = match_pattern();
|
let pattern = match_pattern();
|
||||||
let grid: PointGrid = tokio::task::spawn_blocking(move || {
|
let grid: PointGrid =
|
||||||
decoder::extract_grid(&blob, &pattern, grid_spec)
|
tokio::task::spawn_blocking(move || decoder::extract_grid(&blob, &pattern, grid_spec))
|
||||||
})
|
.await
|
||||||
.await
|
.expect("blocking join")?;
|
||||||
.expect("blocking join")?;
|
|
||||||
|
|
||||||
Ok(reduce_grid_to_ducts(grid))
|
Ok(reduce_grid_to_ducts(grid))
|
||||||
}
|
}
|
||||||
|
|
@ -150,8 +149,12 @@ pub fn build_native_profile(cell: &CellValues) -> Option<NativeProfile> {
|
||||||
let mut levels: Vec<(f64, f64, f64, f64)> = Vec::with_capacity(NATIVE_LEVEL_COUNT as usize);
|
let mut levels: Vec<(f64, f64, f64, f64)> = Vec::with_capacity(NATIVE_LEVEL_COUNT as usize);
|
||||||
for level in 1..=NATIVE_LEVEL_COUNT {
|
for level in 1..=NATIVE_LEVEL_COUNT {
|
||||||
let lvl_str = format!("{level} hybrid level");
|
let lvl_str = format!("{level} hybrid level");
|
||||||
let Some(&hgt) = cell.get(&format!("HGT:{lvl_str}")) else { continue };
|
let Some(&hgt) = cell.get(&format!("HGT:{lvl_str}")) else {
|
||||||
let Some(&tmp) = cell.get(&format!("TMP:{lvl_str}")) else { continue };
|
continue;
|
||||||
|
};
|
||||||
|
let Some(&tmp) = cell.get(&format!("TMP:{lvl_str}")) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
// SPFH / PRES default to 0.0 if absent — the duct math handles
|
// SPFH / PRES default to 0.0 if absent — the duct math handles
|
||||||
// degenerate rows (returns 0.0 gradient / no duct) without
|
// degenerate rows (returns 0.0 gradient / no duct) without
|
||||||
// raising, matching Elixir's nil-tolerance.
|
// raising, matching Elixir's nil-tolerance.
|
||||||
|
|
@ -170,7 +173,12 @@ pub fn build_native_profile(cell: &CellValues) -> Option<NativeProfile> {
|
||||||
let temp_k = levels.iter().map(|x| x.1).collect();
|
let temp_k = levels.iter().map(|x| x.1).collect();
|
||||||
let spfh = levels.iter().map(|x| x.2).collect();
|
let spfh = levels.iter().map(|x| x.2).collect();
|
||||||
let pressure_pa = levels.iter().map(|x| x.3).collect();
|
let pressure_pa = levels.iter().map(|x| x.3).collect();
|
||||||
Some(NativeProfile { heights_m, temp_k, spfh, pressure_pa })
|
Some(NativeProfile {
|
||||||
|
heights_m,
|
||||||
|
temp_k,
|
||||||
|
spfh,
|
||||||
|
pressure_pa,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merge native duct metrics into an existing surface+pressure grid.
|
/// Merge native duct metrics into an existing surface+pressure grid.
|
||||||
|
|
@ -186,7 +194,10 @@ pub fn merge_duct_grid(
|
||||||
) -> HashMap<(i32, i32), CellValues> {
|
) -> HashMap<(i32, i32), CellValues> {
|
||||||
for (key, metrics) in ducts {
|
for (key, metrics) in ducts {
|
||||||
if let Some(cell) = base.get_mut(key) {
|
if let Some(cell) = base.get_mut(key) {
|
||||||
cell.insert("native_min_gradient".into(), metrics.native_min_gradient as f32);
|
cell.insert(
|
||||||
|
"native_min_gradient".into(),
|
||||||
|
metrics.native_min_gradient as f32,
|
||||||
|
);
|
||||||
if let Some(f) = metrics.best_duct_freq_ghz {
|
if let Some(f) = metrics.best_duct_freq_ghz {
|
||||||
cell.insert("best_duct_freq_ghz".into(), f as f32);
|
cell.insert("best_duct_freq_ghz".into(), f as f32);
|
||||||
}
|
}
|
||||||
|
|
@ -246,8 +257,14 @@ mod tests {
|
||||||
for i in 1..=count {
|
for i in 1..=count {
|
||||||
c.insert(format!("HGT:{i} hybrid level"), (i as f32) * 50.0);
|
c.insert(format!("HGT:{i} hybrid level"), (i as f32) * 50.0);
|
||||||
c.insert(format!("TMP:{i} hybrid level"), 290.0 - (i as f32) * 0.5);
|
c.insert(format!("TMP:{i} hybrid level"), 290.0 - (i as f32) * 0.5);
|
||||||
c.insert(format!("SPFH:{i} hybrid level"), 0.008 - (i as f32) * 0.0001);
|
c.insert(
|
||||||
c.insert(format!("PRES:{i} hybrid level"), 101_000.0 - (i as f32) * 600.0);
|
format!("SPFH:{i} hybrid level"),
|
||||||
|
0.008 - (i as f32) * 0.0001,
|
||||||
|
);
|
||||||
|
c.insert(
|
||||||
|
format!("PRES:{i} hybrid level"),
|
||||||
|
101_000.0 - (i as f32) * 600.0,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
c
|
c
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,11 @@ pub async fn fetch_frame(
|
||||||
.map(|&(lat, lon)| {
|
.map(|&(lat, lon)| {
|
||||||
let (cx, cy) = latlon_to_pixel(lat, lon);
|
let (cx, cy) = latlon_to_pixel(lat, lon);
|
||||||
let dbz = max_dbz_in_box(&pixels, width, cx, cy, DEFAULT_BOX_HALF);
|
let dbz = max_dbz_in_box(&pixels, width, cx, cy, DEFAULT_BOX_HALF);
|
||||||
NexradObservation { lat, lon, max_reflectivity_dbz: dbz }
|
NexradObservation {
|
||||||
|
lat,
|
||||||
|
lon,
|
||||||
|
max_reflectivity_dbz: dbz,
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Ok(obs)
|
Ok(obs)
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,8 @@ use crate::grid::wgrib2_grid_spec;
|
||||||
use crate::native_duct;
|
use crate::native_duct;
|
||||||
use crate::nexrad::{self, NexradObservation};
|
use crate::nexrad::{self, NexradObservation};
|
||||||
use crate::profiles_file::{self, CellEntry};
|
use crate::profiles_file::{self, CellEntry};
|
||||||
use crate::scores_file::{self, ScorePoint};
|
|
||||||
use crate::scorer::{self, Conditions};
|
use crate::scorer::{self, Conditions};
|
||||||
|
use crate::scores_file::{self, ScorePoint};
|
||||||
use crate::sounding_params::{self, Level};
|
use crate::sounding_params::{self, Level};
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
|
@ -165,7 +165,11 @@ pub async fn run_chain_step(
|
||||||
let mut scores: Vec<ScorePoint> = Vec::with_capacity(prepared.len());
|
let mut scores: Vec<ScorePoint> = Vec::with_capacity(prepared.len());
|
||||||
for (lat, lon, conditions, invariants) in prepared.iter() {
|
for (lat, lon, conditions, invariants) in prepared.iter() {
|
||||||
let r = scorer::composite_score_with(conditions, band, Some(*invariants));
|
let r = scorer::composite_score_with(conditions, band, Some(*invariants));
|
||||||
scores.push(ScorePoint { lat: *lat, lon: *lon, score: r.score });
|
scores.push(ScorePoint {
|
||||||
|
lat: *lat,
|
||||||
|
lon: *lon,
|
||||||
|
score: r.score,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
(band.freq_mhz, scores)
|
(band.freq_mhz, scores)
|
||||||
})
|
})
|
||||||
|
|
@ -178,7 +182,9 @@ pub async fn run_chain_step(
|
||||||
scores_file::write_atomic(&dir, band_mhz, valid_time, &scores)
|
scores_file::write_atomic(&dir, band_mhz, valid_time, &scores)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
let results = futures::future::try_join_all(write_futs).await.expect("blocking join");
|
let results = futures::future::try_join_all(write_futs)
|
||||||
|
.await
|
||||||
|
.expect("blocking join");
|
||||||
for r in results {
|
for r in results {
|
||||||
r?;
|
r?;
|
||||||
}
|
}
|
||||||
|
|
@ -229,7 +235,10 @@ fn cell_to_conditions(
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let sky_cover_pct = cell.get("TCDC:entire atmosphere").copied().map(|v| v as f64);
|
let sky_cover_pct = cell
|
||||||
|
.get("TCDC:entire atmosphere")
|
||||||
|
.copied()
|
||||||
|
.map(|v| v as f64);
|
||||||
let pwat_mm = cell
|
let pwat_mm = cell
|
||||||
.get("PWAT:entire atmosphere (considered as a single layer)")
|
.get("PWAT:entire atmosphere (considered as a single layer)")
|
||||||
.copied()
|
.copied()
|
||||||
|
|
@ -309,7 +318,10 @@ mod tests {
|
||||||
cell.insert("TCDC:entire atmosphere".into(), 30.0);
|
cell.insert("TCDC:entire atmosphere".into(), 30.0);
|
||||||
cell.insert("PRES:surface".into(), 101_000.0);
|
cell.insert("PRES:surface".into(), 101_000.0);
|
||||||
cell.insert("HPBL:surface".into(), 400.0);
|
cell.insert("HPBL:surface".into(), 400.0);
|
||||||
cell.insert("PWAT:entire atmosphere (considered as a single layer)".into(), 25.0);
|
cell.insert(
|
||||||
|
"PWAT:entire atmosphere (considered as a single layer)".into(),
|
||||||
|
25.0,
|
||||||
|
);
|
||||||
|
|
||||||
let vt = Utc.with_ymd_and_hms(2026, 6, 15, 18, 0, 0).unwrap();
|
let vt = Utc.with_ymd_and_hms(2026, 6, 15, 18, 0, 0).unwrap();
|
||||||
let c = cell_to_conditions(&cell, 32.0, -97.0, &vt).unwrap();
|
let c = cell_to_conditions(&cell, 32.0, -97.0, &vt).unwrap();
|
||||||
|
|
@ -342,7 +354,9 @@ mod tests {
|
||||||
forecast_hour: 0,
|
forecast_hour: 0,
|
||||||
};
|
};
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
let err = rt.block_on(run_chain_step(&client, dir.path(), &step)).unwrap_err();
|
let err = rt
|
||||||
|
.block_on(run_chain_step(&client, dir.path(), &step))
|
||||||
|
.unwrap_err();
|
||||||
assert!(matches!(err, PipelineError::F00Reserved));
|
assert!(matches!(err, PipelineError::F00Reserved));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -450,18 +464,21 @@ pub async fn run_analysis_step(
|
||||||
.user_agent("prop-grid-rs/0.1")
|
.user_agent("prop-grid-rs/0.1")
|
||||||
.build()
|
.build()
|
||||||
.expect("reqwest client");
|
.expect("reqwest client");
|
||||||
let nexrad_points: Vec<(f64, f64)> = merged
|
let nexrad_points: Vec<(f64, f64)> =
|
||||||
.keys()
|
merged.keys().map(|&k| decoder::key_to_latlon(k)).collect();
|
||||||
.map(|&k| decoder::key_to_latlon(k))
|
let nexrad_obs: Vec<NexradObservation> = match nexrad::fetch_frame(
|
||||||
.collect();
|
&nexrad_http,
|
||||||
let nexrad_obs: Vec<NexradObservation> =
|
valid_time,
|
||||||
match nexrad::fetch_frame(&nexrad_http, valid_time, &nexrad_points).await {
|
&nexrad_points,
|
||||||
Ok(obs) => obs,
|
)
|
||||||
Err(e) => {
|
.await
|
||||||
tracing::warn!(error = %e, "NEXRAD fetch failed — continuing without radar overlay");
|
{
|
||||||
Vec::new()
|
Ok(obs) => obs,
|
||||||
}
|
Err(e) => {
|
||||||
};
|
tracing::warn!(error = %e, "NEXRAD fetch failed — continuing without radar overlay");
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
};
|
||||||
let nexrad_cells_with_echo = apply_nexrad(&mut merged, &nexrad_obs);
|
let nexrad_cells_with_echo = apply_nexrad(&mut merged, &nexrad_obs);
|
||||||
|
|
||||||
// Commercial-link degradation — precompute once, then fold into
|
// Commercial-link degradation — precompute once, then fold into
|
||||||
|
|
@ -470,8 +487,7 @@ pub async fn run_analysis_step(
|
||||||
let commercial_lookup: Vec<LinkLookupEntry> = commercial::build_link_lookup(pool, valid_time)
|
let commercial_lookup: Vec<LinkLookupEntry> = commercial::build_link_lookup(pool, valid_time)
|
||||||
.await
|
.await
|
||||||
.map_err(PipelineError::Commercial)?;
|
.map_err(PipelineError::Commercial)?;
|
||||||
let commercial_cells_boosted =
|
let commercial_cells_boosted = apply_commercial(&mut merged, &commercial_lookup);
|
||||||
apply_commercial(&mut merged, &commercial_lookup);
|
|
||||||
|
|
||||||
let point_count = merged.len() as u32;
|
let point_count = merged.len() as u32;
|
||||||
|
|
||||||
|
|
@ -480,7 +496,8 @@ pub async fn run_analysis_step(
|
||||||
// atmospheric + duct + NEXRAD + commercial fields under
|
// atmospheric + duct + NEXRAD + commercial fields under
|
||||||
// string-keyed msgpack; Rust doesn't need to mutate those further.
|
// string-keyed msgpack; Rust doesn't need to mutate those further.
|
||||||
let mut profile_entries: Vec<CellEntry> = Vec::with_capacity(merged.len());
|
let mut profile_entries: Vec<CellEntry> = Vec::with_capacity(merged.len());
|
||||||
let mut prepared: Vec<(f64, f64, Conditions, scorer::BandInvariants)> = Vec::with_capacity(merged.len());
|
let mut prepared: Vec<(f64, f64, Conditions, scorer::BandInvariants)> =
|
||||||
|
Vec::with_capacity(merged.len());
|
||||||
for (key, cell) in merged.iter() {
|
for (key, cell) in merged.iter() {
|
||||||
let (lat, lon) = decoder::key_to_latlon(*key);
|
let (lat, lon) = decoder::key_to_latlon(*key);
|
||||||
if let Some(conditions) = cell_to_conditions(cell, lat, lon, &valid_time) {
|
if let Some(conditions) = cell_to_conditions(cell, lat, lon, &valid_time) {
|
||||||
|
|
@ -515,7 +532,11 @@ pub async fn run_analysis_step(
|
||||||
let mut scores: Vec<ScorePoint> = Vec::with_capacity(prepared_arc.len());
|
let mut scores: Vec<ScorePoint> = Vec::with_capacity(prepared_arc.len());
|
||||||
for (lat, lon, conditions, invariants) in prepared_arc.iter() {
|
for (lat, lon, conditions, invariants) in prepared_arc.iter() {
|
||||||
let r = scorer::composite_score_with(conditions, band, Some(*invariants));
|
let r = scorer::composite_score_with(conditions, band, Some(*invariants));
|
||||||
scores.push(ScorePoint { lat: *lat, lon: *lon, score: r.score });
|
scores.push(ScorePoint {
|
||||||
|
lat: *lat,
|
||||||
|
lon: *lon,
|
||||||
|
score: r.score,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
(band.freq_mhz, scores)
|
(band.freq_mhz, scores)
|
||||||
})
|
})
|
||||||
|
|
@ -528,7 +549,9 @@ pub async fn run_analysis_step(
|
||||||
scores_file::write_atomic(&dir, band_mhz, valid_time, &scores)
|
scores_file::write_atomic(&dir, band_mhz, valid_time, &scores)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
let results = futures::future::try_join_all(write_futs).await.expect("blocking join");
|
let results = futures::future::try_join_all(write_futs)
|
||||||
|
.await
|
||||||
|
.expect("blocking join");
|
||||||
for r in results {
|
for r in results {
|
||||||
r?;
|
r?;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,8 @@ use std::io::{BufWriter, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use flate2::Compression;
|
|
||||||
use flate2::write::GzEncoder;
|
use flate2::write::GzEncoder;
|
||||||
|
use flate2::Compression;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
/// Bump whenever the on-disk layout changes in a way that isn't a
|
/// Bump whenever the on-disk layout changes in a way that isn't a
|
||||||
|
|
@ -131,7 +131,10 @@ pub fn write_atomic(
|
||||||
/// Round a lat/lon to 3 decimal places — matches Elixir's
|
/// Round a lat/lon to 3 decimal places — matches Elixir's
|
||||||
/// `Float.round(_, 3)` snap before keying into the profiles map.
|
/// `Float.round(_, 3)` snap before keying into the profiles map.
|
||||||
pub fn snap_coords(lat: f64, lon: f64) -> (f64, f64) {
|
pub fn snap_coords(lat: f64, lon: f64) -> (f64, f64) {
|
||||||
((lat * 1000.0).round() / 1000.0, (lon * 1000.0).round() / 1000.0)
|
(
|
||||||
|
(lat * 1000.0).round() / 1000.0,
|
||||||
|
(lon * 1000.0).round() / 1000.0,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience helper: build a `rmpv::Value` map from an
|
/// Convenience helper: build a `rmpv::Value` map from an
|
||||||
|
|
@ -176,11 +179,18 @@ mod tests {
|
||||||
gz.read_to_end(&mut buf).unwrap();
|
gz.read_to_end(&mut buf).unwrap();
|
||||||
let decoded: rmpv::Value = rmp_serde::from_slice(&buf).unwrap();
|
let decoded: rmpv::Value = rmp_serde::from_slice(&buf).unwrap();
|
||||||
let map = decoded.as_map().unwrap();
|
let map = decoded.as_map().unwrap();
|
||||||
assert!(map.iter().any(|(k, v)| k.as_str() == Some("v") && v.as_u64() == Some(1)));
|
|
||||||
assert!(map
|
assert!(map
|
||||||
.iter()
|
.iter()
|
||||||
.any(|(k, v)| k.as_str() == Some("valid_time") && v.as_str() == Some("2026-04-19T14:00:00Z")));
|
.any(|(k, v)| k.as_str() == Some("v") && v.as_u64() == Some(1)));
|
||||||
let cells_val = &map.iter().find(|(k, _)| k.as_str() == Some("cells")).unwrap().1;
|
assert!(map
|
||||||
|
.iter()
|
||||||
|
.any(|(k, v)| k.as_str() == Some("valid_time")
|
||||||
|
&& v.as_str() == Some("2026-04-19T14:00:00Z")));
|
||||||
|
let cells_val = &map
|
||||||
|
.iter()
|
||||||
|
.find(|(k, _)| k.as_str() == Some("cells"))
|
||||||
|
.unwrap()
|
||||||
|
.1;
|
||||||
let cells_arr = cells_val.as_array().unwrap();
|
let cells_arr = cells_val.as_array().unwrap();
|
||||||
assert_eq!(cells_arr.len(), 1);
|
assert_eq!(cells_arr.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
@ -190,11 +200,13 @@ mod tests {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let vt = Utc.with_ymd_and_hms(2026, 4, 19, 14, 0, 0).unwrap();
|
let vt = Utc.with_ymd_and_hms(2026, 4, 19, 14, 0, 0).unwrap();
|
||||||
let first = vec![CellEntry {
|
let first = vec![CellEntry {
|
||||||
lat: 32.0, lon: -97.0,
|
lat: 32.0,
|
||||||
|
lon: -97.0,
|
||||||
profile: value_map([("v", rmpv::Value::Integer(1.into()))]),
|
profile: value_map([("v", rmpv::Value::Integer(1.into()))]),
|
||||||
}];
|
}];
|
||||||
let second = vec![CellEntry {
|
let second = vec![CellEntry {
|
||||||
lat: 32.0, lon: -97.0,
|
lat: 32.0,
|
||||||
|
lon: -97.0,
|
||||||
profile: value_map([("v", rmpv::Value::Integer(2.into()))]),
|
profile: value_map([("v", rmpv::Value::Integer(2.into()))]),
|
||||||
}];
|
}];
|
||||||
let p1 = write_atomic(dir.path(), vt, &first).unwrap();
|
let p1 = write_atomic(dir.path(), vt, &first).unwrap();
|
||||||
|
|
@ -212,7 +224,10 @@ mod tests {
|
||||||
fn path_for_layout() {
|
fn path_for_layout() {
|
||||||
let vt = Utc.with_ymd_and_hms(2026, 4, 19, 14, 0, 0).unwrap();
|
let vt = Utc.with_ymd_and_hms(2026, 4, 19, 14, 0, 0).unwrap();
|
||||||
let p = path_for(std::path::Path::new("/data/scores"), vt);
|
let p = path_for(std::path::Path::new("/data/scores"), vt);
|
||||||
assert_eq!(p.to_str().unwrap(), "/data/scores/profiles/2026-04-19T14:00:00Z.mp.gz");
|
assert_eq!(
|
||||||
|
p.to_str().unwrap(),
|
||||||
|
"/data/scores/profiles/2026-04-19T14:00:00Z.mp.gz"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,8 @@
|
||||||
//! from-zero, so no custom rounding helper is needed.
|
//! from-zero, so no custom rounding helper is needed.
|
||||||
|
|
||||||
use crate::band_config::{
|
use crate::band_config::{
|
||||||
BandConfig, HumidityEffect, Weights, DEFAULT_WEIGHTS,
|
BandConfig, HumidityEffect, Weights, DEFAULT_WEIGHTS, HUMIDITY_BENEFICIAL_DEFAULT,
|
||||||
HUMIDITY_BENEFICIAL_DEFAULT, HUMIDITY_BENEFICIAL_THRESHOLDS,
|
HUMIDITY_BENEFICIAL_THRESHOLDS, REFRACTIVITY_DEFAULT, REFRACTIVITY_THRESHOLDS, SUNRISE_TABLE,
|
||||||
REFRACTIVITY_DEFAULT, REFRACTIVITY_THRESHOLDS, SUNRISE_TABLE,
|
|
||||||
};
|
};
|
||||||
use crate::region;
|
use crate::region;
|
||||||
|
|
||||||
|
|
@ -243,7 +242,12 @@ pub fn score_refractivity(
|
||||||
};
|
};
|
||||||
|
|
||||||
let with_hpbl = apply_hpbl_multiplier(base, bl_depth_m);
|
let with_hpbl = apply_hpbl_multiplier(base, bl_depth_m);
|
||||||
apply_native_duct_boost(with_hpbl, best_duct_band_ghz, bulk_richardson, band.freq_mhz)
|
apply_native_duct_boost(
|
||||||
|
with_hpbl,
|
||||||
|
best_duct_band_ghz,
|
||||||
|
bulk_richardson,
|
||||||
|
band.freq_mhz,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_hpbl_multiplier(base: i32, hpbl: Option<f64>) -> i32 {
|
fn apply_hpbl_multiplier(base: i32, hpbl: Option<f64>) -> i32 {
|
||||||
|
|
@ -453,7 +457,12 @@ pub fn composite_score_with(
|
||||||
band,
|
band,
|
||||||
),
|
),
|
||||||
sky: inv.sky,
|
sky: inv.sky,
|
||||||
season: score_season(c.month, c.latitude, Some(c.longitude).filter(|_| c.latitude.is_some()), band),
|
season: score_season(
|
||||||
|
c.month,
|
||||||
|
c.latitude,
|
||||||
|
Some(c.longitude).filter(|_| c.latitude.is_some()),
|
||||||
|
band,
|
||||||
|
),
|
||||||
wind: inv.wind,
|
wind: inv.wind,
|
||||||
rain: score_rain(c.rain_rate_mmhr, band),
|
rain: score_rain(c.rain_rate_mmhr, band),
|
||||||
pwat: score_pwat(c.pwat_mm, band),
|
pwat: score_pwat(c.pwat_mm, band),
|
||||||
|
|
@ -647,27 +656,42 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn refractivity_strong_gradient_beneficial() {
|
fn refractivity_strong_gradient_beneficial() {
|
||||||
assert_eq!(score_refractivity(Some(-600.0), BASELINE_BL, None, None, b10g()), 98);
|
assert_eq!(
|
||||||
|
score_refractivity(Some(-600.0), BASELINE_BL, None, None, b10g()),
|
||||||
|
98
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn refractivity_strong_gradient_harmful() {
|
fn refractivity_strong_gradient_harmful() {
|
||||||
assert_eq!(score_refractivity(Some(-250.0), BASELINE_BL, None, None, b24g()), 85);
|
assert_eq!(
|
||||||
|
score_refractivity(Some(-250.0), BASELINE_BL, None, None, b24g()),
|
||||||
|
85
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn refractivity_moderate_gradient_beneficial() {
|
fn refractivity_moderate_gradient_beneficial() {
|
||||||
assert_eq!(score_refractivity(Some(-160.0), BASELINE_BL, None, None, b10g()), 92);
|
assert_eq!(
|
||||||
|
score_refractivity(Some(-160.0), BASELINE_BL, None, None, b10g()),
|
||||||
|
92
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn refractivity_nil_gradient_returns_50() {
|
fn refractivity_nil_gradient_returns_50() {
|
||||||
assert_eq!(score_refractivity(None, BASELINE_BL, None, None, b10g()), 50);
|
assert_eq!(
|
||||||
|
score_refractivity(None, BASELINE_BL, None, None, b10g()),
|
||||||
|
50
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn refractivity_nil_bl_no_multiplier() {
|
fn refractivity_nil_bl_no_multiplier() {
|
||||||
assert_eq!(score_refractivity(Some(-160.0), None, None, None, b10g()), 92);
|
assert_eq!(
|
||||||
|
score_refractivity(Some(-160.0), None, None, None, b10g()),
|
||||||
|
92
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -723,8 +747,7 @@ mod tests {
|
||||||
score_refractivity(Some(-80.0), Some(800.0), Some(15.0), None, b10g());
|
score_refractivity(Some(-80.0), Some(800.0), Some(15.0), None, b10g());
|
||||||
assert_eq!(nil_r, four_arity_equiv);
|
assert_eq!(nil_r, four_arity_equiv);
|
||||||
|
|
||||||
let boundary =
|
let boundary = score_refractivity(Some(-80.0), Some(800.0), Some(15.0), Some(25.0), b10g());
|
||||||
score_refractivity(Some(-80.0), Some(800.0), Some(15.0), Some(25.0), b10g());
|
|
||||||
assert_eq!(boundary, plain);
|
assert_eq!(boundary, plain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -902,8 +925,16 @@ mod tests {
|
||||||
let r = composite_score(&canonical_conditions(), b10g());
|
let r = composite_score(&canonical_conditions(), b10g());
|
||||||
let f = &r.factors;
|
let f = &r.factors;
|
||||||
for s in [
|
for s in [
|
||||||
f.humidity, f.time_of_day, f.td_depression, f.refractivity,
|
f.humidity,
|
||||||
f.sky, f.season, f.wind, f.rain, f.pwat, f.pressure,
|
f.time_of_day,
|
||||||
|
f.td_depression,
|
||||||
|
f.refractivity,
|
||||||
|
f.sky,
|
||||||
|
f.season,
|
||||||
|
f.wind,
|
||||||
|
f.rain,
|
||||||
|
f.pwat,
|
||||||
|
f.pressure,
|
||||||
] {
|
] {
|
||||||
assert!((0..=100).contains(&s), "factor out of range: {s}");
|
assert!((0..=100).contains(&s), "factor out of range: {s}");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,9 +35,7 @@ static UNIQUE: AtomicU64 = AtomicU64::new(1);
|
||||||
|
|
||||||
/// Absolute path `<base>/<band_mhz>/<iso>.ntms`.
|
/// Absolute path `<base>/<band_mhz>/<iso>.ntms`.
|
||||||
pub fn path_for(base_dir: &Path, band_mhz: u32, valid_time: DateTime<Utc>) -> PathBuf {
|
pub fn path_for(base_dir: &Path, band_mhz: u32, valid_time: DateTime<Utc>) -> PathBuf {
|
||||||
let iso = valid_time
|
let iso = valid_time.format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
||||||
.format("%Y-%m-%dT%H:%M:%SZ")
|
|
||||||
.to_string();
|
|
||||||
base_dir
|
base_dir
|
||||||
.join(band_mhz.to_string())
|
.join(band_mhz.to_string())
|
||||||
.join(format!("{iso}.ntms"))
|
.join(format!("{iso}.ntms"))
|
||||||
|
|
@ -87,8 +85,7 @@ pub fn encode(
|
||||||
body[(row as usize) * col_count + (col as usize)] = clamp_score(s.score);
|
body[(row as usize) * col_count + (col as usize)] = clamp_score(s.score);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut out =
|
let mut out = Vec::with_capacity(4 + 1 + 4 + 8 + 4 + 4 + 4 + 2 + 2 + body.len());
|
||||||
Vec::with_capacity(4 + 1 + 4 + 8 + 4 + 4 + 4 + 2 + 2 + body.len());
|
|
||||||
out.extend_from_slice(MAGIC);
|
out.extend_from_slice(MAGIC);
|
||||||
out.push(VERSION);
|
out.push(VERSION);
|
||||||
out.extend_from_slice(&band_mhz.to_le_bytes());
|
out.extend_from_slice(&band_mhz.to_le_bytes());
|
||||||
|
|
@ -246,8 +243,16 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn clamps_out_of_range_scores() {
|
fn clamps_out_of_range_scores() {
|
||||||
let scores = vec![
|
let scores = vec![
|
||||||
ScorePoint { lat: 25.0, lon: -125.0, score: -5 },
|
ScorePoint {
|
||||||
ScorePoint { lat: 25.125, lon: -125.0, score: 150 },
|
lat: 25.0,
|
||||||
|
lon: -125.0,
|
||||||
|
score: -5,
|
||||||
|
},
|
||||||
|
ScorePoint {
|
||||||
|
lat: 25.125,
|
||||||
|
lon: -125.0,
|
||||||
|
score: 150,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
let ts = Utc.with_ymd_and_hms(2026, 4, 19, 15, 0, 0).unwrap();
|
let ts = Utc.with_ymd_and_hms(2026, 4, 19, 15, 0, 0).unwrap();
|
||||||
let bytes = encode(10_000, ts, &scores).unwrap();
|
let bytes = encode(10_000, ts, &scores).unwrap();
|
||||||
|
|
@ -271,7 +276,11 @@ mod tests {
|
||||||
fn atomic_write_and_decode() {
|
fn atomic_write_and_decode() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let ts = Utc.with_ymd_and_hms(2026, 4, 19, 15, 0, 0).unwrap();
|
let ts = Utc.with_ymd_and_hms(2026, 4, 19, 15, 0, 0).unwrap();
|
||||||
let scores = vec![ScorePoint { lat: 32.0, lon: -97.0, score: 73 }];
|
let scores = vec![ScorePoint {
|
||||||
|
lat: 32.0,
|
||||||
|
lon: -97.0,
|
||||||
|
score: 73,
|
||||||
|
}];
|
||||||
|
|
||||||
write_atomic(dir.path(), 10_000, ts, &scores).unwrap();
|
write_atomic(dir.path(), 10_000, ts, &scores).unwrap();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,10 +91,30 @@ mod tests {
|
||||||
// Synthetic profile: refractivity decreases with altitude → negative
|
// Synthetic profile: refractivity decreases with altitude → negative
|
||||||
// dN/dh. Realistic numbers from a standard atmosphere.
|
// dN/dh. Realistic numbers from a standard atmosphere.
|
||||||
let levels = vec![
|
let levels = vec![
|
||||||
Level { pres_mb: 1000.0, hght_m: 100.0, tmpc: 25.0, dwpc: Some(20.0) },
|
Level {
|
||||||
Level { pres_mb: 925.0, hght_m: 800.0, tmpc: 20.0, dwpc: Some(14.0) },
|
pres_mb: 1000.0,
|
||||||
Level { pres_mb: 850.0, hght_m: 1500.0, tmpc: 15.0, dwpc: Some(8.0) },
|
hght_m: 100.0,
|
||||||
Level { pres_mb: 700.0, hght_m: 3000.0, tmpc: 5.0, dwpc: Some(-5.0) },
|
tmpc: 25.0,
|
||||||
|
dwpc: Some(20.0),
|
||||||
|
},
|
||||||
|
Level {
|
||||||
|
pres_mb: 925.0,
|
||||||
|
hght_m: 800.0,
|
||||||
|
tmpc: 20.0,
|
||||||
|
dwpc: Some(14.0),
|
||||||
|
},
|
||||||
|
Level {
|
||||||
|
pres_mb: 850.0,
|
||||||
|
hght_m: 1500.0,
|
||||||
|
tmpc: 15.0,
|
||||||
|
dwpc: Some(8.0),
|
||||||
|
},
|
||||||
|
Level {
|
||||||
|
pres_mb: 700.0,
|
||||||
|
hght_m: 3000.0,
|
||||||
|
tmpc: 5.0,
|
||||||
|
dwpc: Some(-5.0),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
let g = min_refractivity_gradient(levels).unwrap();
|
let g = min_refractivity_gradient(levels).unwrap();
|
||||||
assert!(g < 0.0, "gradient should be negative: {g}");
|
assert!(g < 0.0, "gradient should be negative: {g}");
|
||||||
|
|
@ -106,20 +126,58 @@ mod tests {
|
||||||
// Surface inversion: cold moist near the ground, warm dry above —
|
// Surface inversion: cold moist near the ground, warm dry above —
|
||||||
// strong negative refractivity gradient.
|
// strong negative refractivity gradient.
|
||||||
let inversion = vec![
|
let inversion = vec![
|
||||||
Level { pres_mb: 1000.0, hght_m: 100.0, tmpc: 18.0, dwpc: Some(17.0) },
|
Level {
|
||||||
Level { pres_mb: 990.0, hght_m: 300.0, tmpc: 22.0, dwpc: Some(5.0) },
|
pres_mb: 1000.0,
|
||||||
Level { pres_mb: 950.0, hght_m: 700.0, tmpc: 20.0, dwpc: Some(3.0) },
|
hght_m: 100.0,
|
||||||
Level { pres_mb: 850.0, hght_m: 1500.0, tmpc: 15.0, dwpc: Some(0.0) },
|
tmpc: 18.0,
|
||||||
|
dwpc: Some(17.0),
|
||||||
|
},
|
||||||
|
Level {
|
||||||
|
pres_mb: 990.0,
|
||||||
|
hght_m: 300.0,
|
||||||
|
tmpc: 22.0,
|
||||||
|
dwpc: Some(5.0),
|
||||||
|
},
|
||||||
|
Level {
|
||||||
|
pres_mb: 950.0,
|
||||||
|
hght_m: 700.0,
|
||||||
|
tmpc: 20.0,
|
||||||
|
dwpc: Some(3.0),
|
||||||
|
},
|
||||||
|
Level {
|
||||||
|
pres_mb: 850.0,
|
||||||
|
hght_m: 1500.0,
|
||||||
|
tmpc: 15.0,
|
||||||
|
dwpc: Some(0.0),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
let strong = min_refractivity_gradient(inversion).unwrap();
|
let strong = min_refractivity_gradient(inversion).unwrap();
|
||||||
|
|
||||||
let neutral = vec![
|
let neutral = vec![
|
||||||
Level { pres_mb: 1000.0, hght_m: 100.0, tmpc: 25.0, dwpc: Some(18.0) },
|
Level {
|
||||||
Level { pres_mb: 925.0, hght_m: 800.0, tmpc: 20.0, dwpc: Some(14.0) },
|
pres_mb: 1000.0,
|
||||||
Level { pres_mb: 850.0, hght_m: 1500.0, tmpc: 15.0, dwpc: Some(8.0) },
|
hght_m: 100.0,
|
||||||
|
tmpc: 25.0,
|
||||||
|
dwpc: Some(18.0),
|
||||||
|
},
|
||||||
|
Level {
|
||||||
|
pres_mb: 925.0,
|
||||||
|
hght_m: 800.0,
|
||||||
|
tmpc: 20.0,
|
||||||
|
dwpc: Some(14.0),
|
||||||
|
},
|
||||||
|
Level {
|
||||||
|
pres_mb: 850.0,
|
||||||
|
hght_m: 1500.0,
|
||||||
|
tmpc: 15.0,
|
||||||
|
dwpc: Some(8.0),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
let baseline = min_refractivity_gradient(neutral).unwrap();
|
let baseline = min_refractivity_gradient(neutral).unwrap();
|
||||||
|
|
||||||
assert!(strong < baseline, "inversion {strong} vs baseline {baseline}");
|
assert!(
|
||||||
|
strong < baseline,
|
||||||
|
"inversion {strong} vs baseline {baseline}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,10 @@
|
||||||
//! (`service.name`, `service.namespace`) so Tempo groups spans per
|
//! (`service.name`, `service.namespace`) so Tempo groups spans per
|
||||||
//! binary. `service.namespace=prop` matches the k8s namespace.
|
//! binary. `service.namespace=prop` matches the k8s namespace.
|
||||||
|
|
||||||
use opentelemetry::{KeyValue, global, trace::TracerProvider as _};
|
use opentelemetry::{global, trace::TracerProvider as _, KeyValue};
|
||||||
use opentelemetry_otlp::WithExportConfig;
|
use opentelemetry_otlp::WithExportConfig;
|
||||||
use opentelemetry_sdk::{Resource, trace::SdkTracerProvider};
|
use opentelemetry_sdk::{trace::SdkTracerProvider, Resource};
|
||||||
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||||
|
|
||||||
/// Opaque handle the caller must hold until process shutdown; dropping
|
/// Opaque handle the caller must hold until process shutdown; dropping
|
||||||
/// it stops the OTLP exporter. Returns `None` when OTLP is disabled
|
/// it stops the OTLP exporter. Returns `None` when OTLP is disabled
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue