From ed2563f4f838003bf7d24f4d38e9f1c968ed0ca9 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Mon, 20 Apr 2026 13:20:22 -0500 Subject: [PATCH] style(rust): apply cargo fmt across prop_grid_rs --- rust/prop_grid_rs/src/band_config.rs | 664 +++++++++++++----- .../prop_grid_rs/src/bin/hrrr_point_worker.rs | 6 +- rust/prop_grid_rs/src/bin/worker.rs | 44 +- rust/prop_grid_rs/src/db.rs | 4 +- rust/prop_grid_rs/src/decoder.rs | 21 +- rust/prop_grid_rs/src/duct.rs | 47 +- rust/prop_grid_rs/src/hrrr_points.rs | 22 +- rust/prop_grid_rs/src/lib.rs | 2 +- rust/prop_grid_rs/src/metrics.rs | 8 +- rust/prop_grid_rs/src/native_duct.rs | 39 +- rust/prop_grid_rs/src/nexrad.rs | 6 +- rust/prop_grid_rs/src/pipeline.rs | 69 +- rust/prop_grid_rs/src/profiles_file.rs | 31 +- rust/prop_grid_rs/src/scorer.rs | 59 +- rust/prop_grid_rs/src/scores_file.rs | 25 +- rust/prop_grid_rs/src/sounding_params.rs | 82 ++- rust/prop_grid_rs/src/telemetry.rs | 6 +- 17 files changed, 839 insertions(+), 296 deletions(-) diff --git a/rust/prop_grid_rs/src/band_config.rs b/rust/prop_grid_rs/src/band_config.rs index 928dcff7..4001dc41 100644 --- a/rust/prop_grid_rs/src/band_config.rs +++ b/rust/prop_grid_rs/src/band_config.rs @@ -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, ]; -pub const HUMIDITY_BENEFICIAL_THRESHOLDS: &[(u8, i32)] = &[ - (4, 55), - (7, 70), - (10, 82), - (14, 90), - (18, 95), - (22, 88), -]; +pub const HUMIDITY_BENEFICIAL_THRESHOLDS: &[(u8, i32)] = + &[(4, 55), (7, 70), (10, 82), (14, 90), (18, 95), (22, 88)]; pub const HUMIDITY_BENEFICIAL_DEFAULT: i32 = 75; /// `(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 // common shape once so the 22 band definitions stay scannable. const BASE_LOW_BAND: [i32; 12] = months([ - (1, 38), (2, 40), (3, 22), (4, 55), (5, 68), (6, 90), - (7, 95), (8, 75), (9, 78), (10, 88), (11, 78), (12, 25), + (1, 38), + (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([ - (1, 30), (2, 32), (3, 25), (4, 50), (5, 70), (6, 95), - (7, 95), (8, 70), (9, 65), (10, 70), (11, 60), (12, 25), + (1, 30), + (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([ - (1, 88), (2, 84), (3, 68), (4, 62), (5, 51), (6, 34), - (7, 18), (8, 18), (9, 48), (10, 68), (11, 96), (12, 88), + (1, 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([ - (1, 90), (2, 88), (3, 78), (4, 68), (5, 55), (6, 38), - (7, 22), (8, 22), (9, 48), (10, 74), (11, 96), (12, 90), + (1, 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([ - (1, 90), (2, 88), (3, 78), (4, 65), (5, 50), (6, 32), - (7, 18), (8, 18), (9, 44), (10, 70), (11, 92), (12, 90), + (1, 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([ - (1, 90), (2, 90), (3, 80), (4, 68), (5, 55), (6, 38), - (7, 22), (8, 22), (9, 48), (10, 74), (11, 96), (12, 90), + (1, 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([ - (1, 92), (2, 90), (3, 78), (4, 62), (5, 45), (6, 28), - (7, 15), (8, 15), (9, 38), (10, 68), (11, 92), (12, 92), + (1, 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([ - (1, 92), (2, 90), (3, 78), (4, 65), (5, 48), (6, 30), - (7, 18), (8, 18), (9, 42), (10, 70), (11, 92), (12, 92), + (1, 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([ - (1, 92), (2, 90), (3, 78), (4, 65), (5, 47), (6, 28), - (7, 16), (8, 16), (9, 40), (10, 68), (11, 92), (12, 92), + (1, 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([ - (1, 92), (2, 90), (3, 78), (4, 64), (5, 46), (6, 27), - (7, 15), (8, 15), (9, 38), (10, 66), (11, 92), (12, 92), + (1, 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([ - (1, 95), (2, 92), (3, 75), (4, 55), (5, 35), (6, 15), - (7, 8), (8, 8), (9, 30), (10, 65), (11, 95), (12, 95), + (1, 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([ - (1, 95), (2, 92), (3, 75), (4, 55), (5, 35), (6, 14), - (7, 7), (8, 7), (9, 28), (10, 64), (11, 95), (12, 95), + (1, 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([ - (1, 96), (2, 92), (3, 74), (4, 52), (5, 32), (6, 12), - (7, 6), (8, 6), (9, 26), (10, 62), (11, 96), (12, 96), + (1, 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([ - (1, 96), (2, 92), (3, 74), (4, 52), (5, 32), (6, 12), - (7, 6), (8, 6), (9, 26), (10, 62), (11, 96), (12, 96), + (1, 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]; @@ -242,187 +376,387 @@ fn build_bands() -> Vec { use HumidityEffect::*; vec![ BandConfig { - freq_mhz: 50, label: "50 MHz", - o2_db_km: 0.0, h2o_coeff: 0.0, 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, + freq_mhz: 50, + label: "50 MHz", + o2_db_km: 0.0, + h2o_coeff: 0.0, + 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, }, BandConfig { - freq_mhz: 144, label: "144 MHz", - o2_db_km: 0.0, h2o_coeff: 0.0, 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, + freq_mhz: 144, + label: "144 MHz", + o2_db_km: 0.0, + h2o_coeff: 0.0, + 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, }, BandConfig { - freq_mhz: 222, label: "222 MHz", - o2_db_km: 0.0, h2o_coeff: 0.0, 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: 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)), + freq_mhz: 222, + label: "222 MHz", + o2_db_km: 0.0, + h2o_coeff: 0.0, + 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: 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 { - freq_mhz: 432, label: "432 MHz", - o2_db_km: 0.0, h2o_coeff: 0.0, 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: 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)), + freq_mhz: 432, + label: "432 MHz", + o2_db_km: 0.0, + h2o_coeff: 0.0, + 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: 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 { - freq_mhz: 902, label: "902 MHz", - o2_db_km: 0.006, h2o_coeff: 0.0, humidity_effect: Beneficial, 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)), + freq_mhz: 902, + label: "902 MHz", + o2_db_km: 0.006, + h2o_coeff: 0.0, + humidity_effect: Beneficial, + 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 { - freq_mhz: 1_296, label: "1296 MHz", - o2_db_km: 0.006, h2o_coeff: 0.0, humidity_effect: Beneficial, 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)), + freq_mhz: 1_296, + label: "1296 MHz", + o2_db_km: 0.006, + h2o_coeff: 0.0, + humidity_effect: Beneficial, + 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 { - freq_mhz: 2_304, label: "2304 MHz", - o2_db_km: 0.006, h2o_coeff: 0.0, humidity_effect: Beneficial, 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)), + freq_mhz: 2_304, + label: "2304 MHz", + o2_db_km: 0.006, + h2o_coeff: 0.0, + humidity_effect: Beneficial, + 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 { - freq_mhz: 3_400, label: "3400 MHz", - o2_db_km: 0.006, h2o_coeff: 0.0, humidity_effect: Beneficial, 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)), + freq_mhz: 3_400, + label: "3400 MHz", + o2_db_km: 0.006, + h2o_coeff: 0.0, + humidity_effect: Beneficial, + 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 { - freq_mhz: 5_760, label: "5760 MHz", - o2_db_km: 0.007, h2o_coeff: 0.0, humidity_effect: Beneficial, 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)), + freq_mhz: 5_760, + label: "5760 MHz", + o2_db_km: 0.007, + h2o_coeff: 0.0, + humidity_effect: Beneficial, + 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 { - freq_mhz: 10_000, label: "10 GHz", - o2_db_km: 0.007, h2o_coeff: 0.0, 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, + freq_mhz: 10_000, + label: "10 GHz", + o2_db_km: 0.007, + h2o_coeff: 0.0, + 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, }, BandConfig { - freq_mhz: 24_000, label: "24 GHz", - o2_db_km: 0.02, h2o_coeff: 0.002, humidity_effect: Harmful, 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)), + freq_mhz: 24_000, + label: "24 GHz", + o2_db_km: 0.02, + h2o_coeff: 0.002, + humidity_effect: Harmful, + 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 { - freq_mhz: 47_000, label: "47 GHz", - o2_db_km: 0.04, h2o_coeff: 0.003, 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, + freq_mhz: 47_000, + label: "47 GHz", + o2_db_km: 0.04, + h2o_coeff: 0.003, + 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, }, BandConfig { - freq_mhz: 68_000, label: "68 GHz", - o2_db_km: 0.90, h2o_coeff: 0.007, 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, + freq_mhz: 68_000, + label: "68 GHz", + o2_db_km: 0.90, + h2o_coeff: 0.007, + 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, }, BandConfig { - freq_mhz: 75_000, label: "75 GHz", - o2_db_km: 0.012, h2o_coeff: 0.006, 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, + freq_mhz: 75_000, + label: "75 GHz", + o2_db_km: 0.012, + h2o_coeff: 0.006, + 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, }, BandConfig { - freq_mhz: 122_000, label: "122 GHz", - o2_db_km: 0.80, h2o_coeff: 0.010, 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, + freq_mhz: 122_000, + label: "122 GHz", + o2_db_km: 0.80, + h2o_coeff: 0.010, + 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, }, BandConfig { - freq_mhz: 134_000, label: "134 GHz", - o2_db_km: 0.08, h2o_coeff: 0.015, 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, + freq_mhz: 134_000, + label: "134 GHz", + o2_db_km: 0.08, + h2o_coeff: 0.015, + 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, }, BandConfig { - freq_mhz: 142_000, label: "142 GHz", - o2_db_km: 0.05, h2o_coeff: 0.025, 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, + freq_mhz: 142_000, + label: "142 GHz", + o2_db_km: 0.05, + h2o_coeff: 0.025, + 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, }, BandConfig { - freq_mhz: 145_000, label: "145 GHz", - o2_db_km: 0.06, h2o_coeff: 0.040, 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, + freq_mhz: 145_000, + label: "145 GHz", + o2_db_km: 0.06, + h2o_coeff: 0.040, + 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, }, BandConfig { - freq_mhz: 241_000, label: "241 GHz", - o2_db_km: 0.08, h2o_coeff: 0.30, 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, + freq_mhz: 241_000, + label: "241 GHz", + o2_db_km: 0.08, + h2o_coeff: 0.30, + 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, }, BandConfig { - freq_mhz: 288_000, label: "288 GHz", - o2_db_km: 0.10, h2o_coeff: 0.45, 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, + freq_mhz: 288_000, + label: "288 GHz", + o2_db_km: 0.10, + h2o_coeff: 0.45, + 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, }, BandConfig { - freq_mhz: 322_000, label: "322 GHz", - o2_db_km: 0.12, h2o_coeff: 0.55, 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, + freq_mhz: 322_000, + label: "322 GHz", + o2_db_km: 0.12, + h2o_coeff: 0.55, + 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, }, BandConfig { - freq_mhz: 403_000, label: "403 GHz", - o2_db_km: 0.15, h2o_coeff: 0.40, 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, + freq_mhz: 403_000, + label: "403 GHz", + o2_db_km: 0.15, + h2o_coeff: 0.40, + 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, }, BandConfig { - freq_mhz: 411_000, label: "411 GHz", - o2_db_km: 0.15, h2o_coeff: 0.42, 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, + freq_mhz: 411_000, + label: "411 GHz", + o2_db_km: 0.15, + h2o_coeff: 0.42, + 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, }, ] @@ -448,8 +782,16 @@ mod tests { #[test] fn default_weights_sum_to_one() { let w = DEFAULT_WEIGHTS; - let sum = w.humidity + w.time_of_day + w.td_depression + w.refractivity - + w.sky + w.season + w.wind + w.rain + w.pressure + w.pwat; + let sum = w.humidity + + 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. assert!((sum - 1.0).abs() < 1e-9, "weights sum = {sum}"); } diff --git a/rust/prop_grid_rs/src/bin/hrrr_point_worker.rs b/rust/prop_grid_rs/src/bin/hrrr_point_worker.rs index b9fd213c..f4e94985 100644 --- a/rust/prop_grid_rs/src/bin/hrrr_point_worker.rs +++ b/rust/prop_grid_rs/src/bin/hrrr_point_worker.rs @@ -5,8 +5,8 @@ //! LOCKED allows N replicas with no coordination. use std::path::PathBuf; -use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use std::time::{Duration, Instant}; use prop_grid_rs::{db, fetcher::HrrrClient, hrrr_points, telemetry}; @@ -44,7 +44,9 @@ async fn main() -> Result<(), Box> { let points = task.points.clone(); 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) => { let elapsed = started.elapsed(); info!( diff --git a/rust/prop_grid_rs/src/bin/worker.rs b/rust/prop_grid_rs/src/bin/worker.rs index 792b58cc..cc0b2957 100644 --- a/rust/prop_grid_rs/src/bin/worker.rs +++ b/rust/prop_grid_rs/src/bin/worker.rs @@ -12,8 +12,8 @@ use std::net::SocketAddr; use std::path::PathBuf; -use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; use std::time::{Duration, Instant}; use prop_grid_rs::{db, fetcher::HrrrClient, metrics, pipeline, telemetry}; @@ -115,28 +115,26 @@ async fn worker_loop( let _guard = metrics::InFlightGuard::new(); let started = Instant::now(); let result = match task.kind { - db::TaskKind::Forecast => { - pipeline::run_chain_step( - &client, - &scores_dir, - &pipeline::ChainStepInput { - run_time: task.run_time, - forecast_hour: task.forecast_hour as u8, - }, - ) - .await - .map(ChainOutcome::Forecast) - } - db::TaskKind::Analysis => { - pipeline::run_analysis_step( - &client, - &pool, - &scores_dir, - &pipeline::AnalysisStepInput { run_time: task.run_time }, - ) - .await - .map(ChainOutcome::Analysis) - } + db::TaskKind::Forecast => pipeline::run_chain_step( + &client, + &scores_dir, + &pipeline::ChainStepInput { + run_time: task.run_time, + forecast_hour: task.forecast_hour as u8, + }, + ) + .await + .map(ChainOutcome::Forecast), + db::TaskKind::Analysis => pipeline::run_analysis_step( + &client, + &pool, + &scores_dir, + &pipeline::AnalysisStepInput { + run_time: task.run_time, + }, + ) + .await + .map(ChainOutcome::Analysis), }; match result { diff --git a/rust/prop_grid_rs/src/db.rs b/rust/prop_grid_rs/src/db.rs index 68ab2234..e55753e9 100644 --- a/rust/prop_grid_rs/src/db.rs +++ b/rust/prop_grid_rs/src/db.rs @@ -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)> { - let Some(arr) = v.as_array() else { return Vec::new() }; + let Some(arr) = v.as_array() else { + return Vec::new(); + }; arr.iter() .filter_map(|p| { let lat = p.get("lat")?.as_f64()?; diff --git a/rust/prop_grid_rs/src/decoder.rs b/rust/prop_grid_rs/src/decoder.rs index dcd59349..cc22004e 100644 --- a/rust/prop_grid_rs/src/decoder.rs +++ b/rust/prop_grid_rs/src/decoder.rs @@ -149,7 +149,11 @@ fn run_wgrib2_lola( 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("-match") .arg(match_pattern) @@ -290,8 +294,14 @@ mod tests { lat_step: 0.5, }; let msgs = vec![ - Message { var: "TMP".into(), level: "surface".into() }, - Message { var: "DPT".into(), level: "surface".into() }, + Message { + var: "TMP".into(), + level: "surface".into(), + }, + Message { + var: "DPT".into(), + level: "surface".into(), + }, ]; let mut buf = Vec::new(); for (msg_idx, _msg) in msgs.iter().enumerate() { @@ -332,7 +342,10 @@ mod tests { lat_count: 1, 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 len: u32 = 2 * 4; buf.extend_from_slice(&len.to_le_bytes()); diff --git a/rust/prop_grid_rs/src/duct.rs b/rust/prop_grid_rs/src/duct.rs index c6a68c94..8be47b8a 100644 --- a/rust/prop_grid_rs/src/duct.rs +++ b/rust/prop_grid_rs/src/duct.rs @@ -83,7 +83,12 @@ pub fn detect_ducts(m: &[(f64, f64)]) -> Vec { if m2 < m1 { // in a duct 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 { top: h2, min_m_val: d.min_m_val.min(m2), @@ -146,7 +151,10 @@ pub fn analyze(profile: &NativeProfile) -> Analysis { None => Some(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 @@ -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. pub fn max_duct_thickness_m(ducts: &[Duct]) -> Option { - ducts.iter().map(|d| d.thickness_m).fold(None, |acc, t| match acc { - None => Some(t), - Some(a) => Some(a.max(t)), - }) + ducts + .iter() + .map(|d| d.thickness_m) + .fold(None, |acc, t| match acc { + None => Some(t), + Some(a) => Some(a.max(t)), + }) } #[cfg(test)] @@ -193,7 +208,12 @@ mod tests { let pressure_pa: Vec = (0..10) .map(|i| 101_325.0 * (1.0 - 0.000_022_6 * (i as f64 * 100.0)).powf(5.255)) .collect(); - NativeProfile { heights_m: heights, temp_k, spfh, pressure_pa } + NativeProfile { + heights_m: heights, + temp_k, + spfh, + pressure_pa, + } } #[test] @@ -212,8 +232,15 @@ mod tests { 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 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 p = NativeProfile { heights_m, temp_k, spfh, pressure_pa }; + let pressure_pa = vec![ + 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); assert!(!a.ducts.is_empty(), "expected at least one duct"); // The duct must have positive thickness and deficit. diff --git a/rust/prop_grid_rs/src/hrrr_points.rs b/rust/prop_grid_rs/src/hrrr_points.rs index cb762674..39fdd17d 100644 --- a/rust/prop_grid_rs/src/hrrr_points.rs +++ b/rust/prop_grid_rs/src/hrrr_points.rs @@ -60,11 +60,19 @@ pub async fn process_batch( let prs_wanted = fetcher::pressure_messages_grid(); let sfc_pattern = format!( ":({}):", - sfc_wanted.iter().map(|(v, _)| v.as_str()).collect::>().join("|") + sfc_wanted + .iter() + .map(|(v, _)| v.as_str()) + .collect::>() + .join("|") ); let prs_pattern = format!( ":({}):", - prs_wanted.iter().map(|(v, _)| v.as_str()).collect::>().join("|") + prs_wanted + .iter() + .map(|(v, _)| v.as_str()) + .collect::>() + .join("|") ); 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). let mut inserted = 0u32; for &(lat, lon) in points { - let key = ( - (lat * 1000.0).round() as i32, - (lon * 1000.0).round() as i32, - ); + let key = ((lat * 1000.0).round() as i32, (lon * 1000.0).round() as i32); let mut cell: CellValues = sfc_grid.get(&key).cloned().unwrap_or_default(); if let Some(prs) = prs_grid.get(&key) { cell.extend(prs.iter().map(|(k, v)| (k.clone(), *v))); @@ -136,7 +141,10 @@ async fn upsert_profile( .get("DPT:2 m above ground") .copied() .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 pwat_mm = cell .get("PWAT:entire atmosphere (considered as a single layer)") diff --git a/rust/prop_grid_rs/src/lib.rs b/rust/prop_grid_rs/src/lib.rs index 54ee763e..a7b46cdf 100644 --- a/rust/prop_grid_rs/src/lib.rs +++ b/rust/prop_grid_rs/src/lib.rs @@ -32,7 +32,7 @@ pub mod nexrad; pub mod pipeline; pub mod profiles_file; pub mod region; -pub mod scores_file; pub mod scorer; +pub mod scores_file; pub mod sounding_params; pub mod telemetry; diff --git a/rust/prop_grid_rs/src/metrics.rs b/rust/prop_grid_rs/src/metrics.rs index ea69f5b9..62a51805 100644 --- a/rust/prop_grid_rs/src/metrics.rs +++ b/rust/prop_grid_rs/src/metrics.rs @@ -6,13 +6,7 @@ use std::sync::LazyLock; use std::time::Duration; -use axum::{ - extract::State, - http::StatusCode, - response::IntoResponse, - routing::get, - Router, -}; +use axum::{extract::State, http::StatusCode, response::IntoResponse, routing::get, Router}; use prometheus::{ register_histogram, register_histogram_vec, register_int_counter_vec, register_int_gauge, Encoder, Histogram, HistogramVec, IntCounterVec, IntGauge, TextEncoder, diff --git a/rust/prop_grid_rs/src/native_duct.rs b/rust/prop_grid_rs/src/native_duct.rs index 7845fa5c..7a1cb360 100644 --- a/rust/prop_grid_rs/src/native_duct.rs +++ b/rust/prop_grid_rs/src/native_duct.rs @@ -109,11 +109,10 @@ pub async fn fetch_native_duct_grid( // wgrib2 decode must run on the blocking pool — it forks a // subprocess and the stdout read is synchronous IO. let pattern = match_pattern(); - let grid: PointGrid = tokio::task::spawn_blocking(move || { - decoder::extract_grid(&blob, &pattern, grid_spec) - }) - .await - .expect("blocking join")?; + let grid: PointGrid = + tokio::task::spawn_blocking(move || decoder::extract_grid(&blob, &pattern, grid_spec)) + .await + .expect("blocking join")?; Ok(reduce_grid_to_ducts(grid)) } @@ -150,8 +149,12 @@ pub fn build_native_profile(cell: &CellValues) -> Option { let mut levels: Vec<(f64, f64, f64, f64)> = Vec::with_capacity(NATIVE_LEVEL_COUNT as usize); for level in 1..=NATIVE_LEVEL_COUNT { let lvl_str = format!("{level} hybrid level"); - let Some(&hgt) = cell.get(&format!("HGT:{lvl_str}")) else { continue }; - let Some(&tmp) = cell.get(&format!("TMP:{lvl_str}")) else { continue }; + let Some(&hgt) = cell.get(&format!("HGT:{lvl_str}")) else { + continue; + }; + let Some(&tmp) = cell.get(&format!("TMP:{lvl_str}")) else { + continue; + }; // SPFH / PRES default to 0.0 if absent — the duct math handles // degenerate rows (returns 0.0 gradient / no duct) without // raising, matching Elixir's nil-tolerance. @@ -170,7 +173,12 @@ pub fn build_native_profile(cell: &CellValues) -> Option { let temp_k = levels.iter().map(|x| x.1).collect(); let spfh = levels.iter().map(|x| x.2).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. @@ -186,7 +194,10 @@ pub fn merge_duct_grid( ) -> HashMap<(i32, i32), CellValues> { for (key, metrics) in ducts { 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 { cell.insert("best_duct_freq_ghz".into(), f as f32); } @@ -246,8 +257,14 @@ mod tests { for i in 1..=count { 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!("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.insert( + 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 } diff --git a/rust/prop_grid_rs/src/nexrad.rs b/rust/prop_grid_rs/src/nexrad.rs index 4c2912f9..7a8e8a95 100644 --- a/rust/prop_grid_rs/src/nexrad.rs +++ b/rust/prop_grid_rs/src/nexrad.rs @@ -141,7 +141,11 @@ pub async fn fetch_frame( .map(|&(lat, lon)| { let (cx, cy) = latlon_to_pixel(lat, lon); 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(); Ok(obs) diff --git a/rust/prop_grid_rs/src/pipeline.rs b/rust/prop_grid_rs/src/pipeline.rs index e50a331f..62c16879 100644 --- a/rust/prop_grid_rs/src/pipeline.rs +++ b/rust/prop_grid_rs/src/pipeline.rs @@ -19,8 +19,8 @@ use crate::grid::wgrib2_grid_spec; use crate::native_duct; use crate::nexrad::{self, NexradObservation}; use crate::profiles_file::{self, CellEntry}; -use crate::scores_file::{self, ScorePoint}; use crate::scorer::{self, Conditions}; +use crate::scores_file::{self, ScorePoint}; use crate::sounding_params::{self, Level}; #[derive(Debug, thiserror::Error)] @@ -165,7 +165,11 @@ pub async fn run_chain_step( let mut scores: Vec = Vec::with_capacity(prepared.len()); for (lat, lon, conditions, invariants) in prepared.iter() { 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) }) @@ -178,7 +182,9 @@ pub async fn run_chain_step( 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 { r?; } @@ -229,7 +235,10 @@ fn cell_to_conditions( _ => 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 .get("PWAT:entire atmosphere (considered as a single layer)") .copied() @@ -309,7 +318,10 @@ mod tests { cell.insert("TCDC:entire atmosphere".into(), 30.0); cell.insert("PRES:surface".into(), 101_000.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 c = cell_to_conditions(&cell, 32.0, -97.0, &vt).unwrap(); @@ -342,7 +354,9 @@ mod tests { forecast_hour: 0, }; 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)); } } @@ -450,18 +464,21 @@ pub async fn run_analysis_step( .user_agent("prop-grid-rs/0.1") .build() .expect("reqwest client"); - let nexrad_points: Vec<(f64, f64)> = merged - .keys() - .map(|&k| decoder::key_to_latlon(k)) - .collect(); - let nexrad_obs: Vec = - match nexrad::fetch_frame(&nexrad_http, valid_time, &nexrad_points).await { - Ok(obs) => obs, - Err(e) => { - tracing::warn!(error = %e, "NEXRAD fetch failed — continuing without radar overlay"); - Vec::new() - } - }; + let nexrad_points: Vec<(f64, f64)> = + merged.keys().map(|&k| decoder::key_to_latlon(k)).collect(); + let nexrad_obs: Vec = match nexrad::fetch_frame( + &nexrad_http, + valid_time, + &nexrad_points, + ) + .await + { + 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); // Commercial-link degradation — precompute once, then fold into @@ -470,8 +487,7 @@ pub async fn run_analysis_step( let commercial_lookup: Vec = commercial::build_link_lookup(pool, valid_time) .await .map_err(PipelineError::Commercial)?; - let commercial_cells_boosted = - apply_commercial(&mut merged, &commercial_lookup); + let commercial_cells_boosted = apply_commercial(&mut merged, &commercial_lookup); let point_count = merged.len() as u32; @@ -480,7 +496,8 @@ pub async fn run_analysis_step( // atmospheric + duct + NEXRAD + commercial fields under // string-keyed msgpack; Rust doesn't need to mutate those further. let mut profile_entries: Vec = 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() { let (lat, lon) = decoder::key_to_latlon(*key); 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 = Vec::with_capacity(prepared_arc.len()); for (lat, lon, conditions, invariants) in prepared_arc.iter() { 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) }) @@ -528,7 +549,9 @@ pub async fn run_analysis_step( 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 { r?; } diff --git a/rust/prop_grid_rs/src/profiles_file.rs b/rust/prop_grid_rs/src/profiles_file.rs index 1dfb1e8d..3160a4ab 100644 --- a/rust/prop_grid_rs/src/profiles_file.rs +++ b/rust/prop_grid_rs/src/profiles_file.rs @@ -46,8 +46,8 @@ use std::io::{BufWriter, Write}; use std::path::{Path, PathBuf}; use chrono::{DateTime, Utc}; -use flate2::Compression; use flate2::write::GzEncoder; +use flate2::Compression; use serde::Serialize; /// 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 /// `Float.round(_, 3)` snap before keying into the profiles map. 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 @@ -176,11 +179,18 @@ mod tests { gz.read_to_end(&mut buf).unwrap(); let decoded: rmpv::Value = rmp_serde::from_slice(&buf).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 .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; + .any(|(k, v)| k.as_str() == Some("v") && v.as_u64() == Some(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(); assert_eq!(cells_arr.len(), 1); } @@ -190,11 +200,13 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let vt = Utc.with_ymd_and_hms(2026, 4, 19, 14, 0, 0).unwrap(); 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()))]), }]; 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()))]), }]; let p1 = write_atomic(dir.path(), vt, &first).unwrap(); @@ -212,7 +224,10 @@ mod tests { fn path_for_layout() { 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); - 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] diff --git a/rust/prop_grid_rs/src/scorer.rs b/rust/prop_grid_rs/src/scorer.rs index afc364ac..92808841 100644 --- a/rust/prop_grid_rs/src/scorer.rs +++ b/rust/prop_grid_rs/src/scorer.rs @@ -11,9 +11,8 @@ //! from-zero, so no custom rounding helper is needed. use crate::band_config::{ - BandConfig, HumidityEffect, Weights, DEFAULT_WEIGHTS, - HUMIDITY_BENEFICIAL_DEFAULT, HUMIDITY_BENEFICIAL_THRESHOLDS, - REFRACTIVITY_DEFAULT, REFRACTIVITY_THRESHOLDS, SUNRISE_TABLE, + BandConfig, HumidityEffect, Weights, DEFAULT_WEIGHTS, HUMIDITY_BENEFICIAL_DEFAULT, + HUMIDITY_BENEFICIAL_THRESHOLDS, REFRACTIVITY_DEFAULT, REFRACTIVITY_THRESHOLDS, SUNRISE_TABLE, }; use crate::region; @@ -243,7 +242,12 @@ pub fn score_refractivity( }; 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) -> i32 { @@ -453,7 +457,12 @@ pub fn composite_score_with( band, ), 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, rain: score_rain(c.rain_rate_mmhr, band), pwat: score_pwat(c.pwat_mm, band), @@ -647,27 +656,42 @@ mod tests { #[test] 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] 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] 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] 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] 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] @@ -723,8 +747,7 @@ mod tests { score_refractivity(Some(-80.0), Some(800.0), Some(15.0), None, b10g()); assert_eq!(nil_r, four_arity_equiv); - let boundary = - score_refractivity(Some(-80.0), Some(800.0), Some(15.0), Some(25.0), b10g()); + let boundary = score_refractivity(Some(-80.0), Some(800.0), Some(15.0), Some(25.0), b10g()); assert_eq!(boundary, plain); } @@ -902,8 +925,16 @@ mod tests { let r = composite_score(&canonical_conditions(), b10g()); let f = &r.factors; for s in [ - f.humidity, f.time_of_day, f.td_depression, f.refractivity, - f.sky, f.season, f.wind, f.rain, f.pwat, f.pressure, + f.humidity, + 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}"); } diff --git a/rust/prop_grid_rs/src/scores_file.rs b/rust/prop_grid_rs/src/scores_file.rs index c5a42663..63918cb4 100644 --- a/rust/prop_grid_rs/src/scores_file.rs +++ b/rust/prop_grid_rs/src/scores_file.rs @@ -35,9 +35,7 @@ static UNIQUE: AtomicU64 = AtomicU64::new(1); /// Absolute path `//.ntms`. pub fn path_for(base_dir: &Path, band_mhz: u32, valid_time: DateTime) -> PathBuf { - let iso = valid_time - .format("%Y-%m-%dT%H:%M:%SZ") - .to_string(); + let iso = valid_time.format("%Y-%m-%dT%H:%M:%SZ").to_string(); base_dir .join(band_mhz.to_string()) .join(format!("{iso}.ntms")) @@ -87,8 +85,7 @@ pub fn encode( body[(row as usize) * col_count + (col as usize)] = clamp_score(s.score); } - let mut out = - Vec::with_capacity(4 + 1 + 4 + 8 + 4 + 4 + 4 + 2 + 2 + body.len()); + let mut out = Vec::with_capacity(4 + 1 + 4 + 8 + 4 + 4 + 4 + 2 + 2 + body.len()); out.extend_from_slice(MAGIC); out.push(VERSION); out.extend_from_slice(&band_mhz.to_le_bytes()); @@ -246,8 +243,16 @@ mod tests { #[test] fn clamps_out_of_range_scores() { let scores = vec![ - ScorePoint { lat: 25.0, lon: -125.0, score: -5 }, - ScorePoint { lat: 25.125, lon: -125.0, score: 150 }, + ScorePoint { + 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 bytes = encode(10_000, ts, &scores).unwrap(); @@ -271,7 +276,11 @@ mod tests { fn atomic_write_and_decode() { let dir = tempfile::tempdir().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(); diff --git a/rust/prop_grid_rs/src/sounding_params.rs b/rust/prop_grid_rs/src/sounding_params.rs index cbc73231..020d3a75 100644 --- a/rust/prop_grid_rs/src/sounding_params.rs +++ b/rust/prop_grid_rs/src/sounding_params.rs @@ -91,10 +91,30 @@ mod tests { // Synthetic profile: refractivity decreases with altitude → negative // dN/dh. Realistic numbers from a standard atmosphere. let levels = vec![ - Level { pres_mb: 1000.0, hght_m: 100.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) }, + Level { + pres_mb: 1000.0, + hght_m: 100.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(); 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 — // strong negative refractivity gradient. let inversion = vec![ - Level { pres_mb: 1000.0, hght_m: 100.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) }, + Level { + pres_mb: 1000.0, + hght_m: 100.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 neutral = vec![ - Level { pres_mb: 1000.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) }, + Level { + pres_mb: 1000.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(); - assert!(strong < baseline, "inversion {strong} vs baseline {baseline}"); + assert!( + strong < baseline, + "inversion {strong} vs baseline {baseline}" + ); } } diff --git a/rust/prop_grid_rs/src/telemetry.rs b/rust/prop_grid_rs/src/telemetry.rs index eb2204a3..17ae1df7 100644 --- a/rust/prop_grid_rs/src/telemetry.rs +++ b/rust/prop_grid_rs/src/telemetry.rs @@ -17,10 +17,10 @@ //! (`service.name`, `service.namespace`) so Tempo groups spans per //! 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_sdk::{Resource, trace::SdkTracerProvider}; -use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; +use opentelemetry_sdk::{trace::SdkTracerProvider, Resource}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; /// Opaque handle the caller must hold until process shutdown; dropping /// it stops the OTLP exporter. Returns `None` when OTLP is disabled