From c12cf582d274ce38483c3dc704d9510c149cc2a8 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Mon, 20 May 2024 12:24:37 -0700 Subject: [PATCH 001/115] add new crate for mobile reward coverage points calculations --- Cargo.lock | 4 ++++ Cargo.toml | 1 + coverage_point_calculator/Cargo.toml | 9 +++++++++ coverage_point_calculator/src/lib.rs | 14 ++++++++++++++ 4 files changed, 28 insertions(+) create mode 100644 coverage_point_calculator/Cargo.toml create mode 100644 coverage_point_calculator/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 003135243..7abc42002 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2130,6 +2130,10 @@ dependencies = [ "hextree", ] +[[package]] +name = "coverage_point_calculator" +version = "0.1.0" + [[package]] name = "cpufeatures" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index 46474b62e..a9bbc425f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ debug = true members = [ "boost_manager", "coverage_map", + "coverage_point_calculator", "custom_tracing", "db_store", "denylist", diff --git a/coverage_point_calculator/Cargo.toml b/coverage_point_calculator/Cargo.toml new file mode 100644 index 000000000..448dada85 --- /dev/null +++ b/coverage_point_calculator/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "coverage_point_calculator" +version = "0.1.0" +description = "Calculate Coverage Points for hotspots in the Mobile Network" +authors.workspace = true +license.workspace = true +edition.workspace = true + +[dependencies] diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs new file mode 100644 index 000000000..7d12d9af8 --- /dev/null +++ b/coverage_point_calculator/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} From a752017597214a5870d616e9b50ac52f8c8f8643 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Mon, 20 May 2024 15:16:20 -0700 Subject: [PATCH 002/115] super simple calculate function --- coverage_point_calculator/src/lib.rs | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 7d12d9af8..374a4a36d 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -1,5 +1,19 @@ -pub fn add(left: usize, right: usize) -> usize { - left + right +#[derive(Debug)] +struct Radio; +#[derive(Debug)] +struct BoostedHexes; +#[derive(Debug)] +struct CoverageMap; + +#[derive(Debug, PartialEq)] +struct CoveragePoints; + +fn calculate( + _radio: Radio, + _boosted_hexes: BoostedHexes, + _coverage_map: CoverageMap, +) -> CoveragePoints { + CoveragePoints } #[cfg(test)] @@ -8,7 +22,12 @@ mod tests { #[test] fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); + let radio = Radio; + let boosted_hexes = BoostedHexes; + let coverage_map = CoverageMap; + + let coverage_points = calculate(radio, boosted_hexes, coverage_map); + + assert_eq!(coverage_points, CoveragePoints); } } From a38119e2d08ac2961bf6d13eba722179b1c04567 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Mon, 20 May 2024 15:47:06 -0700 Subject: [PATCH 003/115] starting with indoor wifi coverage points --- Cargo.lock | 4 + coverage_point_calculator/Cargo.toml | 2 + coverage_point_calculator/src/lib.rs | 162 +++++++++++++++++++++++---- 3 files changed, 148 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7abc42002..148e04066 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2133,6 +2133,10 @@ dependencies = [ [[package]] name = "coverage_point_calculator" version = "0.1.0" +dependencies = [ + "hextree", + "rust_decimal", +] [[package]] name = "cpufeatures" diff --git a/coverage_point_calculator/Cargo.toml b/coverage_point_calculator/Cargo.toml index 448dada85..6faf58bf1 100644 --- a/coverage_point_calculator/Cargo.toml +++ b/coverage_point_calculator/Cargo.toml @@ -7,3 +7,5 @@ license.workspace = true edition.workspace = true [dependencies] +hextree.workspace = true +rust_decimal.workspace = true diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 374a4a36d..fe2832862 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -1,33 +1,155 @@ -#[derive(Debug)] -struct Radio; -#[derive(Debug)] -struct BoostedHexes; -#[derive(Debug)] -struct CoverageMap; +#![allow(unused)] + +use std::num::NonZeroU16; + +use hextree::Cell; +use rust_decimal::Decimal; + +type Multiplier = NonZeroU16; +type Points = u32; + +#[derive(Debug, Clone, PartialEq)] +enum RadioType { + IndoorWifi, + OutdoorWifi, + IndoorCbrs, + OutdoorCbrs, +} +impl RadioType { + fn coverage_points(&self, signal_level: &SignalLevel) -> Points { + match self { + RadioType::IndoorWifi => match signal_level { + SignalLevel::High => 400, + SignalLevel::Low => 100, + other => panic!("indoor wifi radios cannot have {other:?} signal levels"), + }, + RadioType::OutdoorWifi => match signal_level { + SignalLevel::High => 16, + SignalLevel::Medium => 8, + SignalLevel::Low => 4, + SignalLevel::None => 0, + }, + RadioType::IndoorCbrs => todo!(), + RadioType::OutdoorCbrs => todo!(), + } + } +} + +#[derive(Debug, Clone, PartialEq)] +enum SignalLevel { + High, + Medium, + Low, + None, +} + +trait Coverage { + fn radio_type(&self) -> RadioType; + fn signal_level(&self) -> SignalLevel; +} + +trait CoverageMap { + fn get(&self, cell: Cell) -> Vec; +} + +trait RewardableRadio { + fn hex(&self) -> Cell; + fn radio_type(&self) -> RadioType; + fn location_trust_scores(&self) -> Vec; + fn verified_radio_threshold(&self) -> bool; +} #[derive(Debug, PartialEq)] -struct CoveragePoints; +struct LocalRadio { + radio_type: RadioType, + speedtest_multiplier: Multiplier, + location_trust_scores: Vec, + verified_radio_threshold: bool, + hexes: Vec, +} -fn calculate( - _radio: Radio, - _boosted_hexes: BoostedHexes, - _coverage_map: CoverageMap, -) -> CoveragePoints { - CoveragePoints +#[derive(Debug, PartialEq)] +struct LocalHex { + rank: u16, + signal_level: SignalLevel, + boosted: Option, +} + +fn calculate( + radio: impl RewardableRadio, + coverage_map: impl CoverageMap, +) -> LocalRadio { + todo!() +} + +impl LocalRadio { + pub fn coverage_points(&self) -> Points { + let mut points = 0; + for hex in self.hexes.iter() { + points += self.radio_type.coverage_points(&hex.signal_level); + } + points + } } #[cfg(test)] mod tests { + use super::*; #[test] - fn it_works() { - let radio = Radio; - let boosted_hexes = BoostedHexes; - let coverage_map = CoverageMap; - - let coverage_points = calculate(radio, boosted_hexes, coverage_map); + fn outdoor_wifi_radio_coverage_points() { + let local_radio = LocalRadio { + radio_type: RadioType::OutdoorWifi, + speedtest_multiplier: NonZeroU16::new(1).unwrap(), + location_trust_scores: vec![NonZeroU16::new(1).unwrap()], + verified_radio_threshold: true, + hexes: vec![ + LocalHex { + rank: 1, + signal_level: SignalLevel::High, + boosted: None, + }, + LocalHex { + rank: 1, + signal_level: SignalLevel::Medium, + boosted: None, + }, + LocalHex { + rank: 1, + signal_level: SignalLevel::Low, + boosted: None, + }, + LocalHex { + rank: 1, + signal_level: SignalLevel::None, + boosted: None, + }, + ], + }; + assert_eq!(28, local_radio.coverage_points()); + } - assert_eq!(coverage_points, CoveragePoints); + #[test] + fn indoor_wifi_radio_coverage_points() { + let local_radio = LocalRadio { + radio_type: RadioType::IndoorWifi, + speedtest_multiplier: NonZeroU16::new(1).unwrap(), + location_trust_scores: vec![NonZeroU16::new(1).unwrap()], + verified_radio_threshold: true, + hexes: vec![ + LocalHex { + rank: 1, + signal_level: SignalLevel::High, + boosted: None, + }, + LocalHex { + rank: 1, + signal_level: SignalLevel::Low, + boosted: None, + }, + ], + }; + assert_eq!(500, local_radio.coverage_points()); } } From c628887aaf44bcae66fa1f01f1ad8fbba957572e Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 21 May 2024 14:16:32 -0700 Subject: [PATCH 004/115] base tests for all radio types --- coverage_point_calculator/src/lib.rs | 79 ++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index fe2832862..494a69172 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -29,8 +29,17 @@ impl RadioType { SignalLevel::Low => 4, SignalLevel::None => 0, }, - RadioType::IndoorCbrs => todo!(), - RadioType::OutdoorCbrs => todo!(), + RadioType::IndoorCbrs => match signal_level { + SignalLevel::High => 100, + SignalLevel::Low => 25, + other => panic!("indoor cbrs radios cannot have {other:?} signal levels"), + }, + RadioType::OutdoorCbrs => match signal_level { + SignalLevel::High => 4, + SignalLevel::Medium => 2, + SignalLevel::Low => 1, + SignalLevel::None => 0, + }, } } } @@ -98,8 +107,56 @@ mod tests { use super::*; #[test] - fn outdoor_wifi_radio_coverage_points() { - let local_radio = LocalRadio { + fn base_radio_coverage_points() { + let outdoor_cbrs = LocalRadio { + radio_type: RadioType::OutdoorCbrs, + speedtest_multiplier: NonZeroU16::new(1).unwrap(), + location_trust_scores: vec![NonZeroU16::new(1).unwrap()], + verified_radio_threshold: true, + hexes: vec![ + LocalHex { + rank: 1, + signal_level: SignalLevel::High, + boosted: None, + }, + LocalHex { + rank: 1, + signal_level: SignalLevel::Medium, + boosted: None, + }, + LocalHex { + rank: 1, + signal_level: SignalLevel::Low, + boosted: None, + }, + LocalHex { + rank: 1, + signal_level: SignalLevel::None, + boosted: None, + }, + ], + }; + + let indoor_cbrs = LocalRadio { + radio_type: RadioType::IndoorCbrs, + speedtest_multiplier: NonZeroU16::new(1).unwrap(), + location_trust_scores: vec![NonZeroU16::new(1).unwrap()], + verified_radio_threshold: true, + hexes: vec![ + LocalHex { + rank: 1, + signal_level: SignalLevel::High, + boosted: None, + }, + LocalHex { + rank: 1, + signal_level: SignalLevel::Low, + boosted: None, + }, + ], + }; + + let outdoor_wifi = LocalRadio { radio_type: RadioType::OutdoorWifi, speedtest_multiplier: NonZeroU16::new(1).unwrap(), location_trust_scores: vec![NonZeroU16::new(1).unwrap()], @@ -127,12 +184,8 @@ mod tests { }, ], }; - assert_eq!(28, local_radio.coverage_points()); - } - #[test] - fn indoor_wifi_radio_coverage_points() { - let local_radio = LocalRadio { + let indoor_wifi = LocalRadio { radio_type: RadioType::IndoorWifi, speedtest_multiplier: NonZeroU16::new(1).unwrap(), location_trust_scores: vec![NonZeroU16::new(1).unwrap()], @@ -150,6 +203,12 @@ mod tests { }, ], }; - assert_eq!(500, local_radio.coverage_points()); + + // When each radio contains a hex of every applicable signal_level, and + // multipliers are break even. These are the accumulated coverage points. + assert_eq!(7, outdoor_cbrs.coverage_points()); + assert_eq!(125, indoor_cbrs.coverage_points()); + assert_eq!(28, outdoor_wifi.coverage_points()); + assert_eq!(500, indoor_wifi.coverage_points()); } } From 92ac409269836c94547909a5fe7b28045da4e467 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 21 May 2024 14:35:42 -0700 Subject: [PATCH 005/115] boosted hexes --- coverage_point_calculator/src/lib.rs | 61 ++++++++++++++++++++++------ 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 494a69172..384ad3258 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -1,11 +1,9 @@ #![allow(unused)] -use std::num::NonZeroU16; - use hextree::Cell; use rust_decimal::Decimal; -type Multiplier = NonZeroU16; +type Multiplier = std::num::NonZeroU32; type Points = u32; #[derive(Debug, Clone, PartialEq)] @@ -95,7 +93,17 @@ impl LocalRadio { pub fn coverage_points(&self) -> Points { let mut points = 0; for hex in self.hexes.iter() { - points += self.radio_type.coverage_points(&hex.signal_level); + let hex_points = self.radio_type.coverage_points(&hex.signal_level); + + // When the radio is verified to receive boosted rewards we ask for + // the boosted value, falling back to 1 as a passthrough value. + let maybe_boost = if self.verified_radio_threshold { + hex.boosted.map_or(1, |boost| boost.get()) + } else { + 1 + }; + + points += hex_points * maybe_boost; } points } @@ -106,12 +114,41 @@ mod tests { use super::*; + #[test] + fn boosted_hex() { + let mut indoor_wifi = LocalRadio { + radio_type: RadioType::IndoorWifi, + speedtest_multiplier: Multiplier::new(1).unwrap(), + location_trust_scores: vec![Multiplier::new(1).unwrap()], + verified_radio_threshold: true, + hexes: vec![ + LocalHex { + rank: 1, + signal_level: SignalLevel::High, + boosted: None, + }, + LocalHex { + rank: 1, + signal_level: SignalLevel::Low, + boosted: Multiplier::new(4), + }, + ], + }; + // The hex with a low signal_level is boosted to the same level as a + // signal_level of High. + assert_eq!(800, indoor_wifi.coverage_points()); + + // When the radio is not verified for boosted rewards, the boost has no effect. + indoor_wifi.verified_radio_threshold = false; + assert_eq!(500, indoor_wifi.coverage_points()); + } + #[test] fn base_radio_coverage_points() { let outdoor_cbrs = LocalRadio { radio_type: RadioType::OutdoorCbrs, - speedtest_multiplier: NonZeroU16::new(1).unwrap(), - location_trust_scores: vec![NonZeroU16::new(1).unwrap()], + speedtest_multiplier: Multiplier::new(1).unwrap(), + location_trust_scores: vec![Multiplier::new(1).unwrap()], verified_radio_threshold: true, hexes: vec![ LocalHex { @@ -139,8 +176,8 @@ mod tests { let indoor_cbrs = LocalRadio { radio_type: RadioType::IndoorCbrs, - speedtest_multiplier: NonZeroU16::new(1).unwrap(), - location_trust_scores: vec![NonZeroU16::new(1).unwrap()], + speedtest_multiplier: Multiplier::new(1).unwrap(), + location_trust_scores: vec![Multiplier::new(1).unwrap()], verified_radio_threshold: true, hexes: vec![ LocalHex { @@ -158,8 +195,8 @@ mod tests { let outdoor_wifi = LocalRadio { radio_type: RadioType::OutdoorWifi, - speedtest_multiplier: NonZeroU16::new(1).unwrap(), - location_trust_scores: vec![NonZeroU16::new(1).unwrap()], + speedtest_multiplier: Multiplier::new(1).unwrap(), + location_trust_scores: vec![Multiplier::new(1).unwrap()], verified_radio_threshold: true, hexes: vec![ LocalHex { @@ -187,8 +224,8 @@ mod tests { let indoor_wifi = LocalRadio { radio_type: RadioType::IndoorWifi, - speedtest_multiplier: NonZeroU16::new(1).unwrap(), - location_trust_scores: vec![NonZeroU16::new(1).unwrap()], + speedtest_multiplier: Multiplier::new(1).unwrap(), + location_trust_scores: vec![Multiplier::new(1).unwrap()], verified_radio_threshold: true, hexes: vec![ LocalHex { From 7c0956efa8c4fdc0894d8d1edd149d793df1f8e8 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 21 May 2024 15:51:12 -0700 Subject: [PATCH 006/115] location trust score moved to using decimals now that summing lists is involved, and we need to retain accuracy --- Cargo.lock | 1 + coverage_point_calculator/Cargo.toml | 1 + coverage_point_calculator/src/lib.rs | 120 ++++++++++++++++++--------- 3 files changed, 85 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 148e04066..eeae46b57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2136,6 +2136,7 @@ version = "0.1.0" dependencies = [ "hextree", "rust_decimal", + "rust_decimal_macros", ] [[package]] diff --git a/coverage_point_calculator/Cargo.toml b/coverage_point_calculator/Cargo.toml index 6faf58bf1..46bbb1968 100644 --- a/coverage_point_calculator/Cargo.toml +++ b/coverage_point_calculator/Cargo.toml @@ -9,3 +9,4 @@ edition.workspace = true [dependencies] hextree.workspace = true rust_decimal.workspace = true +rust_decimal_macros.workspace = true diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 384ad3258..cfa97d5d7 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -2,9 +2,11 @@ use hextree::Cell; use rust_decimal::Decimal; +use rust_decimal_macros::dec; type Multiplier = std::num::NonZeroU32; -type Points = u32; +type MaxOneMultplier = Decimal; +type Points = Decimal; #[derive(Debug, Clone, PartialEq)] enum RadioType { @@ -13,30 +15,31 @@ enum RadioType { IndoorCbrs, OutdoorCbrs, } + impl RadioType { fn coverage_points(&self, signal_level: &SignalLevel) -> Points { match self { RadioType::IndoorWifi => match signal_level { - SignalLevel::High => 400, - SignalLevel::Low => 100, + SignalLevel::High => dec!(400), + SignalLevel::Low => dec!(100), other => panic!("indoor wifi radios cannot have {other:?} signal levels"), }, RadioType::OutdoorWifi => match signal_level { - SignalLevel::High => 16, - SignalLevel::Medium => 8, - SignalLevel::Low => 4, - SignalLevel::None => 0, + SignalLevel::High => dec!(16), + SignalLevel::Medium => dec!(8), + SignalLevel::Low => dec!(4), + SignalLevel::None => dec!(0), }, RadioType::IndoorCbrs => match signal_level { - SignalLevel::High => 100, - SignalLevel::Low => 25, + SignalLevel::High => dec!(100), + SignalLevel::Low => dec!(25), other => panic!("indoor cbrs radios cannot have {other:?} signal levels"), }, RadioType::OutdoorCbrs => match signal_level { - SignalLevel::High => 4, - SignalLevel::Medium => 2, - SignalLevel::Low => 1, - SignalLevel::None => 0, + SignalLevel::High => dec!(4), + SignalLevel::Medium => dec!(2), + SignalLevel::Low => dec!(1), + SignalLevel::None => dec!(0), }, } } @@ -70,7 +73,7 @@ trait RewardableRadio { struct LocalRadio { radio_type: RadioType, speedtest_multiplier: Multiplier, - location_trust_scores: Vec, + location_trust_scores: Vec, verified_radio_threshold: bool, hexes: Vec, } @@ -91,21 +94,39 @@ fn calculate( impl LocalRadio { pub fn coverage_points(&self) -> Points { - let mut points = 0; + let mut points = vec![]; + let location_trust_score_multiplier = self.location_trust_multiplier(); for hex in self.hexes.iter() { - let hex_points = self.radio_type.coverage_points(&hex.signal_level); - - // When the radio is verified to receive boosted rewards we ask for - // the boosted value, falling back to 1 as a passthrough value. - let maybe_boost = if self.verified_radio_threshold { - hex.boosted.map_or(1, |boost| boost.get()) - } else { - 1 - }; + let base_coverage_points = self.radio_type.coverage_points(&hex.signal_level); + let oracle_multiplier = dec!(1); + let rank = dec!(1); + let hex_boost_multiplier = self.hex_boosting_multiplier(&hex); - points += hex_points * maybe_boost; + // https://www.notion.so/nova-labs/POC-reward-formula-7d1f62b638b5447fbfe37a11c0a3d3c8 + let coverage_points = base_coverage_points + * oracle_multiplier + * rank + * hex_boost_multiplier + * location_trust_score_multiplier; + points.push(coverage_points) } - points + + points.iter().sum::().round_dp(2) + } + + fn location_trust_multiplier(&self) -> Decimal { + let trust_score_count = Decimal::from(self.location_trust_scores.len()); + let trust_score_sum = self.location_trust_scores.iter().sum::(); + trust_score_sum / trust_score_count + } + + fn hex_boosting_multiplier(&self, hex: &LocalHex) -> Decimal { + let maybe_boost = if self.verified_radio_threshold { + hex.boosted.map_or(1, |boost| boost.get()) + } else { + 1 + }; + Decimal::from(maybe_boost) } } @@ -113,13 +134,38 @@ impl LocalRadio { mod tests { use super::*; + use rust_decimal_macros::dec; + + #[test] + fn location_trust_score_multiplier() { + // Location scores are averaged together + let indoor_wifi = LocalRadio { + radio_type: RadioType::IndoorWifi, + speedtest_multiplier: Multiplier::new(1).unwrap(), + location_trust_scores: vec![ + MaxOneMultplier::from_f32_retain(0.1).unwrap(), + MaxOneMultplier::from_f32_retain(0.2).unwrap(), + MaxOneMultplier::from_f32_retain(0.3).unwrap(), + MaxOneMultplier::from_f32_retain(0.4).unwrap(), + ], + verified_radio_threshold: true, + hexes: vec![LocalHex { + rank: 1, + signal_level: SignalLevel::High, + boosted: None, + }], + }; + + // Location trust scores is 1/4 + assert_eq!(dec!(100), indoor_wifi.coverage_points()); + } #[test] fn boosted_hex() { let mut indoor_wifi = LocalRadio { radio_type: RadioType::IndoorWifi, speedtest_multiplier: Multiplier::new(1).unwrap(), - location_trust_scores: vec![Multiplier::new(1).unwrap()], + location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ LocalHex { @@ -136,11 +182,11 @@ mod tests { }; // The hex with a low signal_level is boosted to the same level as a // signal_level of High. - assert_eq!(800, indoor_wifi.coverage_points()); + assert_eq!(dec!(800), indoor_wifi.coverage_points()); // When the radio is not verified for boosted rewards, the boost has no effect. indoor_wifi.verified_radio_threshold = false; - assert_eq!(500, indoor_wifi.coverage_points()); + assert_eq!(dec!(500), indoor_wifi.coverage_points()); } #[test] @@ -148,7 +194,7 @@ mod tests { let outdoor_cbrs = LocalRadio { radio_type: RadioType::OutdoorCbrs, speedtest_multiplier: Multiplier::new(1).unwrap(), - location_trust_scores: vec![Multiplier::new(1).unwrap()], + location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ LocalHex { @@ -177,7 +223,7 @@ mod tests { let indoor_cbrs = LocalRadio { radio_type: RadioType::IndoorCbrs, speedtest_multiplier: Multiplier::new(1).unwrap(), - location_trust_scores: vec![Multiplier::new(1).unwrap()], + location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ LocalHex { @@ -196,7 +242,7 @@ mod tests { let outdoor_wifi = LocalRadio { radio_type: RadioType::OutdoorWifi, speedtest_multiplier: Multiplier::new(1).unwrap(), - location_trust_scores: vec![Multiplier::new(1).unwrap()], + location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ LocalHex { @@ -225,7 +271,7 @@ mod tests { let indoor_wifi = LocalRadio { radio_type: RadioType::IndoorWifi, speedtest_multiplier: Multiplier::new(1).unwrap(), - location_trust_scores: vec![Multiplier::new(1).unwrap()], + location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ LocalHex { @@ -243,9 +289,9 @@ mod tests { // When each radio contains a hex of every applicable signal_level, and // multipliers are break even. These are the accumulated coverage points. - assert_eq!(7, outdoor_cbrs.coverage_points()); - assert_eq!(125, indoor_cbrs.coverage_points()); - assert_eq!(28, outdoor_wifi.coverage_points()); - assert_eq!(500, indoor_wifi.coverage_points()); + assert_eq!(dec!(7), outdoor_cbrs.coverage_points()); + assert_eq!(dec!(125), indoor_cbrs.coverage_points()); + assert_eq!(dec!(28), outdoor_wifi.coverage_points()); + assert_eq!(dec!(500), indoor_wifi.coverage_points()); } } From 172d3a8ab37ec79f901f5d758dcdf851665e037b Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 21 May 2024 16:05:24 -0700 Subject: [PATCH 007/115] rank multiplier still need to decide how the multipliers are going to be indexed. 0-indexed is easy, but it seems what to talk about a radio being "0th ranked" in a hex. --- coverage_point_calculator/src/lib.rs | 88 +++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 3 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index cfa97d5d7..bd0ec1aff 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -43,6 +43,18 @@ impl RadioType { }, } } + + fn rank_multiplier(&self, hex: &LocalHex) -> Option { + let multipliers = match self { + RadioType::IndoorWifi => vec![dec!(1)], + RadioType::IndoorCbrs => vec![dec!(1)], + RadioType::OutdoorWifi => vec![dec!(1), dec!(0.5), dec!(0.25)], + RadioType::OutdoorCbrs => vec![dec!(1), dec!(0.5), dec!(0.25)], + }; + + // TODO: decide if rank should be 0-indexed + multipliers.get(hex.rank - 1).cloned() + } } #[derive(Debug, Clone, PartialEq)] @@ -80,7 +92,7 @@ struct LocalRadio { #[derive(Debug, PartialEq)] struct LocalHex { - rank: u16, + rank: usize, signal_level: SignalLevel, boosted: Option, } @@ -99,7 +111,10 @@ impl LocalRadio { for hex in self.hexes.iter() { let base_coverage_points = self.radio_type.coverage_points(&hex.signal_level); let oracle_multiplier = dec!(1); - let rank = dec!(1); + let Some(rank) = self.radio_type.rank_multiplier(hex) else { + // Rank falls outside what is allowed, hex is skipped + continue; + }; let hex_boost_multiplier = self.hex_boosting_multiplier(&hex); // https://www.notion.so/nova-labs/POC-reward-formula-7d1f62b638b5447fbfe37a11c0a3d3c8 @@ -120,7 +135,7 @@ impl LocalRadio { trust_score_sum / trust_score_count } - fn hex_boosting_multiplier(&self, hex: &LocalHex) -> Decimal { + fn hex_boosting_multiplier(&self, hex: &LocalHex) -> MaxOneMultplier { let maybe_boost = if self.verified_radio_threshold { hex.boosted.map_or(1, |boost| boost.get()) } else { @@ -136,6 +151,73 @@ mod tests { use super::*; use rust_decimal_macros::dec; + #[test] + fn outdoor_radios_consider_top_3_ranked_hexes() { + let outdoor_wifi = LocalRadio { + radio_type: RadioType::OutdoorWifi, + speedtest_multiplier: Multiplier::new(1).unwrap(), + location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], + verified_radio_threshold: true, + hexes: vec![ + LocalHex { + rank: 1, + signal_level: SignalLevel::High, + boosted: None, + }, + LocalHex { + rank: 2, + signal_level: SignalLevel::High, + boosted: None, + }, + LocalHex { + rank: 3, + signal_level: SignalLevel::High, + boosted: None, + }, + LocalHex { + rank: 42, + signal_level: SignalLevel::High, + boosted: None, + }, + ], + }; + + // rank 1 :: 1.00 * 16 == 16 + // rank 2 :: 0.50 * 16 == 8 + // rank 3 :: 0.25 * 16 == 4 + // rank 42 :: 0.00 * 16 == 0 + assert_eq!(dec!(28), outdoor_wifi.coverage_points()); + } + + #[test] + fn indoor_radios_only_consider_first_ranked_hexes() { + let indoor_wifi = LocalRadio { + radio_type: RadioType::IndoorWifi, + speedtest_multiplier: Multiplier::new(1).unwrap(), + location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], + verified_radio_threshold: true, + hexes: vec![ + LocalHex { + rank: 1, + signal_level: SignalLevel::High, + boosted: None, + }, + LocalHex { + rank: 2, + signal_level: SignalLevel::High, + boosted: None, + }, + LocalHex { + rank: 42, + signal_level: SignalLevel::High, + boosted: None, + }, + ], + }; + + assert_eq!(dec!(400), indoor_wifi.coverage_points()); + } + #[test] fn location_trust_score_multiplier() { // Location scores are averaged together From 38e25d264e310b502b610652ce876cf8c6ecec45 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 21 May 2024 16:13:21 -0700 Subject: [PATCH 008/115] Start adding Assignment --- coverage_point_calculator/src/lib.rs | 51 ++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index bd0ec1aff..0959d31ef 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -65,6 +65,20 @@ enum SignalLevel { None, } +#[derive(Debug, Clone, PartialEq)] +struct Assignments { + footfall: Assignment, + landtype: Assignment, + urbanized: Assignment, +} + +#[derive(Debug, Clone, PartialEq)] +enum Assignment { + A, + B, + C, +} + trait Coverage { fn radio_type(&self) -> RadioType; fn signal_level(&self) -> SignalLevel; @@ -94,6 +108,7 @@ struct LocalRadio { struct LocalHex { rank: usize, signal_level: SignalLevel, + assignment: Assignments, boosted: Option, } @@ -110,7 +125,7 @@ impl LocalRadio { let location_trust_score_multiplier = self.location_trust_multiplier(); for hex in self.hexes.iter() { let base_coverage_points = self.radio_type.coverage_points(&hex.signal_level); - let oracle_multiplier = dec!(1); + let assignments_multiplier = dec!(1); let Some(rank) = self.radio_type.rank_multiplier(hex) else { // Rank falls outside what is allowed, hex is skipped continue; @@ -119,7 +134,7 @@ impl LocalRadio { // https://www.notion.so/nova-labs/POC-reward-formula-7d1f62b638b5447fbfe37a11c0a3d3c8 let coverage_points = base_coverage_points - * oracle_multiplier + * assignments_multiplier * rank * hex_boost_multiplier * location_trust_score_multiplier; @@ -151,6 +166,16 @@ mod tests { use super::*; use rust_decimal_macros::dec; + impl Assignments { + fn best() -> Self { + Self { + footfall: Assignment::A, + landtype: Assignment::A, + urbanized: Assignment::A, + } + } + } + #[test] fn outdoor_radios_consider_top_3_ranked_hexes() { let outdoor_wifi = LocalRadio { @@ -162,21 +187,25 @@ mod tests { LocalHex { rank: 1, signal_level: SignalLevel::High, + assignment: Assignments::best(), boosted: None, }, LocalHex { rank: 2, signal_level: SignalLevel::High, + assignment: Assignments::best(), boosted: None, }, LocalHex { rank: 3, signal_level: SignalLevel::High, + assignment: Assignments::best(), boosted: None, }, LocalHex { rank: 42, signal_level: SignalLevel::High, + assignment: Assignments::best(), boosted: None, }, ], @@ -200,16 +229,19 @@ mod tests { LocalHex { rank: 1, signal_level: SignalLevel::High, + assignment: Assignments::best(), boosted: None, }, LocalHex { rank: 2, signal_level: SignalLevel::High, + assignment: Assignments::best(), boosted: None, }, LocalHex { rank: 42, signal_level: SignalLevel::High, + assignment: Assignments::best(), boosted: None, }, ], @@ -234,6 +266,7 @@ mod tests { hexes: vec![LocalHex { rank: 1, signal_level: SignalLevel::High, + assignment: Assignments::best(), boosted: None, }], }; @@ -253,11 +286,13 @@ mod tests { LocalHex { rank: 1, signal_level: SignalLevel::High, + assignment: Assignments::best(), boosted: None, }, LocalHex { rank: 1, signal_level: SignalLevel::Low, + assignment: Assignments::best(), boosted: Multiplier::new(4), }, ], @@ -282,21 +317,25 @@ mod tests { LocalHex { rank: 1, signal_level: SignalLevel::High, + assignment: Assignments::best(), boosted: None, }, LocalHex { rank: 1, signal_level: SignalLevel::Medium, + assignment: Assignments::best(), boosted: None, }, LocalHex { rank: 1, signal_level: SignalLevel::Low, + assignment: Assignments::best(), boosted: None, }, LocalHex { rank: 1, signal_level: SignalLevel::None, + assignment: Assignments::best(), boosted: None, }, ], @@ -311,11 +350,13 @@ mod tests { LocalHex { rank: 1, signal_level: SignalLevel::High, + assignment: Assignments::best(), boosted: None, }, LocalHex { rank: 1, signal_level: SignalLevel::Low, + assignment: Assignments::best(), boosted: None, }, ], @@ -330,21 +371,25 @@ mod tests { LocalHex { rank: 1, signal_level: SignalLevel::High, + assignment: Assignments::best(), boosted: None, }, LocalHex { rank: 1, signal_level: SignalLevel::Medium, + assignment: Assignments::best(), boosted: None, }, LocalHex { rank: 1, signal_level: SignalLevel::Low, + assignment: Assignments::best(), boosted: None, }, LocalHex { rank: 1, signal_level: SignalLevel::None, + assignment: Assignments::best(), boosted: None, }, ], @@ -359,11 +404,13 @@ mod tests { LocalHex { rank: 1, signal_level: SignalLevel::High, + assignment: Assignments::best(), boosted: None, }, LocalHex { rank: 1, signal_level: SignalLevel::Low, + assignment: Assignments::best(), boosted: None, }, ], From 86af16ff54590d117d5d285c5c5fd414b775add6 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 21 May 2024 16:47:07 -0700 Subject: [PATCH 009/115] oracle boosting assignment multiplier --- coverage_point_calculator/src/lib.rs | 115 ++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 3 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 0959d31ef..faa02de7f 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -79,6 +79,46 @@ enum Assignment { C, } +impl Assignments { + fn multiplier(&self) -> MaxOneMultplier { + let Assignments { + footfall, + urbanized, + landtype, + } = self; + + use Assignment::*; + match (footfall, landtype, urbanized) { + // yellow - POI ≥ 1 Urbanized + (A, A, A) => dec!(1.00), + (A, B, A) => dec!(1.00), + (A, C, A) => dec!(1.00), + // orange - POI ≥ 1 Not Urbanized + (A, A, B) => dec!(1.00), + (A, B, B) => dec!(1.00), + (A, C, B) => dec!(1.00), + // light green - Point of Interest Urbanized + (B, A, A) => dec!(0.70), + (B, B, A) => dec!(0.70), + (B, C, A) => dec!(0.70), + // dark green - Point of Interest Not Urbanized + (B, A, B) => dec!(0.50), + (B, B, B) => dec!(0.50), + (B, C, B) => dec!(0.50), + // light blue - No POI Urbanized + (C, A, A) => dec!(0.40), + (C, B, A) => dec!(0.30), + (C, C, A) => dec!(0.05), + // dark blue - No POI Not Urbanized + (C, A, B) => dec!(0.20), + (C, B, B) => dec!(0.15), + (C, C, B) => dec!(0.03), + // gray - Outside of USA + (_, _, C) => dec!(0.00), + } + } +} + trait Coverage { fn radio_type(&self) -> RadioType; fn signal_level(&self) -> SignalLevel; @@ -123,13 +163,15 @@ impl LocalRadio { pub fn coverage_points(&self) -> Points { let mut points = vec![]; let location_trust_score_multiplier = self.location_trust_multiplier(); + for hex in self.hexes.iter() { - let base_coverage_points = self.radio_type.coverage_points(&hex.signal_level); - let assignments_multiplier = dec!(1); let Some(rank) = self.radio_type.rank_multiplier(hex) else { - // Rank falls outside what is allowed, hex is skipped + // Rank falls outside what is allowed, skip as early as possible continue; }; + + let base_coverage_points = self.radio_type.coverage_points(&hex.signal_level); + let assignments_multiplier = hex.assignment.multiplier(); let hex_boost_multiplier = self.hex_boosting_multiplier(&hex); // https://www.notion.so/nova-labs/POC-reward-formula-7d1f62b638b5447fbfe37a11c0a3d3c8 @@ -138,6 +180,7 @@ impl LocalRadio { * rank * hex_boost_multiplier * location_trust_score_multiplier; + points.push(coverage_points) } @@ -176,6 +219,72 @@ mod tests { } } + #[test] + fn oracle_boosting_assignments_apply_per_hex() { + fn local_hex( + footfall: Assignment, + landtype: Assignment, + urbanized: Assignment, + ) -> LocalHex { + LocalHex { + rank: 1, + signal_level: SignalLevel::High, + assignment: Assignments { + footfall, + landtype, + urbanized, + }, + boosted: None, + } + } + + use Assignment::*; + let indoor_cbrs = LocalRadio { + radio_type: RadioType::IndoorCbrs, + speedtest_multiplier: Multiplier::new(1).unwrap(), + location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], + verified_radio_threshold: true, + hexes: vec![ + // yellow - POI ≥ 1 Urbanized + local_hex(A, A, A), // 100 + local_hex(A, B, A), // 100 + local_hex(A, C, A), // 100 + // orange - POI ≥ 1 Not Urbanized + local_hex(A, A, B), // 100 + local_hex(A, B, B), // 100 + local_hex(A, C, B), // 100 + // light green - Point of Interest Urbanized + local_hex(B, A, A), // 70 + local_hex(B, B, A), // 70 + local_hex(B, C, A), // 70 + // dark green - Point of Interest Not Urbanized + local_hex(B, A, B), // 50 + local_hex(B, B, B), // 50 + local_hex(B, C, B), // 50 + // light blue - No POI Urbanized + local_hex(C, A, A), // 40 + local_hex(C, B, A), // 30 + local_hex(C, C, A), // 5 + // dark blue - No POI Not Urbanized + local_hex(C, A, B), // 20 + local_hex(C, B, B), // 15 + local_hex(C, C, B), // 3 + // gray - Outside of USA + local_hex(A, A, C), // 0 + local_hex(A, B, C), // 0 + local_hex(A, C, C), // 0 + local_hex(B, A, C), // 0 + local_hex(B, B, C), // 0 + local_hex(B, C, C), // 0 + local_hex(C, A, C), // 0 + local_hex(C, B, C), // 0 + local_hex(C, C, C), // 0 + ], + }; + + assert_eq!(dec!(1073), indoor_cbrs.coverage_points()); + } + #[test] fn outdoor_radios_consider_top_3_ranked_hexes() { let outdoor_wifi = LocalRadio { From bd868f3e8d103b0c3c245c7ff9bbafd38e694a72 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 21 May 2024 17:05:19 -0700 Subject: [PATCH 010/115] Comment which HIPs have impacted coverage_points calculation by field --- coverage_point_calculator/src/lib.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index faa02de7f..97f407cca 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -1,5 +1,27 @@ #![allow(unused)] - +/// +/// The coverage_points calculation in [`LocalRadio.coverage_points()`] are +/// comprised 5 top level fields. +/// +/// - base_coverage_points +/// - [HIP-74][modeled-coverage] +/// - [HIP-113][cbrs-experimental] +/// - assignment_multiplier +/// - [HIP-103][oracle-boosting] +/// - rank +/// - [HIP-105][hex-limits] +/// - hex_boost_multiplier +/// - [HIP-84][provider-boosting] +/// - location_trust_score_multiplier +/// - [HIP-98][qos-score] +/// +/// [modeled-coverage]: https://github.com/helium/HIP/blob/main/0074-mobile-poc-modeled-coverage-rewards.md#outdoor-radios +/// [cbrs-experimental]: https://github.com/helium/HIP/blob/main/0113-reward-cbrs-as-experimental.md +/// [oracle-boosting]: https://github.com/helium/HIP/blob/main/0103-oracle-hex-boosting.md +/// [hex-limits]: https://github.com/helium/HIP/blob/main/0105-modification-of-mobile-subdao-hex-limits.md +/// [provider-boosting]: https://github.com/helium/HIP/blob/main/0084-service-provider-hex-boosting.md#mechanics-and-price-of-boosting-hexes +/// [qos-score]: https://github.com/helium/HIP/blob/main/0098-mobile-subdao-quality-of-service-requirements.md +/// use hextree::Cell; use rust_decimal::Decimal; use rust_decimal_macros::dec; @@ -174,7 +196,6 @@ impl LocalRadio { let assignments_multiplier = hex.assignment.multiplier(); let hex_boost_multiplier = self.hex_boosting_multiplier(&hex); - // https://www.notion.so/nova-labs/POC-reward-formula-7d1f62b638b5447fbfe37a11c0a3d3c8 let coverage_points = base_coverage_points * assignments_multiplier * rank From ce66053c4d4e4906fd9843aa68f8b3542ba811d8 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 21 May 2024 17:17:13 -0700 Subject: [PATCH 011/115] use same verbiage as modeled coverage hip --- coverage_point_calculator/src/lib.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 97f407cca..4a2fab7d5 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -3,7 +3,7 @@ /// The coverage_points calculation in [`LocalRadio.coverage_points()`] are /// comprised 5 top level fields. /// -/// - base_coverage_points +/// - estimated_coverage_points /// - [HIP-74][modeled-coverage] /// - [HIP-113][cbrs-experimental] /// - assignment_multiplier @@ -39,7 +39,7 @@ enum RadioType { } impl RadioType { - fn coverage_points(&self, signal_level: &SignalLevel) -> Points { + fn estimated_coverage_points(&self, signal_level: &SignalLevel) -> Points { match self { RadioType::IndoorWifi => match signal_level { SignalLevel::High => dec!(400), @@ -192,11 +192,12 @@ impl LocalRadio { continue; }; - let base_coverage_points = self.radio_type.coverage_points(&hex.signal_level); + let estimated_coverage_points = + self.radio_type.estimated_coverage_points(&hex.signal_level); let assignments_multiplier = hex.assignment.multiplier(); let hex_boost_multiplier = self.hex_boosting_multiplier(&hex); - let coverage_points = base_coverage_points + let coverage_points = estimated_coverage_points * assignments_multiplier * rank * hex_boost_multiplier From ac5576376b9121d7b5b15194ad001a421db46c56 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 22 May 2024 17:21:45 -0700 Subject: [PATCH 012/115] Speedtest mutliplier the speedtest multiplier is allowed to be zero. It can completely negate a radios rewards. speedtest 2 --- coverage_point_calculator/src/lib.rs | 78 +++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 12 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 4a2fab7d5..5f7819184 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -14,6 +14,8 @@ /// - [HIP-84][provider-boosting] /// - location_trust_score_multiplier /// - [HIP-98][qos-score] +/// - speedtest_multiplier +/// - [HIP-74][modeled-coverage] /// /// [modeled-coverage]: https://github.com/helium/HIP/blob/main/0074-mobile-poc-modeled-coverage-rewards.md#outdoor-radios /// [cbrs-experimental]: https://github.com/helium/HIP/blob/main/0113-reward-cbrs-as-experimental.md @@ -141,6 +143,27 @@ impl Assignments { } } +#[derive(Debug, Clone, PartialEq)] +enum Speedtest { + Good, + Acceptable, + Degraded, + Poor, + Fail, +} + +impl Speedtest { + fn multiplier(&self) -> MaxOneMultplier { + match self { + Speedtest::Good => dec!(1.00), + Speedtest::Acceptable => dec!(0.75), + Speedtest::Degraded => dec!(0.50), + Speedtest::Poor => dec!(0.25), + Speedtest::Fail => dec!(0), + } + } +} + trait Coverage { fn radio_type(&self) -> RadioType; fn signal_level(&self) -> SignalLevel; @@ -153,14 +176,14 @@ trait CoverageMap { trait RewardableRadio { fn hex(&self) -> Cell; fn radio_type(&self) -> RadioType; - fn location_trust_scores(&self) -> Vec; + fn location_trust_scores(&self) -> Vec; fn verified_radio_threshold(&self) -> bool; } #[derive(Debug, PartialEq)] struct LocalRadio { radio_type: RadioType, - speedtest_multiplier: Multiplier, + speedtest: Speedtest, location_trust_scores: Vec, verified_radio_threshold: bool, hexes: Vec, @@ -206,7 +229,9 @@ impl LocalRadio { points.push(coverage_points) } - points.iter().sum::().round_dp(2) + let mut coverage_points = points.iter().sum::(); + coverage_points *= self.speedtest.multiplier(); + coverage_points.round_dp(2) } fn location_trust_multiplier(&self) -> Decimal { @@ -241,6 +266,35 @@ mod tests { } } + #[test] + fn speedtest() { + let mut indoor_cbrs = LocalRadio { + radio_type: RadioType::IndoorCbrs, + speedtest: Speedtest::Good, + location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], + verified_radio_threshold: true, + hexes: vec![LocalHex { + rank: 1, + signal_level: SignalLevel::High, + assignment: Assignments::best(), + boosted: None, + }], + }; + assert_eq!(dec!(100), indoor_cbrs.coverage_points()); + + indoor_cbrs.speedtest = Speedtest::Acceptable; + assert_eq!(dec!(75), indoor_cbrs.coverage_points()); + + indoor_cbrs.speedtest = Speedtest::Degraded; + assert_eq!(dec!(50), indoor_cbrs.coverage_points()); + + indoor_cbrs.speedtest = Speedtest::Poor; + assert_eq!(dec!(25), indoor_cbrs.coverage_points()); + + indoor_cbrs.speedtest = Speedtest::Fail; + assert_eq!(dec!(0), indoor_cbrs.coverage_points()); + } + #[test] fn oracle_boosting_assignments_apply_per_hex() { fn local_hex( @@ -263,7 +317,7 @@ mod tests { use Assignment::*; let indoor_cbrs = LocalRadio { radio_type: RadioType::IndoorCbrs, - speedtest_multiplier: Multiplier::new(1).unwrap(), + speedtest: Speedtest::Good, location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ @@ -311,7 +365,7 @@ mod tests { fn outdoor_radios_consider_top_3_ranked_hexes() { let outdoor_wifi = LocalRadio { radio_type: RadioType::OutdoorWifi, - speedtest_multiplier: Multiplier::new(1).unwrap(), + speedtest: Speedtest::Good, location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ @@ -353,7 +407,7 @@ mod tests { fn indoor_radios_only_consider_first_ranked_hexes() { let indoor_wifi = LocalRadio { radio_type: RadioType::IndoorWifi, - speedtest_multiplier: Multiplier::new(1).unwrap(), + speedtest: Speedtest::Good, location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ @@ -386,7 +440,7 @@ mod tests { // Location scores are averaged together let indoor_wifi = LocalRadio { radio_type: RadioType::IndoorWifi, - speedtest_multiplier: Multiplier::new(1).unwrap(), + speedtest: Speedtest::Good, location_trust_scores: vec![ MaxOneMultplier::from_f32_retain(0.1).unwrap(), MaxOneMultplier::from_f32_retain(0.2).unwrap(), @@ -410,7 +464,7 @@ mod tests { fn boosted_hex() { let mut indoor_wifi = LocalRadio { radio_type: RadioType::IndoorWifi, - speedtest_multiplier: Multiplier::new(1).unwrap(), + speedtest: Speedtest::Good, location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ @@ -441,7 +495,7 @@ mod tests { fn base_radio_coverage_points() { let outdoor_cbrs = LocalRadio { radio_type: RadioType::OutdoorCbrs, - speedtest_multiplier: Multiplier::new(1).unwrap(), + speedtest: Speedtest::Good, location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ @@ -474,7 +528,7 @@ mod tests { let indoor_cbrs = LocalRadio { radio_type: RadioType::IndoorCbrs, - speedtest_multiplier: Multiplier::new(1).unwrap(), + speedtest: Speedtest::Good, location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ @@ -495,7 +549,7 @@ mod tests { let outdoor_wifi = LocalRadio { radio_type: RadioType::OutdoorWifi, - speedtest_multiplier: Multiplier::new(1).unwrap(), + speedtest: Speedtest::Good, location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ @@ -528,7 +582,7 @@ mod tests { let indoor_wifi = LocalRadio { radio_type: RadioType::IndoorWifi, - speedtest_multiplier: Multiplier::new(1).unwrap(), + speedtest: Speedtest::Good, location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ From c63cf03aedcdb9d8e1b4760826acebce651d777a Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 22 May 2024 17:22:38 -0700 Subject: [PATCH 013/115] reformat docs --- coverage_point_calculator/src/lib.rs | 30 ++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 5f7819184..02ccba179 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -1,11 +1,16 @@ #![allow(unused)] /// +/// Many changes to the rewards algorithm are contained in and across many HIPs. +/// The blog post [MOBILE Proof of Coverage][mobile-poc-blog] contains a more +/// thorough explanation of many of them. It is not exhaustive, but a great +/// place to start. +/// /// The coverage_points calculation in [`LocalRadio.coverage_points()`] are -/// comprised 5 top level fields. +/// comprised the following fields. /// /// - estimated_coverage_points /// - [HIP-74][modeled-coverage] -/// - [HIP-113][cbrs-experimental] +/// - reduced cbrs radio coverage points [HIP-113][cbrs-experimental] /// - assignment_multiplier /// - [HIP-103][oracle-boosting] /// - rank @@ -16,13 +21,22 @@ /// - [HIP-98][qos-score] /// - speedtest_multiplier /// - [HIP-74][modeled-coverage] +/// - added "Good" speedtest tier [HIP-98][qos-score] /// -/// [modeled-coverage]: https://github.com/helium/HIP/blob/main/0074-mobile-poc-modeled-coverage-rewards.md#outdoor-radios -/// [cbrs-experimental]: https://github.com/helium/HIP/blob/main/0113-reward-cbrs-as-experimental.md -/// [oracle-boosting]: https://github.com/helium/HIP/blob/main/0103-oracle-hex-boosting.md -/// [hex-limits]: https://github.com/helium/HIP/blob/main/0105-modification-of-mobile-subdao-hex-limits.md -/// [provider-boosting]: https://github.com/helium/HIP/blob/main/0084-service-provider-hex-boosting.md#mechanics-and-price-of-boosting-hexes -/// [qos-score]: https://github.com/helium/HIP/blob/main/0098-mobile-subdao-quality-of-service-requirements.md +/// [modeled-coverage]: +/// https://github.com/helium/HIP/blob/main/0074-mobile-poc-modeled-coverage-rewards.md#outdoor-radios +/// [cbrs-experimental]: +/// https://github.com/helium/HIP/blob/main/0113-reward-cbrs-as-experimental.md +/// [oracle-boosting]: +/// https://github.com/helium/HIP/blob/main/0103-oracle-hex-boosting.md +/// [hex-limits]: +/// https://github.com/helium/HIP/blob/main/0105-modification-of-mobile-subdao-hex-limits.md +/// [provider-boosting]: +/// https://github.com/helium/HIP/blob/main/0084-service-provider-hex-boosting.md#mechanics-and-price-of-boosting-hexes +/// [qos-score]: +/// https://github.com/helium/HIP/blob/main/0098-mobile-subdao-quality-of-service-requirements.md +/// [mobile-poc-blog]: +/// https://docs.helium.com/mobile/proof-of-coverage /// use hextree::Cell; use rust_decimal::Decimal; From 901dae774e6adf79ed0a36b6913fe7d6cc0c013c Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 22 May 2024 17:26:33 -0700 Subject: [PATCH 014/115] remove to let use drive the interface --- coverage_point_calculator/src/lib.rs | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 02ccba179..dabe6bb9a 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -178,22 +178,6 @@ impl Speedtest { } } -trait Coverage { - fn radio_type(&self) -> RadioType; - fn signal_level(&self) -> SignalLevel; -} - -trait CoverageMap { - fn get(&self, cell: Cell) -> Vec; -} - -trait RewardableRadio { - fn hex(&self) -> Cell; - fn radio_type(&self) -> RadioType; - fn location_trust_scores(&self) -> Vec; - fn verified_radio_threshold(&self) -> bool; -} - #[derive(Debug, PartialEq)] struct LocalRadio { radio_type: RadioType, @@ -211,13 +195,6 @@ struct LocalHex { boosted: Option, } -fn calculate( - radio: impl RewardableRadio, - coverage_map: impl CoverageMap, -) -> LocalRadio { - todo!() -} - impl LocalRadio { pub fn coverage_points(&self) -> Points { let mut points = vec![]; From f9fe0949aaa40336a02b877619330b62744d0e0d Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 22 May 2024 17:32:39 -0700 Subject: [PATCH 015/115] LocalRadio -> RewardableRadio it seems a more accurate name for where we are currently, I think --- coverage_point_calculator/src/lib.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index dabe6bb9a..b82d34427 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -179,7 +179,7 @@ impl Speedtest { } #[derive(Debug, PartialEq)] -struct LocalRadio { +struct RewardableRadio { radio_type: RadioType, speedtest: Speedtest, location_trust_scores: Vec, @@ -195,7 +195,7 @@ struct LocalHex { boosted: Option, } -impl LocalRadio { +impl RewardableRadio { pub fn coverage_points(&self) -> Points { let mut points = vec![]; let location_trust_score_multiplier = self.location_trust_multiplier(); @@ -259,7 +259,7 @@ mod tests { #[test] fn speedtest() { - let mut indoor_cbrs = LocalRadio { + let mut indoor_cbrs = RewardableRadio { radio_type: RadioType::IndoorCbrs, speedtest: Speedtest::Good, location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], @@ -306,7 +306,7 @@ mod tests { } use Assignment::*; - let indoor_cbrs = LocalRadio { + let indoor_cbrs = RewardableRadio { radio_type: RadioType::IndoorCbrs, speedtest: Speedtest::Good, location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], @@ -354,7 +354,7 @@ mod tests { #[test] fn outdoor_radios_consider_top_3_ranked_hexes() { - let outdoor_wifi = LocalRadio { + let outdoor_wifi = RewardableRadio { radio_type: RadioType::OutdoorWifi, speedtest: Speedtest::Good, location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], @@ -396,7 +396,7 @@ mod tests { #[test] fn indoor_radios_only_consider_first_ranked_hexes() { - let indoor_wifi = LocalRadio { + let indoor_wifi = RewardableRadio { radio_type: RadioType::IndoorWifi, speedtest: Speedtest::Good, location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], @@ -429,7 +429,7 @@ mod tests { #[test] fn location_trust_score_multiplier() { // Location scores are averaged together - let indoor_wifi = LocalRadio { + let indoor_wifi = RewardableRadio { radio_type: RadioType::IndoorWifi, speedtest: Speedtest::Good, location_trust_scores: vec![ @@ -453,7 +453,7 @@ mod tests { #[test] fn boosted_hex() { - let mut indoor_wifi = LocalRadio { + let mut indoor_wifi = RewardableRadio { radio_type: RadioType::IndoorWifi, speedtest: Speedtest::Good, location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], @@ -484,7 +484,7 @@ mod tests { #[test] fn base_radio_coverage_points() { - let outdoor_cbrs = LocalRadio { + let outdoor_cbrs = RewardableRadio { radio_type: RadioType::OutdoorCbrs, speedtest: Speedtest::Good, location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], @@ -517,7 +517,7 @@ mod tests { ], }; - let indoor_cbrs = LocalRadio { + let indoor_cbrs = RewardableRadio { radio_type: RadioType::IndoorCbrs, speedtest: Speedtest::Good, location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], @@ -538,7 +538,7 @@ mod tests { ], }; - let outdoor_wifi = LocalRadio { + let outdoor_wifi = RewardableRadio { radio_type: RadioType::OutdoorWifi, speedtest: Speedtest::Good, location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], @@ -571,7 +571,7 @@ mod tests { ], }; - let indoor_wifi = LocalRadio { + let indoor_wifi = RewardableRadio { radio_type: RadioType::IndoorWifi, speedtest: Speedtest::Good, location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], From 1a02c2ffa0293510f6c8f1935ef840bc41ddb092 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 23 May 2024 09:49:22 -0700 Subject: [PATCH 016/115] integration tests to start messing with the API --- coverage_point_calculator/src/lib.rs | 157 +++++++++++------- .../tests/coverage_point_calculator.rs | 136 +++++++++++++++ 2 files changed, 230 insertions(+), 63 deletions(-) create mode 100644 coverage_point_calculator/tests/coverage_point_calculator.rs diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index b82d34427..302f538f7 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -39,15 +39,45 @@ /// https://docs.helium.com/mobile/proof-of-coverage /// use hextree::Cell; -use rust_decimal::Decimal; +use rust_decimal::{Decimal, RoundingStrategy}; use rust_decimal_macros::dec; type Multiplier = std::num::NonZeroU32; -type MaxOneMultplier = Decimal; +pub type MaxOneMultplier = Decimal; type Points = Decimal; -#[derive(Debug, Clone, PartialEq)] -enum RadioType { +pub trait Radio { + fn radio_type(&self) -> RadioType; + fn speedtest(&self) -> Speedtest; + fn location_trust_scores(&self) -> Vec; + fn verified_radio_threshold(&self) -> bool; +} + +pub trait CoverageMap { + fn hexes(&self, radio: &impl Radio) -> Vec; +} + +pub fn calculate<'a>( + radios: &'a [impl Radio], + coverage_map: &'a impl CoverageMap, +) -> impl Iterator + 'a { + radios + .iter() + .map(|radio| calculate_single(radio, coverage_map)) +} + +pub fn calculate_single(radio: &impl Radio, coverage_map: &impl CoverageMap) -> RewardableRadio { + RewardableRadio { + radio_type: radio.radio_type(), + speedtest: radio.speedtest(), + location_trust_scores: radio.location_trust_scores(), + verified_radio_threshold: radio.verified_radio_threshold(), + hexes: coverage_map.hexes(radio), + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum RadioType { IndoorWifi, OutdoorWifi, IndoorCbrs, @@ -82,7 +112,7 @@ impl RadioType { } } - fn rank_multiplier(&self, hex: &LocalHex) -> Option { + fn rank_multiplier(&self, hex: &CoveredHex) -> Option { let multipliers = match self { RadioType::IndoorWifi => vec![dec!(1)], RadioType::IndoorCbrs => vec![dec!(1)], @@ -95,8 +125,8 @@ impl RadioType { } } -#[derive(Debug, Clone, PartialEq)] -enum SignalLevel { +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SignalLevel { High, Medium, Low, @@ -104,14 +134,14 @@ enum SignalLevel { } #[derive(Debug, Clone, PartialEq)] -struct Assignments { - footfall: Assignment, - landtype: Assignment, - urbanized: Assignment, +pub struct Assignments { + pub footfall: Assignment, + pub landtype: Assignment, + pub urbanized: Assignment, } #[derive(Debug, Clone, PartialEq)] -enum Assignment { +pub enum Assignment { A, B, C, @@ -157,8 +187,8 @@ impl Assignments { } } -#[derive(Debug, Clone, PartialEq)] -enum Speedtest { +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Speedtest { Good, Acceptable, Degraded, @@ -179,50 +209,51 @@ impl Speedtest { } #[derive(Debug, PartialEq)] -struct RewardableRadio { - radio_type: RadioType, - speedtest: Speedtest, - location_trust_scores: Vec, - verified_radio_threshold: bool, - hexes: Vec, +pub struct RewardableRadio { + pub radio_type: RadioType, + pub speedtest: Speedtest, + pub location_trust_scores: Vec, + pub verified_radio_threshold: bool, + pub hexes: Vec, } -#[derive(Debug, PartialEq)] -struct LocalHex { - rank: usize, - signal_level: SignalLevel, - assignment: Assignments, - boosted: Option, +#[derive(Debug, Clone, PartialEq)] +pub struct CoveredHex { + pub rank: usize, + pub signal_level: SignalLevel, + pub assignment: Assignments, + pub boosted: Option, } impl RewardableRadio { pub fn coverage_points(&self) -> Points { let mut points = vec![]; - let location_trust_score_multiplier = self.location_trust_multiplier(); + let radio_type = &self.radio_type; for hex in self.hexes.iter() { - let Some(rank) = self.radio_type.rank_multiplier(hex) else { + let Some(rank_multiplier) = radio_type.rank_multiplier(hex) else { // Rank falls outside what is allowed, skip as early as possible continue; }; - let estimated_coverage_points = - self.radio_type.estimated_coverage_points(&hex.signal_level); + let estimated_coverage_points = radio_type.estimated_coverage_points(&hex.signal_level); let assignments_multiplier = hex.assignment.multiplier(); - let hex_boost_multiplier = self.hex_boosting_multiplier(&hex); + let hex_boost_multiplier = self.hex_boosting_multiplier(hex); let coverage_points = estimated_coverage_points * assignments_multiplier - * rank - * hex_boost_multiplier - * location_trust_score_multiplier; + * rank_multiplier + * hex_boost_multiplier; points.push(coverage_points) } - let mut coverage_points = points.iter().sum::(); - coverage_points *= self.speedtest.multiplier(); - coverage_points.round_dp(2) + let base_points = points.iter().sum::(); + let location_score = self.location_trust_multiplier(); + let speedtest = self.speedtest.multiplier(); + + let coverage_points = base_points * location_score * speedtest; + coverage_points.round_dp_with_strategy(2, RoundingStrategy::ToZero) } fn location_trust_multiplier(&self) -> Decimal { @@ -231,7 +262,7 @@ impl RewardableRadio { trust_score_sum / trust_score_count } - fn hex_boosting_multiplier(&self, hex: &LocalHex) -> MaxOneMultplier { + fn hex_boosting_multiplier(&self, hex: &CoveredHex) -> MaxOneMultplier { let maybe_boost = if self.verified_radio_threshold { hex.boosted.map_or(1, |boost| boost.get()) } else { @@ -264,7 +295,7 @@ mod tests { speedtest: Speedtest::Good, location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, - hexes: vec![LocalHex { + hexes: vec![CoveredHex { rank: 1, signal_level: SignalLevel::High, assignment: Assignments::best(), @@ -292,8 +323,8 @@ mod tests { footfall: Assignment, landtype: Assignment, urbanized: Assignment, - ) -> LocalHex { - LocalHex { + ) -> CoveredHex { + CoveredHex { rank: 1, signal_level: SignalLevel::High, assignment: Assignments { @@ -360,25 +391,25 @@ mod tests { location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ - LocalHex { + CoveredHex { rank: 1, signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, }, - LocalHex { + CoveredHex { rank: 2, signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, }, - LocalHex { + CoveredHex { rank: 3, signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, }, - LocalHex { + CoveredHex { rank: 42, signal_level: SignalLevel::High, assignment: Assignments::best(), @@ -402,19 +433,19 @@ mod tests { location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ - LocalHex { + CoveredHex { rank: 1, signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, }, - LocalHex { + CoveredHex { rank: 2, signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, }, - LocalHex { + CoveredHex { rank: 42, signal_level: SignalLevel::High, assignment: Assignments::best(), @@ -439,7 +470,7 @@ mod tests { MaxOneMultplier::from_f32_retain(0.4).unwrap(), ], verified_radio_threshold: true, - hexes: vec![LocalHex { + hexes: vec![CoveredHex { rank: 1, signal_level: SignalLevel::High, assignment: Assignments::best(), @@ -459,13 +490,13 @@ mod tests { location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ - LocalHex { + CoveredHex { rank: 1, signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, }, - LocalHex { + CoveredHex { rank: 1, signal_level: SignalLevel::Low, assignment: Assignments::best(), @@ -490,25 +521,25 @@ mod tests { location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ - LocalHex { + CoveredHex { rank: 1, signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, }, - LocalHex { + CoveredHex { rank: 1, signal_level: SignalLevel::Medium, assignment: Assignments::best(), boosted: None, }, - LocalHex { + CoveredHex { rank: 1, signal_level: SignalLevel::Low, assignment: Assignments::best(), boosted: None, }, - LocalHex { + CoveredHex { rank: 1, signal_level: SignalLevel::None, assignment: Assignments::best(), @@ -523,13 +554,13 @@ mod tests { location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ - LocalHex { + CoveredHex { rank: 1, signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, }, - LocalHex { + CoveredHex { rank: 1, signal_level: SignalLevel::Low, assignment: Assignments::best(), @@ -544,25 +575,25 @@ mod tests { location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ - LocalHex { + CoveredHex { rank: 1, signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, }, - LocalHex { + CoveredHex { rank: 1, signal_level: SignalLevel::Medium, assignment: Assignments::best(), boosted: None, }, - LocalHex { + CoveredHex { rank: 1, signal_level: SignalLevel::Low, assignment: Assignments::best(), boosted: None, }, - LocalHex { + CoveredHex { rank: 1, signal_level: SignalLevel::None, assignment: Assignments::best(), @@ -577,13 +608,13 @@ mod tests { location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ - LocalHex { + CoveredHex { rank: 1, signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, }, - LocalHex { + CoveredHex { rank: 1, signal_level: SignalLevel::Low, assignment: Assignments::best(), diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs new file mode 100644 index 000000000..2565a7193 --- /dev/null +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -0,0 +1,136 @@ +use std::{collections::HashMap, num::NonZeroU32}; + +use coverage_point_calculator::{ + calculate, calculate_single, Assignment, Assignments, CoverageMap, CoveredHex, MaxOneMultplier, + Radio, RadioType, SignalLevel, Speedtest, +}; +use rust_decimal_macros::dec; + +#[test] +fn base_radio_coverage_points() { + struct TestRadio(RadioType); + struct TestCoverageMap; + + impl Radio for TestRadio { + fn radio_type(&self) -> RadioType { + self.0 + } + + fn speedtest(&self) -> Speedtest { + Speedtest::Good + } + + fn location_trust_scores(&self) -> Vec { + vec![dec!(1)] + } + + fn verified_radio_threshold(&self) -> bool { + true + } + } + + impl CoverageMap for TestCoverageMap { + fn hexes(&self, _radio: &impl Radio) -> Vec { + vec![CoveredHex { + rank: 1, + signal_level: SignalLevel::High, + assignment: Assignments { + footfall: Assignment::A, + landtype: Assignment::A, + urbanized: Assignment::A, + }, + boosted: NonZeroU32::new(0), + }] + } + } + + for radio_type in [ + RadioType::IndoorWifi, + RadioType::IndoorCbrs, + RadioType::OutdoorWifi, + RadioType::OutdoorCbrs, + ] { + let radio = calculate_single(&TestRadio(radio_type), &TestCoverageMap); + println!("{radio_type:?} \t--> {}", radio.coverage_points()); + } + + let radios = vec![ + TestRadio(RadioType::IndoorWifi), + TestRadio(RadioType::IndoorCbrs), + TestRadio(RadioType::OutdoorWifi), + TestRadio(RadioType::OutdoorCbrs), + ]; + let output = calculate(&radios, &TestCoverageMap) + .map(|r| (r.radio_type, r.coverage_points())) + .collect::>(); + println!("{output:#?}"); +} + +#[test] +fn radio_unique_coverage() { + struct TestRadio(RadioType); + + let radios = vec![ + TestRadio(RadioType::IndoorWifi), + TestRadio(RadioType::IndoorCbrs), + TestRadio(RadioType::OutdoorWifi), + TestRadio(RadioType::OutdoorCbrs), + ]; + + impl Radio for TestRadio { + fn radio_type(&self) -> RadioType { + self.0 + } + + fn speedtest(&self) -> Speedtest { + Speedtest::Good + } + + fn location_trust_scores(&self) -> Vec { + vec![dec!(1)] + } + + fn verified_radio_threshold(&self) -> bool { + true + } + } + + // all radios will receive 400 coverage points + let base_hex = CoveredHex { + rank: 1, + signal_level: SignalLevel::High, + assignment: Assignments { + footfall: Assignment::A, + landtype: Assignment::A, + urbanized: Assignment::A, + }, + boosted: NonZeroU32::new(0), + }; + let hex = std::iter::repeat(base_hex); + + let mut map = HashMap::new(); + map.insert("indoor_wifi", hex.clone().take(1).collect()); + map.insert("indoor_cbrs", hex.clone().take(4).collect()); + map.insert("outdoor_wifi", hex.clone().take(25).collect()); + map.insert("outdoor_cbrs", hex.clone().take(100).collect()); + + struct TestCoverageMap<'a>(HashMap<&'a str, Vec>); + let coverage_map = TestCoverageMap(map); + + impl CoverageMap for TestCoverageMap<'_> { + fn hexes(&self, radio: &impl Radio) -> Vec { + let key = match radio.radio_type() { + RadioType::IndoorWifi => "indoor_wifi", + RadioType::OutdoorWifi => "outdoor_wifi", + RadioType::IndoorCbrs => "indoor_cbrs", + RadioType::OutdoorCbrs => "outdoor_cbrs", + }; + self.0.get(key).unwrap().clone() + } + } + + let coverage_points = calculate(&radios, &coverage_map) + .map(|r| (r.radio_type, r.coverage_points())) + .collect::>(); + println!("{coverage_points:#?}") +} From 526a9d640aa4e6214f9da55d1c69f948e798f5d1 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 23 May 2024 10:32:31 -0700 Subject: [PATCH 017/115] Make a full CoverageRewards struct that contains the points --- coverage_point_calculator/src/lib.rs | 81 ++++++++++++------- .../tests/coverage_point_calculator.rs | 9 ++- 2 files changed, 58 insertions(+), 32 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 302f538f7..f969d13aa 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -208,7 +208,13 @@ impl Speedtest { } } -#[derive(Debug, PartialEq)] +#[derive(Debug)] +pub struct CoveragePoints { + pub coverage_points: Decimal, + pub radio: RewardableRadio, +} + +#[derive(Debug, Clone, PartialEq)] pub struct RewardableRadio { pub radio_type: RadioType, pub speedtest: Speedtest, @@ -226,34 +232,36 @@ pub struct CoveredHex { } impl RewardableRadio { - pub fn coverage_points(&self) -> Points { - let mut points = vec![]; + pub fn to_coverage_points(self) -> CoveragePoints { let radio_type = &self.radio_type; - for hex in self.hexes.iter() { + let hex_points = self.hexes.iter().filter_map(|hex| { let Some(rank_multiplier) = radio_type.rank_multiplier(hex) else { // Rank falls outside what is allowed, skip as early as possible - continue; + return None; }; let estimated_coverage_points = radio_type.estimated_coverage_points(&hex.signal_level); let assignments_multiplier = hex.assignment.multiplier(); let hex_boost_multiplier = self.hex_boosting_multiplier(hex); - let coverage_points = estimated_coverage_points - * assignments_multiplier - * rank_multiplier - * hex_boost_multiplier; + Some( + estimated_coverage_points + * assignments_multiplier + * rank_multiplier + * hex_boost_multiplier, + ) + }); - points.push(coverage_points) - } - - let base_points = points.iter().sum::(); + let base_points = hex_points.sum::(); let location_score = self.location_trust_multiplier(); let speedtest = self.speedtest.multiplier(); let coverage_points = base_points * location_score * speedtest; - coverage_points.round_dp_with_strategy(2, RoundingStrategy::ToZero) + CoveragePoints { + coverage_points: coverage_points.round_dp_with_strategy(2, RoundingStrategy::ToZero), + radio: self, + } } fn location_trust_multiplier(&self) -> Decimal { @@ -302,19 +310,31 @@ mod tests { boosted: None, }], }; - assert_eq!(dec!(100), indoor_cbrs.coverage_points()); + assert_eq!( + dec!(100), + indoor_cbrs.clone().to_coverage_points().coverage_points + ); indoor_cbrs.speedtest = Speedtest::Acceptable; - assert_eq!(dec!(75), indoor_cbrs.coverage_points()); + assert_eq!( + dec!(75), + indoor_cbrs.clone().to_coverage_points().coverage_points + ); indoor_cbrs.speedtest = Speedtest::Degraded; - assert_eq!(dec!(50), indoor_cbrs.coverage_points()); + assert_eq!( + dec!(50), + indoor_cbrs.clone().to_coverage_points().coverage_points + ); indoor_cbrs.speedtest = Speedtest::Poor; - assert_eq!(dec!(25), indoor_cbrs.coverage_points()); + assert_eq!( + dec!(25), + indoor_cbrs.clone().to_coverage_points().coverage_points + ); indoor_cbrs.speedtest = Speedtest::Fail; - assert_eq!(dec!(0), indoor_cbrs.coverage_points()); + assert_eq!(dec!(0), indoor_cbrs.to_coverage_points().coverage_points); } #[test] @@ -380,7 +400,7 @@ mod tests { ], }; - assert_eq!(dec!(1073), indoor_cbrs.coverage_points()); + assert_eq!(dec!(1073), indoor_cbrs.to_coverage_points().coverage_points); } #[test] @@ -422,7 +442,7 @@ mod tests { // rank 2 :: 0.50 * 16 == 8 // rank 3 :: 0.25 * 16 == 4 // rank 42 :: 0.00 * 16 == 0 - assert_eq!(dec!(28), outdoor_wifi.coverage_points()); + assert_eq!(dec!(28), outdoor_wifi.to_coverage_points().coverage_points); } #[test] @@ -454,7 +474,7 @@ mod tests { ], }; - assert_eq!(dec!(400), indoor_wifi.coverage_points()); + assert_eq!(dec!(400), indoor_wifi.to_coverage_points().coverage_points); } #[test] @@ -479,7 +499,7 @@ mod tests { }; // Location trust scores is 1/4 - assert_eq!(dec!(100), indoor_wifi.coverage_points()); + assert_eq!(dec!(100), indoor_wifi.to_coverage_points().coverage_points); } #[test] @@ -506,11 +526,14 @@ mod tests { }; // The hex with a low signal_level is boosted to the same level as a // signal_level of High. - assert_eq!(dec!(800), indoor_wifi.coverage_points()); + assert_eq!( + dec!(800), + indoor_wifi.clone().to_coverage_points().coverage_points + ); // When the radio is not verified for boosted rewards, the boost has no effect. indoor_wifi.verified_radio_threshold = false; - assert_eq!(dec!(500), indoor_wifi.coverage_points()); + assert_eq!(dec!(500), indoor_wifi.to_coverage_points().coverage_points); } #[test] @@ -625,9 +648,9 @@ mod tests { // When each radio contains a hex of every applicable signal_level, and // multipliers are break even. These are the accumulated coverage points. - assert_eq!(dec!(7), outdoor_cbrs.coverage_points()); - assert_eq!(dec!(125), indoor_cbrs.coverage_points()); - assert_eq!(dec!(28), outdoor_wifi.coverage_points()); - assert_eq!(dec!(500), indoor_wifi.coverage_points()); + assert_eq!(dec!(7), outdoor_cbrs.to_coverage_points().coverage_points); + assert_eq!(dec!(125), indoor_cbrs.to_coverage_points().coverage_points); + assert_eq!(dec!(28), outdoor_wifi.to_coverage_points().coverage_points); + assert_eq!(dec!(500), indoor_wifi.to_coverage_points().coverage_points); } } diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index 2565a7193..295cf4df1 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -51,7 +51,10 @@ fn base_radio_coverage_points() { RadioType::OutdoorCbrs, ] { let radio = calculate_single(&TestRadio(radio_type), &TestCoverageMap); - println!("{radio_type:?} \t--> {}", radio.coverage_points()); + println!( + "{radio_type:?} \t--> {}", + radio.to_coverage_points().coverage_points + ); } let radios = vec![ @@ -61,7 +64,7 @@ fn base_radio_coverage_points() { TestRadio(RadioType::OutdoorCbrs), ]; let output = calculate(&radios, &TestCoverageMap) - .map(|r| (r.radio_type, r.coverage_points())) + .map(|r| (r.radio_type, r.to_coverage_points().coverage_points)) .collect::>(); println!("{output:#?}"); } @@ -130,7 +133,7 @@ fn radio_unique_coverage() { } let coverage_points = calculate(&radios, &coverage_map) - .map(|r| (r.radio_type, r.coverage_points())) + .map(|r| (r.radio_type, r.to_coverage_points().coverage_points)) .collect::>(); println!("{coverage_points:#?}") } From febe516d4f7149d6b45f1c6ffa56884425b9ecd2 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 23 May 2024 16:37:46 -0700 Subject: [PATCH 018/115] Make Rank 1-indexed For the way we talk about Ranking of radios, it doesn't make much sense for a radio to be ranked 0th for a hex. --- coverage_point_calculator/src/lib.rs | 54 +++++++++---------- .../tests/coverage_point_calculator.rs | 6 +-- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index f969d13aa..6fdfbe415 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -42,6 +42,7 @@ use hextree::Cell; use rust_decimal::{Decimal, RoundingStrategy}; use rust_decimal_macros::dec; +pub type Rank = std::num::NonZeroUsize; type Multiplier = std::num::NonZeroU32; pub type MaxOneMultplier = Decimal; type Points = Decimal; @@ -120,8 +121,7 @@ impl RadioType { RadioType::OutdoorCbrs => vec![dec!(1), dec!(0.5), dec!(0.25)], }; - // TODO: decide if rank should be 0-indexed - multipliers.get(hex.rank - 1).cloned() + multipliers.get(hex.rank.get() - 1).cloned() } } @@ -225,7 +225,7 @@ pub struct RewardableRadio { #[derive(Debug, Clone, PartialEq)] pub struct CoveredHex { - pub rank: usize, + pub rank: Rank, pub signal_level: SignalLevel, pub assignment: Assignments, pub boosted: Option, @@ -304,7 +304,7 @@ mod tests { location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![CoveredHex { - rank: 1, + rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, @@ -345,7 +345,7 @@ mod tests { urbanized: Assignment, ) -> CoveredHex { CoveredHex { - rank: 1, + rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignment: Assignments { footfall, @@ -412,25 +412,25 @@ mod tests { verified_radio_threshold: true, hexes: vec![ CoveredHex { - rank: 1, + rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, }, CoveredHex { - rank: 2, + rank: Rank::new(2).unwrap(), signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, }, CoveredHex { - rank: 3, + rank: Rank::new(3).unwrap(), signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, }, CoveredHex { - rank: 42, + rank: Rank::new(42).unwrap(), signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, @@ -454,19 +454,19 @@ mod tests { verified_radio_threshold: true, hexes: vec![ CoveredHex { - rank: 1, + rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, }, CoveredHex { - rank: 2, + rank: Rank::new(2).unwrap(), signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, }, CoveredHex { - rank: 42, + rank: Rank::new(42).unwrap(), signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, @@ -491,7 +491,7 @@ mod tests { ], verified_radio_threshold: true, hexes: vec![CoveredHex { - rank: 1, + rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, @@ -511,13 +511,13 @@ mod tests { verified_radio_threshold: true, hexes: vec![ CoveredHex { - rank: 1, + rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, }, CoveredHex { - rank: 1, + rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, assignment: Assignments::best(), boosted: Multiplier::new(4), @@ -545,25 +545,25 @@ mod tests { verified_radio_threshold: true, hexes: vec![ CoveredHex { - rank: 1, + rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, }, CoveredHex { - rank: 1, + rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Medium, assignment: Assignments::best(), boosted: None, }, CoveredHex { - rank: 1, + rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, assignment: Assignments::best(), boosted: None, }, CoveredHex { - rank: 1, + rank: Rank::new(1).unwrap(), signal_level: SignalLevel::None, assignment: Assignments::best(), boosted: None, @@ -578,13 +578,13 @@ mod tests { verified_radio_threshold: true, hexes: vec![ CoveredHex { - rank: 1, + rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, }, CoveredHex { - rank: 1, + rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, assignment: Assignments::best(), boosted: None, @@ -599,25 +599,25 @@ mod tests { verified_radio_threshold: true, hexes: vec![ CoveredHex { - rank: 1, + rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, }, CoveredHex { - rank: 1, + rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Medium, assignment: Assignments::best(), boosted: None, }, CoveredHex { - rank: 1, + rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, assignment: Assignments::best(), boosted: None, }, CoveredHex { - rank: 1, + rank: Rank::new(1).unwrap(), signal_level: SignalLevel::None, assignment: Assignments::best(), boosted: None, @@ -632,13 +632,13 @@ mod tests { verified_radio_threshold: true, hexes: vec![ CoveredHex { - rank: 1, + rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignment: Assignments::best(), boosted: None, }, CoveredHex { - rank: 1, + rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, assignment: Assignments::best(), boosted: None, diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index 295cf4df1..465f14095 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, num::NonZeroU32}; use coverage_point_calculator::{ calculate, calculate_single, Assignment, Assignments, CoverageMap, CoveredHex, MaxOneMultplier, - Radio, RadioType, SignalLevel, Speedtest, + Radio, RadioType, Rank, SignalLevel, Speedtest, }; use rust_decimal_macros::dec; @@ -32,7 +32,7 @@ fn base_radio_coverage_points() { impl CoverageMap for TestCoverageMap { fn hexes(&self, _radio: &impl Radio) -> Vec { vec![CoveredHex { - rank: 1, + rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignment: Assignments { footfall: Assignment::A, @@ -100,7 +100,7 @@ fn radio_unique_coverage() { // all radios will receive 400 coverage points let base_hex = CoveredHex { - rank: 1, + rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignment: Assignments { footfall: Assignment::A, From 5a69e11985a087a22001480e758eca5d61451742 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 23 May 2024 16:43:39 -0700 Subject: [PATCH 019/115] assignments typo --- coverage_point_calculator/src/lib.rs | 52 +++++++++---------- .../tests/coverage_point_calculator.rs | 4 +- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 6fdfbe415..2a017f525 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -227,7 +227,7 @@ pub struct RewardableRadio { pub struct CoveredHex { pub rank: Rank, pub signal_level: SignalLevel, - pub assignment: Assignments, + pub assignments: Assignments, pub boosted: Option, } @@ -242,7 +242,7 @@ impl RewardableRadio { }; let estimated_coverage_points = radio_type.estimated_coverage_points(&hex.signal_level); - let assignments_multiplier = hex.assignment.multiplier(); + let assignments_multiplier = hex.assignments.multiplier(); let hex_boost_multiplier = self.hex_boosting_multiplier(hex); Some( @@ -306,7 +306,7 @@ mod tests { hexes: vec![CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }], }; @@ -347,7 +347,7 @@ mod tests { CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignment: Assignments { + assignments: Assignments { footfall, landtype, urbanized, @@ -414,25 +414,25 @@ mod tests { CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }, CoveredHex { rank: Rank::new(2).unwrap(), signal_level: SignalLevel::High, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }, CoveredHex { rank: Rank::new(3).unwrap(), signal_level: SignalLevel::High, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }, CoveredHex { rank: Rank::new(42).unwrap(), signal_level: SignalLevel::High, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }, ], @@ -456,19 +456,19 @@ mod tests { CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }, CoveredHex { rank: Rank::new(2).unwrap(), signal_level: SignalLevel::High, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }, CoveredHex { rank: Rank::new(42).unwrap(), signal_level: SignalLevel::High, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }, ], @@ -493,7 +493,7 @@ mod tests { hexes: vec![CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }], }; @@ -513,13 +513,13 @@ mod tests { CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }, CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: Multiplier::new(4), }, ], @@ -547,25 +547,25 @@ mod tests { CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }, CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Medium, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }, CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }, CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::None, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }, ], @@ -580,13 +580,13 @@ mod tests { CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }, CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }, ], @@ -601,25 +601,25 @@ mod tests { CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }, CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Medium, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }, CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }, CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::None, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }, ], @@ -634,13 +634,13 @@ mod tests { CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }, CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, - assignment: Assignments::best(), + assignments: Assignments::best(), boosted: None, }, ], diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index 465f14095..0a1ec6d8b 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -34,7 +34,7 @@ fn base_radio_coverage_points() { vec![CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignment: Assignments { + assignments: Assignments { footfall: Assignment::A, landtype: Assignment::A, urbanized: Assignment::A, @@ -102,7 +102,7 @@ fn radio_unique_coverage() { let base_hex = CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignment: Assignments { + assignments: Assignments { footfall: Assignment::A, landtype: Assignment::A, urbanized: Assignment::A, From 9ed0800f4470fd894a8bb66cdc391e3217a829fa Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 23 May 2024 17:15:50 -0700 Subject: [PATCH 020/115] break up filter and map Removing hexes that do not meet the rank requirements is easier to think about in its own step --- coverage_point_calculator/src/lib.rs | 34 ++++++++++++++-------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 2a017f525..dfbe841cf 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -113,15 +113,13 @@ impl RadioType { } } - fn rank_multiplier(&self, hex: &CoveredHex) -> Option { - let multipliers = match self { + fn rank_multipliers(&self) -> Vec { + match self { RadioType::IndoorWifi => vec![dec!(1)], RadioType::IndoorCbrs => vec![dec!(1)], RadioType::OutdoorWifi => vec![dec!(1), dec!(0.5), dec!(0.25)], RadioType::OutdoorCbrs => vec![dec!(1), dec!(0.5), dec!(0.25)], - }; - - multipliers.get(hex.rank.get() - 1).cloned() + } } } @@ -235,23 +233,25 @@ impl RewardableRadio { pub fn to_coverage_points(self) -> CoveragePoints { let radio_type = &self.radio_type; - let hex_points = self.hexes.iter().filter_map(|hex| { - let Some(rank_multiplier) = radio_type.rank_multiplier(hex) else { - // Rank falls outside what is allowed, skip as early as possible - return None; - }; + let rank_multipliers = radio_type.rank_multipliers(); + let max_rank = rank_multipliers.len(); - let estimated_coverage_points = radio_type.estimated_coverage_points(&hex.signal_level); - let assignments_multiplier = hex.assignments.multiplier(); - let hex_boost_multiplier = self.hex_boosting_multiplier(hex); + let hex_points = self + .hexes + .iter() + .filter(|hex| hex.rank.get() <= max_rank) + .map(|hex| { + let estimated_coverage_points = + radio_type.estimated_coverage_points(&hex.signal_level); + let assignments_multiplier = hex.assignments.multiplier(); + let rank_multiplier = rank_multipliers[hex.rank.get() - 1]; + let hex_boost_multiplier = self.hex_boosting_multiplier(hex); - Some( estimated_coverage_points * assignments_multiplier * rank_multiplier - * hex_boost_multiplier, - ) - }); + * hex_boost_multiplier + }); let base_points = hex_points.sum::(); let location_score = self.location_trust_multiplier(); From 5f90176c47df9a60f1ed28078790238e7dd9c4d8 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 23 May 2024 17:16:40 -0700 Subject: [PATCH 021/115] Use more expected name for hex coverage points the HIP says estimated coverage points, but no one thinks of them that way, because there is nothing about them that is an esimate, they are very concrete values. --- coverage_point_calculator/src/lib.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index dfbe841cf..2f0ab4888 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -86,7 +86,7 @@ pub enum RadioType { } impl RadioType { - fn estimated_coverage_points(&self, signal_level: &SignalLevel) -> Points { + fn base_coverage_points(&self, signal_level: &SignalLevel) -> Points { match self { RadioType::IndoorWifi => match signal_level { SignalLevel::High => dec!(400), @@ -241,13 +241,12 @@ impl RewardableRadio { .iter() .filter(|hex| hex.rank.get() <= max_rank) .map(|hex| { - let estimated_coverage_points = - radio_type.estimated_coverage_points(&hex.signal_level); + let base_coverage_points = radio_type.base_coverage_points(&hex.signal_level); let assignments_multiplier = hex.assignments.multiplier(); let rank_multiplier = rank_multipliers[hex.rank.get() - 1]; let hex_boost_multiplier = self.hex_boosting_multiplier(hex); - estimated_coverage_points + base_coverage_points * assignments_multiplier * rank_multiplier * hex_boost_multiplier From 2fe76e817924d46d8e975f7cff355ca04dcb8cf4 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 23 May 2024 17:17:52 -0700 Subject: [PATCH 022/115] remove unused code Now that we have some integration tests, we can trust what code is being used --- coverage_point_calculator/src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 2f0ab4888..a98e5ab1e 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -1,4 +1,3 @@ -#![allow(unused)] /// /// Many changes to the rewards algorithm are contained in and across many HIPs. /// The blog post [MOBILE Proof of Coverage][mobile-poc-blog] contains a more @@ -38,7 +37,6 @@ /// [mobile-poc-blog]: /// https://docs.helium.com/mobile/proof-of-coverage /// -use hextree::Cell; use rust_decimal::{Decimal, RoundingStrategy}; use rust_decimal_macros::dec; From 5f3f14a34aca784f5c303ab3d0048594c6b3e984 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 23 May 2024 17:51:19 -0700 Subject: [PATCH 023/115] Make calculating coverage points a top level function I want this crate to make the algorithm one of the most important things. Hiding it inside a structs impl block seems not a great of doing that, I think. I also don't think the rewardable radio impl block should be moved to the top to make the algorithm more front and center, in my mind it would only make RewardableRadio look more important. --- coverage_point_calculator/src/lib.rs | 134 +++++++++++------- .../tests/coverage_point_calculator.rs | 17 +-- 2 files changed, 92 insertions(+), 59 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index a98e5ab1e..12a55b9c5 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -56,16 +56,51 @@ pub trait CoverageMap { fn hexes(&self, radio: &impl Radio) -> Vec; } -pub fn calculate<'a>( +pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { + let radio_type = &radio.radio_type; + + let rank_multipliers = radio_type.rank_multipliers(); + let max_rank = rank_multipliers.len(); + + let hex_points = radio + .hexes + .iter() + .filter(|hex| hex.rank.get() <= max_rank) + .map(|hex| { + let base_coverage_points = radio_type.base_coverage_points(&hex.signal_level); + let assignments_multiplier = hex.assignments.multiplier(); + let rank_multiplier = rank_multipliers[hex.rank.get() - 1]; + let hex_boost_multiplier = radio.hex_boosting_multiplier(hex); + + base_coverage_points * assignments_multiplier * rank_multiplier * hex_boost_multiplier + }); + + let base_points = hex_points.sum::(); + let location_score = radio.location_trust_multiplier(); + let speedtest = radio.speedtest.multiplier(); + + let coverage_points = base_points * location_score * speedtest; + let coverage_points = coverage_points.round_dp_with_strategy(2, RoundingStrategy::ToZero); + + CoveragePoints { + coverage_points, + radio, + } +} + +pub fn make_rewardable_radios<'a>( radios: &'a [impl Radio], coverage_map: &'a impl CoverageMap, ) -> impl Iterator + 'a { radios .iter() - .map(|radio| calculate_single(radio, coverage_map)) + .map(|radio| make_rewardable_radio(radio, coverage_map)) } -pub fn calculate_single(radio: &impl Radio, coverage_map: &impl CoverageMap) -> RewardableRadio { +pub fn make_rewardable_radio( + radio: &impl Radio, + coverage_map: &impl CoverageMap, +) -> RewardableRadio { RewardableRadio { radio_type: radio.radio_type(), speedtest: radio.speedtest(), @@ -228,39 +263,6 @@ pub struct CoveredHex { } impl RewardableRadio { - pub fn to_coverage_points(self) -> CoveragePoints { - let radio_type = &self.radio_type; - - let rank_multipliers = radio_type.rank_multipliers(); - let max_rank = rank_multipliers.len(); - - let hex_points = self - .hexes - .iter() - .filter(|hex| hex.rank.get() <= max_rank) - .map(|hex| { - let base_coverage_points = radio_type.base_coverage_points(&hex.signal_level); - let assignments_multiplier = hex.assignments.multiplier(); - let rank_multiplier = rank_multipliers[hex.rank.get() - 1]; - let hex_boost_multiplier = self.hex_boosting_multiplier(hex); - - base_coverage_points - * assignments_multiplier - * rank_multiplier - * hex_boost_multiplier - }); - - let base_points = hex_points.sum::(); - let location_score = self.location_trust_multiplier(); - let speedtest = self.speedtest.multiplier(); - - let coverage_points = base_points * location_score * speedtest; - CoveragePoints { - coverage_points: coverage_points.round_dp_with_strategy(2, RoundingStrategy::ToZero), - radio: self, - } - } - fn location_trust_multiplier(&self) -> Decimal { let trust_score_count = Decimal::from(self.location_trust_scores.len()); let trust_score_sum = self.location_trust_scores.iter().sum::(); @@ -309,29 +311,32 @@ mod tests { }; assert_eq!( dec!(100), - indoor_cbrs.clone().to_coverage_points().coverage_points + calculate_coverage_points(indoor_cbrs.clone()).coverage_points ); indoor_cbrs.speedtest = Speedtest::Acceptable; assert_eq!( dec!(75), - indoor_cbrs.clone().to_coverage_points().coverage_points + calculate_coverage_points(indoor_cbrs.clone()).coverage_points ); indoor_cbrs.speedtest = Speedtest::Degraded; assert_eq!( dec!(50), - indoor_cbrs.clone().to_coverage_points().coverage_points + calculate_coverage_points(indoor_cbrs.clone()).coverage_points ); indoor_cbrs.speedtest = Speedtest::Poor; assert_eq!( dec!(25), - indoor_cbrs.clone().to_coverage_points().coverage_points + calculate_coverage_points(indoor_cbrs.clone()).coverage_points ); indoor_cbrs.speedtest = Speedtest::Fail; - assert_eq!(dec!(0), indoor_cbrs.to_coverage_points().coverage_points); + assert_eq!( + dec!(0), + calculate_coverage_points(indoor_cbrs).coverage_points + ); } #[test] @@ -397,7 +402,10 @@ mod tests { ], }; - assert_eq!(dec!(1073), indoor_cbrs.to_coverage_points().coverage_points); + assert_eq!( + dec!(1073), + calculate_coverage_points(indoor_cbrs).coverage_points + ); } #[test] @@ -439,7 +447,10 @@ mod tests { // rank 2 :: 0.50 * 16 == 8 // rank 3 :: 0.25 * 16 == 4 // rank 42 :: 0.00 * 16 == 0 - assert_eq!(dec!(28), outdoor_wifi.to_coverage_points().coverage_points); + assert_eq!( + dec!(28), + calculate_coverage_points(outdoor_wifi).coverage_points + ); } #[test] @@ -471,7 +482,10 @@ mod tests { ], }; - assert_eq!(dec!(400), indoor_wifi.to_coverage_points().coverage_points); + assert_eq!( + dec!(400), + calculate_coverage_points(indoor_wifi).coverage_points + ); } #[test] @@ -496,7 +510,10 @@ mod tests { }; // Location trust scores is 1/4 - assert_eq!(dec!(100), indoor_wifi.to_coverage_points().coverage_points); + assert_eq!( + dec!(100), + calculate_coverage_points(indoor_wifi).coverage_points + ); } #[test] @@ -525,12 +542,15 @@ mod tests { // signal_level of High. assert_eq!( dec!(800), - indoor_wifi.clone().to_coverage_points().coverage_points + calculate_coverage_points(indoor_wifi.clone()).coverage_points ); // When the radio is not verified for boosted rewards, the boost has no effect. indoor_wifi.verified_radio_threshold = false; - assert_eq!(dec!(500), indoor_wifi.to_coverage_points().coverage_points); + assert_eq!( + dec!(500), + calculate_coverage_points(indoor_wifi).coverage_points + ); } #[test] @@ -645,9 +665,21 @@ mod tests { // When each radio contains a hex of every applicable signal_level, and // multipliers are break even. These are the accumulated coverage points. - assert_eq!(dec!(7), outdoor_cbrs.to_coverage_points().coverage_points); - assert_eq!(dec!(125), indoor_cbrs.to_coverage_points().coverage_points); - assert_eq!(dec!(28), outdoor_wifi.to_coverage_points().coverage_points); - assert_eq!(dec!(500), indoor_wifi.to_coverage_points().coverage_points); + assert_eq!( + dec!(7), + calculate_coverage_points(outdoor_cbrs).coverage_points + ); + assert_eq!( + dec!(125), + calculate_coverage_points(indoor_cbrs).coverage_points + ); + assert_eq!( + dec!(28), + calculate_coverage_points(outdoor_wifi).coverage_points + ); + assert_eq!( + dec!(500), + calculate_coverage_points(indoor_wifi).coverage_points + ); } } diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index 0a1ec6d8b..53a8da944 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -1,8 +1,9 @@ use std::{collections::HashMap, num::NonZeroU32}; use coverage_point_calculator::{ - calculate, calculate_single, Assignment, Assignments, CoverageMap, CoveredHex, MaxOneMultplier, - Radio, RadioType, Rank, SignalLevel, Speedtest, + calculate_coverage_points, make_rewardable_radio, make_rewardable_radios, Assignment, + Assignments, CoverageMap, CoveredHex, MaxOneMultplier, Radio, RadioType, Rank, SignalLevel, + Speedtest, }; use rust_decimal_macros::dec; @@ -50,10 +51,10 @@ fn base_radio_coverage_points() { RadioType::OutdoorWifi, RadioType::OutdoorCbrs, ] { - let radio = calculate_single(&TestRadio(radio_type), &TestCoverageMap); + let radio = make_rewardable_radio(&TestRadio(radio_type), &TestCoverageMap); println!( "{radio_type:?} \t--> {}", - radio.to_coverage_points().coverage_points + calculate_coverage_points(radio).coverage_points ); } @@ -63,8 +64,8 @@ fn base_radio_coverage_points() { TestRadio(RadioType::OutdoorWifi), TestRadio(RadioType::OutdoorCbrs), ]; - let output = calculate(&radios, &TestCoverageMap) - .map(|r| (r.radio_type, r.to_coverage_points().coverage_points)) + let output = make_rewardable_radios(&radios, &TestCoverageMap) + .map(|r| (r.radio_type, calculate_coverage_points(r).coverage_points)) .collect::>(); println!("{output:#?}"); } @@ -132,8 +133,8 @@ fn radio_unique_coverage() { } } - let coverage_points = calculate(&radios, &coverage_map) - .map(|r| (r.radio_type, r.to_coverage_points().coverage_points)) + let coverage_points = make_rewardable_radios(&radios, &coverage_map) + .map(|r| (r.radio_type, calculate_coverage_points(r).coverage_points)) .collect::>(); println!("{coverage_points:#?}") } From 16dee057ae36d26cd4d4634dd3053941a3583ebc Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 24 May 2024 15:21:10 -0700 Subject: [PATCH 024/115] flesh out speedtests --- coverage_point_calculator/src/lib.rs | 172 +++++++++++++----- coverage_point_calculator/src/speedtest.rs | 159 ++++++++++++++++ .../tests/coverage_point_calculator.rs | 37 +++- 3 files changed, 319 insertions(+), 49 deletions(-) create mode 100644 coverage_point_calculator/src/speedtest.rs diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 12a55b9c5..779c768bf 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -37,6 +37,18 @@ /// [mobile-poc-blog]: /// https://docs.helium.com/mobile/proof-of-coverage /// +/// To Integrate in Docs: +/// +/// Some verbiage about ranks. +/// https://github.com/helium/HIP/blob/main/0105-modification-of-mobile-subdao-hex-limits.md +/// +/// Has something to say about 30meters from asserted location wrt poc rewards for boosted hexes. +/// https://github.com/helium/HIP/blob/8b1e814afa61a714b5ba63d3265e5897ab4c5116/0107-preventing-gaming-within-the-mobile-network.md +/// ! I cannot find a reason why the max distance to asserted changed from 30m to 50m. +/// +pub mod speedtest; + +use crate::speedtest::{Speedtest, SpeedtestTier}; use rust_decimal::{Decimal, RoundingStrategy}; use rust_decimal_macros::dec; @@ -47,8 +59,8 @@ type Points = Decimal; pub trait Radio { fn radio_type(&self) -> RadioType; - fn speedtest(&self) -> Speedtest; - fn location_trust_scores(&self) -> Vec; + fn speedtests(&self) -> Vec; + fn location_trust_scores(&self) -> Vec; fn verified_radio_threshold(&self) -> bool; } @@ -77,7 +89,7 @@ pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { let base_points = hex_points.sum::(); let location_score = radio.location_trust_multiplier(); - let speedtest = radio.speedtest.multiplier(); + let speedtest = radio.speedtest_multiplier(); let coverage_points = base_points * location_score * speedtest; let coverage_points = coverage_points.round_dp_with_strategy(2, RoundingStrategy::ToZero); @@ -103,13 +115,28 @@ pub fn make_rewardable_radio( ) -> RewardableRadio { RewardableRadio { radio_type: radio.radio_type(), - speedtest: radio.speedtest(), + speedtests: radio.speedtests(), location_trust_scores: radio.location_trust_scores(), verified_radio_threshold: radio.verified_radio_threshold(), hexes: coverage_map.hexes(radio), } } +#[derive(Debug, Clone, PartialEq, PartialOrd)] +pub struct Meters(u32); + +impl Meters { + fn new(meters: u32) -> Self { + Self(meters) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct LocationTrust { + distance_to_asserted: Meters, + trust_score: Decimal, +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum RadioType { IndoorWifi, @@ -218,27 +245,6 @@ impl Assignments { } } -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Speedtest { - Good, - Acceptable, - Degraded, - Poor, - Fail, -} - -impl Speedtest { - fn multiplier(&self) -> MaxOneMultplier { - match self { - Speedtest::Good => dec!(1.00), - Speedtest::Acceptable => dec!(0.75), - Speedtest::Degraded => dec!(0.50), - Speedtest::Poor => dec!(0.25), - Speedtest::Fail => dec!(0), - } - } -} - #[derive(Debug)] pub struct CoveragePoints { pub coverage_points: Decimal, @@ -248,8 +254,8 @@ pub struct CoveragePoints { #[derive(Debug, Clone, PartialEq)] pub struct RewardableRadio { pub radio_type: RadioType, - pub speedtest: Speedtest, - pub location_trust_scores: Vec, + pub speedtests: Vec, + pub location_trust_scores: Vec, pub verified_radio_threshold: bool, pub hexes: Vec, } @@ -264,8 +270,36 @@ pub struct CoveredHex { impl RewardableRadio { fn location_trust_multiplier(&self) -> Decimal { + const RESTRICTIVE_MAX_DISTANCE: Meters = Meters(50); + + // CBRS radios are always trusted because they have internal GPS + match self.radio_type { + RadioType::IndoorCbrs => return dec!(1), + RadioType::OutdoorCbrs => return dec!(1), + _ => {} + } + + let trust_score_sum: Decimal = if self.any_hexes_boosted() { + // Cap multipliers to 0.25x when a radio covers _any_ boosted hex + // and it's distance to asserted is above the threshold. + self.location_trust_scores + .iter() + .map(|l| { + if l.distance_to_asserted > RESTRICTIVE_MAX_DISTANCE { + dec!(0.25).min(l.trust_score) + } else { + l.trust_score + } + }) + .sum() + } else { + self.location_trust_scores + .iter() + .map(|l| l.trust_score) + .sum() + }; + let trust_score_count = Decimal::from(self.location_trust_scores.len()); - let trust_score_sum = self.location_trust_scores.iter().sum::(); trust_score_sum / trust_score_count } @@ -277,11 +311,28 @@ impl RewardableRadio { }; Decimal::from(maybe_boost) } + + fn speedtest_multiplier(&self) -> MaxOneMultplier { + const MIN_REQUIRED_SPEEDTEST_SAMPLES: usize = 2; + + if self.speedtests.len() < MIN_REQUIRED_SPEEDTEST_SAMPLES { + return SpeedtestTier::Fail.multiplier(); + } + + let speedtest_avg = Speedtest::avg(&self.speedtests); + speedtest_avg.multiplier() + } + + fn any_hexes_boosted(&self) -> bool { + self.hexes.iter().any(|hex| hex.boosted.is_some()) + } } #[cfg(test)] mod tests { + use crate::speedtest::{BytesPs, Millis}; + use super::*; use rust_decimal_macros::dec; @@ -295,11 +346,35 @@ mod tests { } } + impl Speedtest { + fn best() -> Vec { + vec![ + Self { + upload_speed: BytesPs::mbps(15), + download_speed: BytesPs::mbps(150), + latency: Millis::new(15), + }, + Self { + upload_speed: BytesPs::mbps(15), + download_speed: BytesPs::mbps(150), + latency: Millis::new(15), + }, + ] + } + fn download(download: BytesPs) -> Self { + Self { + upload_speed: BytesPs::mbps(15), + download_speed: download, + latency: Millis::new(15), + } + } + } + #[test] fn speedtest() { let mut indoor_cbrs = RewardableRadio { radio_type: RadioType::IndoorCbrs, - speedtest: Speedtest::Good, + speedtests: Speedtest::best(), location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![CoveredHex { @@ -309,30 +384,43 @@ mod tests { boosted: None, }], }; + assert_eq!( dec!(100), calculate_coverage_points(indoor_cbrs.clone()).coverage_points ); - indoor_cbrs.speedtest = Speedtest::Acceptable; + indoor_cbrs.speedtests = vec![ + Speedtest::download(BytesPs::mbps(88)), + Speedtest::download(BytesPs::mbps(88)), + ]; assert_eq!( dec!(75), calculate_coverage_points(indoor_cbrs.clone()).coverage_points ); - indoor_cbrs.speedtest = Speedtest::Degraded; + indoor_cbrs.speedtests = vec![ + Speedtest::download(BytesPs::mbps(62)), + Speedtest::download(BytesPs::mbps(62)), + ]; assert_eq!( dec!(50), calculate_coverage_points(indoor_cbrs.clone()).coverage_points ); - indoor_cbrs.speedtest = Speedtest::Poor; + indoor_cbrs.speedtests = vec![ + Speedtest::download(BytesPs::mbps(42)), + Speedtest::download(BytesPs::mbps(42)), + ]; assert_eq!( dec!(25), calculate_coverage_points(indoor_cbrs.clone()).coverage_points ); - indoor_cbrs.speedtest = Speedtest::Fail; + indoor_cbrs.speedtests = vec![ + Speedtest::download(BytesPs::mbps(25)), + Speedtest::download(BytesPs::mbps(25)), + ]; assert_eq!( dec!(0), calculate_coverage_points(indoor_cbrs).coverage_points @@ -361,7 +449,7 @@ mod tests { use Assignment::*; let indoor_cbrs = RewardableRadio { radio_type: RadioType::IndoorCbrs, - speedtest: Speedtest::Good, + speedtests: Speedtest::best(), location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ @@ -412,7 +500,7 @@ mod tests { fn outdoor_radios_consider_top_3_ranked_hexes() { let outdoor_wifi = RewardableRadio { radio_type: RadioType::OutdoorWifi, - speedtest: Speedtest::Good, + speedtests: Speedtest::best(), location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ @@ -457,7 +545,7 @@ mod tests { fn indoor_radios_only_consider_first_ranked_hexes() { let indoor_wifi = RewardableRadio { radio_type: RadioType::IndoorWifi, - speedtest: Speedtest::Good, + speedtests: Speedtest::best(), location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ @@ -493,7 +581,7 @@ mod tests { // Location scores are averaged together let indoor_wifi = RewardableRadio { radio_type: RadioType::IndoorWifi, - speedtest: Speedtest::Good, + speedtests: Speedtest::best(), location_trust_scores: vec![ MaxOneMultplier::from_f32_retain(0.1).unwrap(), MaxOneMultplier::from_f32_retain(0.2).unwrap(), @@ -520,7 +608,7 @@ mod tests { fn boosted_hex() { let mut indoor_wifi = RewardableRadio { radio_type: RadioType::IndoorWifi, - speedtest: Speedtest::Good, + speedtests: Speedtest::best(), location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ @@ -557,7 +645,7 @@ mod tests { fn base_radio_coverage_points() { let outdoor_cbrs = RewardableRadio { radio_type: RadioType::OutdoorCbrs, - speedtest: Speedtest::Good, + speedtests: Speedtest::best(), location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ @@ -590,7 +678,7 @@ mod tests { let indoor_cbrs = RewardableRadio { radio_type: RadioType::IndoorCbrs, - speedtest: Speedtest::Good, + speedtests: Speedtest::best(), location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ @@ -611,7 +699,7 @@ mod tests { let outdoor_wifi = RewardableRadio { radio_type: RadioType::OutdoorWifi, - speedtest: Speedtest::Good, + speedtests: Speedtest::best(), location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ @@ -644,7 +732,7 @@ mod tests { let indoor_wifi = RewardableRadio { radio_type: RadioType::IndoorWifi, - speedtest: Speedtest::Good, + speedtests: Speedtest::best(), location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], verified_radio_threshold: true, hexes: vec![ diff --git a/coverage_point_calculator/src/speedtest.rs b/coverage_point_calculator/src/speedtest.rs new file mode 100644 index 000000000..ad0e1778f --- /dev/null +++ b/coverage_point_calculator/src/speedtest.rs @@ -0,0 +1,159 @@ +use rust_decimal::Decimal; +use rust_decimal_macros::dec; + +use crate::MaxOneMultplier; + +#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)] +pub struct BytesPs(u64); + +impl BytesPs { + pub fn new(bytes_per_second: u64) -> Self { + Self(bytes_per_second) + } + + pub fn mbps(megabytes_per_second: u64) -> Self { + Self(megabytes_per_second * 12500) + } + + fn to_mbps(&self) -> u64 { + self.0 / 12500 + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub struct Millis(u32); + +impl Millis { + pub fn new(milliseconds: u32) -> Self { + Self(milliseconds) + } +} + +#[derive(Debug, Default, Clone, Copy, PartialEq)] +pub struct Speedtest { + pub upload_speed: BytesPs, + pub download_speed: BytesPs, + pub latency: Millis, +} + +impl Speedtest { + pub fn multiplier(&self) -> Decimal { + let upload = SpeedtestTier::from_upload(&self.upload_speed); + let download = SpeedtestTier::from_download(&self.download_speed); + let latency = SpeedtestTier::from_latency(&self.latency); + + let tier = upload.min(download).min(latency); + tier.multiplier() + } + + pub fn avg(speedtests: &[Self]) -> Self { + let mut download = 0; + let mut upload = 0; + let mut latency = 0; + + for test in speedtests { + upload += test.upload_speed.0; + download += test.download_speed.0; + latency += test.latency.0; + } + + let count = speedtests.len(); + Self { + upload_speed: BytesPs::new(upload / count as u64), + download_speed: BytesPs::new(download / count as u64), + latency: Millis::new(latency / count as u32), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum SpeedtestTier { + Good = 4, + Acceptable = 3, + Degraded = 2, + Poor = 1, + Fail = 0, +} + +impl SpeedtestTier { + pub fn multiplier(&self) -> MaxOneMultplier { + match self { + SpeedtestTier::Good => dec!(1.00), + SpeedtestTier::Acceptable => dec!(0.75), + SpeedtestTier::Degraded => dec!(0.50), + SpeedtestTier::Poor => dec!(0.25), + SpeedtestTier::Fail => dec!(0), + } + } + + // FIXME: The modeled coverage blog post declares all comparisons as non-inclusive. + // And in the blog post, the UI shows what appear to be inclusive ranges. + // + // Speed Test Tier + // Acceptable 100+ Download, AND 10+ Upload, AND <50 Latency + // In the UI + // Acceptable: 0-50ms + // + // I find this confusing + + fn from_download(bytes: &BytesPs) -> Self { + match bytes.to_mbps() { + 100.. => Self::Good, + 75.. => Self::Acceptable, + 50.. => Self::Degraded, + 30.. => Self::Poor, + _ => Self::Fail, + } + } + + fn from_upload(bytes: &BytesPs) -> Self { + match bytes.to_mbps() { + 10.. => Self::Good, + 8.. => Self::Acceptable, + 5.. => Self::Degraded, + 2.. => Self::Poor, + _ => Self::Fail, + } + } + + fn from_latency(Millis(millis): &Millis) -> Self { + // FIXME: comparison in mobile-verifier is non-inclusive + match millis { + ..=50 => Self::Good, + ..=60 => Self::Acceptable, + ..=75 => Self::Degraded, + ..=100 => Self::Poor, + _ => Self::Fail, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn speedtest_teirs() { + use SpeedtestTier::*; + // download + assert_eq!(Good, SpeedtestTier::from_download(&BytesPs::mbps(100))); + assert_eq!(Acceptable, SpeedtestTier::from_download(&BytesPs::mbps(80))); + assert_eq!(Degraded, SpeedtestTier::from_download(&BytesPs::mbps(62))); + assert_eq!(Poor, SpeedtestTier::from_download(&BytesPs::mbps(42))); + assert_eq!(Fail, SpeedtestTier::from_download(&BytesPs::mbps(20))); + + // upload + assert_eq!(Good, SpeedtestTier::from_upload(&BytesPs::mbps(10))); + assert_eq!(Acceptable, SpeedtestTier::from_upload(&BytesPs::mbps(8))); + assert_eq!(Degraded, SpeedtestTier::from_upload(&BytesPs::mbps(6))); + assert_eq!(Poor, SpeedtestTier::from_upload(&BytesPs::mbps(4))); + assert_eq!(Fail, SpeedtestTier::from_upload(&BytesPs::mbps(1))); + + // latency + assert_eq!(Good, SpeedtestTier::from_latency(&Millis::new(50))); + assert_eq!(Acceptable, SpeedtestTier::from_latency(&Millis::new(60))); + assert_eq!(Degraded, SpeedtestTier::from_latency(&Millis::new(75))); + assert_eq!(Poor, SpeedtestTier::from_latency(&Millis::new(100))); + assert_eq!(Fail, SpeedtestTier::from_latency(&Millis::new(101))); + } +} diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index 53a8da944..269512319 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -1,9 +1,10 @@ use std::{collections::HashMap, num::NonZeroU32}; use coverage_point_calculator::{ - calculate_coverage_points, make_rewardable_radio, make_rewardable_radios, Assignment, - Assignments, CoverageMap, CoveredHex, MaxOneMultplier, Radio, RadioType, Rank, SignalLevel, - Speedtest, + calculate_coverage_points, make_rewardable_radio, make_rewardable_radios, + speedtest::{BytesPs, Millis, Speedtest}, + Assignment, Assignments, CoverageMap, CoveredHex, MaxOneMultplier, Radio, RadioType, Rank, + SignalLevel, }; use rust_decimal_macros::dec; @@ -17,8 +18,19 @@ fn base_radio_coverage_points() { self.0 } - fn speedtest(&self) -> Speedtest { - Speedtest::Good + fn speedtests(&self) -> Vec { + vec![ + Speedtest { + upload_speed: BytesPs::mbps(15), + download_speed: BytesPs::mbps(150), + latency: Millis::new(15), + }, + Speedtest { + upload_speed: BytesPs::mbps(15), + download_speed: BytesPs::mbps(150), + latency: Millis::new(15), + }, + ] } fn location_trust_scores(&self) -> Vec { @@ -86,8 +98,19 @@ fn radio_unique_coverage() { self.0 } - fn speedtest(&self) -> Speedtest { - Speedtest::Good + fn speedtests(&self) -> Vec { + vec![ + Speedtest { + upload_speed: BytesPs::mbps(15), + download_speed: BytesPs::mbps(150), + latency: Millis::new(15), + }, + Speedtest { + upload_speed: BytesPs::mbps(15), + download_speed: BytesPs::mbps(150), + latency: Millis::new(15), + }, + ] } fn location_trust_scores(&self) -> Vec { From c5018ddbc8631e45622ba72c7b39b7c6b974f174 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 24 May 2024 15:35:53 -0700 Subject: [PATCH 025/115] make location trust into full struct --- coverage_point_calculator/src/lib.rs | 61 +++++++++++++------ .../tests/coverage_point_calculator.rs | 18 ++++-- 2 files changed, 55 insertions(+), 24 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 779c768bf..56d1b9bad 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -42,9 +42,18 @@ /// Some verbiage about ranks. /// https://github.com/helium/HIP/blob/main/0105-modification-of-mobile-subdao-hex-limits.md /// -/// Has something to say about 30meters from asserted location wrt poc rewards for boosted hexes. +/// Has something to say about 30meters from asserted location wrt poc rewards +/// for boosted hexes. /// https://github.com/helium/HIP/blob/8b1e814afa61a714b5ba63d3265e5897ab4c5116/0107-preventing-gaming-within-the-mobile-network.md -/// ! I cannot find a reason why the max distance to asserted changed from 30m to 50m. +/// +/// From Madninja: +/// A res 12 hex is 307m^2 which makes for a width of about 17m. +/// Since both the asserted location and the skyhook location are snapped to the +/// center of the res 12 hex they are in some distances are up over 30m (17 * 2 +/// > 30) away from where the hotspots actually are. In addition skyhook drift +/// can cause this to drift around a bit (we've seen 10-15m). To take both +/// snapping and some skyhook slop into account the distance between asserted +/// and inferred locations was increase from 30 to 50m /// pub mod speedtest; @@ -126,15 +135,15 @@ pub fn make_rewardable_radio( pub struct Meters(u32); impl Meters { - fn new(meters: u32) -> Self { + pub fn new(meters: u32) -> Self { Self(meters) } } #[derive(Debug, Clone, PartialEq)] pub struct LocationTrust { - distance_to_asserted: Meters, - trust_score: Decimal, + pub distance_to_asserted: Meters, + pub trust_score: Decimal, } #[derive(Debug, Clone, Copy, PartialEq)] @@ -370,12 +379,28 @@ mod tests { } } + impl LocationTrust { + fn best() -> Vec { + vec![Self { + distance_to_asserted: Meters::new(1), + trust_score: dec!(1.0), + }] + } + + fn trust_score(trust_score: Decimal) -> Self { + Self { + distance_to_asserted: Meters::new(1), + trust_score, + } + } + } + #[test] fn speedtest() { let mut indoor_cbrs = RewardableRadio { radio_type: RadioType::IndoorCbrs, speedtests: Speedtest::best(), - location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], + location_trust_scores: LocationTrust::best(), verified_radio_threshold: true, hexes: vec![CoveredHex { rank: Rank::new(1).unwrap(), @@ -450,7 +475,7 @@ mod tests { let indoor_cbrs = RewardableRadio { radio_type: RadioType::IndoorCbrs, speedtests: Speedtest::best(), - location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], + location_trust_scores: LocationTrust::best(), verified_radio_threshold: true, hexes: vec![ // yellow - POI ≥ 1 Urbanized @@ -501,7 +526,7 @@ mod tests { let outdoor_wifi = RewardableRadio { radio_type: RadioType::OutdoorWifi, speedtests: Speedtest::best(), - location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], + location_trust_scores: LocationTrust::best(), verified_radio_threshold: true, hexes: vec![ CoveredHex { @@ -546,7 +571,7 @@ mod tests { let indoor_wifi = RewardableRadio { radio_type: RadioType::IndoorWifi, speedtests: Speedtest::best(), - location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], + location_trust_scores: LocationTrust::best(), verified_radio_threshold: true, hexes: vec![ CoveredHex { @@ -583,10 +608,10 @@ mod tests { radio_type: RadioType::IndoorWifi, speedtests: Speedtest::best(), location_trust_scores: vec![ - MaxOneMultplier::from_f32_retain(0.1).unwrap(), - MaxOneMultplier::from_f32_retain(0.2).unwrap(), - MaxOneMultplier::from_f32_retain(0.3).unwrap(), - MaxOneMultplier::from_f32_retain(0.4).unwrap(), + LocationTrust::trust_score(dec!(0.1)), + LocationTrust::trust_score(dec!(0.2)), + LocationTrust::trust_score(dec!(0.3)), + LocationTrust::trust_score(dec!(0.4)), ], verified_radio_threshold: true, hexes: vec![CoveredHex { @@ -609,7 +634,7 @@ mod tests { let mut indoor_wifi = RewardableRadio { radio_type: RadioType::IndoorWifi, speedtests: Speedtest::best(), - location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], + location_trust_scores: LocationTrust::best(), verified_radio_threshold: true, hexes: vec![ CoveredHex { @@ -646,7 +671,7 @@ mod tests { let outdoor_cbrs = RewardableRadio { radio_type: RadioType::OutdoorCbrs, speedtests: Speedtest::best(), - location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], + location_trust_scores: LocationTrust::best(), verified_radio_threshold: true, hexes: vec![ CoveredHex { @@ -679,7 +704,7 @@ mod tests { let indoor_cbrs = RewardableRadio { radio_type: RadioType::IndoorCbrs, speedtests: Speedtest::best(), - location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], + location_trust_scores: LocationTrust::best(), verified_radio_threshold: true, hexes: vec![ CoveredHex { @@ -700,7 +725,7 @@ mod tests { let outdoor_wifi = RewardableRadio { radio_type: RadioType::OutdoorWifi, speedtests: Speedtest::best(), - location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], + location_trust_scores: LocationTrust::best(), verified_radio_threshold: true, hexes: vec![ CoveredHex { @@ -733,7 +758,7 @@ mod tests { let indoor_wifi = RewardableRadio { radio_type: RadioType::IndoorWifi, speedtests: Speedtest::best(), - location_trust_scores: vec![MaxOneMultplier::from_f32_retain(1.0).unwrap()], + location_trust_scores: LocationTrust::best(), verified_radio_threshold: true, hexes: vec![ CoveredHex { diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index 269512319..455ce5eb6 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -3,8 +3,8 @@ use std::{collections::HashMap, num::NonZeroU32}; use coverage_point_calculator::{ calculate_coverage_points, make_rewardable_radio, make_rewardable_radios, speedtest::{BytesPs, Millis, Speedtest}, - Assignment, Assignments, CoverageMap, CoveredHex, MaxOneMultplier, Radio, RadioType, Rank, - SignalLevel, + Assignment, Assignments, CoverageMap, CoveredHex, LocationTrust, Meters, Radio, RadioType, + Rank, SignalLevel, }; use rust_decimal_macros::dec; @@ -33,8 +33,11 @@ fn base_radio_coverage_points() { ] } - fn location_trust_scores(&self) -> Vec { - vec![dec!(1)] + fn location_trust_scores(&self) -> Vec { + vec![LocationTrust { + distance_to_asserted: Meters::new(1), + trust_score: dec!(1.0), + }] } fn verified_radio_threshold(&self) -> bool { @@ -113,8 +116,11 @@ fn radio_unique_coverage() { ] } - fn location_trust_scores(&self) -> Vec { - vec![dec!(1)] + fn location_trust_scores(&self) -> Vec { + vec![LocationTrust { + distance_to_asserted: Meters::new(1), + trust_score: dec!(1.0), + }] } fn verified_radio_threshold(&self) -> bool { From 18d1ca3d86752e1a63530b6c6b09e8097f217fb6 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 24 May 2024 16:55:02 -0700 Subject: [PATCH 026/115] Location trust scores trust scores and covered hexes are linked in many ways. I'm opting to precompute multiple values then have quick access to them, rather than waiting until they're needed and computing them for every hex. Or carrying around a spot for computed data that get's filled on the first use. When the rest of the types are fleshed out, some benchmarks will be done to determine if this is a problem. --- coverage_point_calculator/src/lib.rs | 380 ++++++++++++++++----------- 1 file changed, 224 insertions(+), 156 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 56d1b9bad..87a58330a 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -84,6 +84,7 @@ pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { let max_rank = rank_multipliers.len(); let hex_points = radio + .covered_hexes .hexes .iter() .filter(|hex| hex.rank.get() <= max_rank) @@ -125,9 +126,9 @@ pub fn make_rewardable_radio( RewardableRadio { radio_type: radio.radio_type(), speedtests: radio.speedtests(), - location_trust_scores: radio.location_trust_scores(), + location_trust_scores: LocationTrustScores::new(radio.location_trust_scores()), verified_radio_threshold: radio.verified_radio_threshold(), - hexes: coverage_map.hexes(radio), + covered_hexes: CoveredHexes::new(coverage_map.hexes(radio)), } } @@ -146,6 +147,51 @@ pub struct LocationTrust { pub trust_score: Decimal, } +#[derive(Debug, Clone, PartialEq)] +pub struct LocationTrustScores { + boosted_multiplier: Decimal, + unboosted_multiplier: Decimal, + trust_scores: Vec, +} + +impl LocationTrustScores { + fn new(trust_scores: Vec) -> Self { + let boosted_multiplier = Self::boosted_multiplier(&trust_scores); + let unboosted_multiplier = Self::unboosted_multiplier(&trust_scores); + Self { + boosted_multiplier, + unboosted_multiplier, + trust_scores, + } + } + + fn boosted_multiplier(trust_scores: &[LocationTrust]) -> Decimal { + const RESTRICTIVE_MAX_DISTANCE: Meters = Meters(50); + // Cap multipliers to 0.25x when a radio covers _any_ boosted hex + // and it's distance to asserted is above the threshold. + let count = Decimal::from(trust_scores.len()); + let scores: Decimal = trust_scores + .iter() + .map(|l| { + if l.distance_to_asserted > RESTRICTIVE_MAX_DISTANCE { + dec!(0.25).min(l.trust_score) + } else { + l.trust_score + } + }) + .sum(); + + scores / count + } + + fn unboosted_multiplier(trust_scores: &[LocationTrust]) -> Decimal { + let count = Decimal::from(trust_scores.len()); + let scores: Decimal = trust_scores.iter().map(|l| l.trust_score).sum(); + + scores / count + } +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum RadioType { IndoorWifi, @@ -264,9 +310,25 @@ pub struct CoveragePoints { pub struct RewardableRadio { pub radio_type: RadioType, pub speedtests: Vec, - pub location_trust_scores: Vec, + pub location_trust_scores: LocationTrustScores, pub verified_radio_threshold: bool, - pub hexes: Vec, + pub covered_hexes: CoveredHexes, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CoveredHexes { + any_boosted: bool, + hexes: Vec, +} + +impl CoveredHexes { + fn new(covered_hexes: Vec) -> Self { + let any_boosted = covered_hexes.iter().any(|hex| hex.boosted.is_some()); + Self { + any_boosted, + hexes: covered_hexes, + } + } } #[derive(Debug, Clone, PartialEq)] @@ -279,46 +341,31 @@ pub struct CoveredHex { impl RewardableRadio { fn location_trust_multiplier(&self) -> Decimal { - const RESTRICTIVE_MAX_DISTANCE: Meters = Meters(50); - // CBRS radios are always trusted because they have internal GPS - match self.radio_type { - RadioType::IndoorCbrs => return dec!(1), - RadioType::OutdoorCbrs => return dec!(1), - _ => {} + if self.is_cbrs() { + return dec!(1); } - let trust_score_sum: Decimal = if self.any_hexes_boosted() { - // Cap multipliers to 0.25x when a radio covers _any_ boosted hex - // and it's distance to asserted is above the threshold. - self.location_trust_scores - .iter() - .map(|l| { - if l.distance_to_asserted > RESTRICTIVE_MAX_DISTANCE { - dec!(0.25).min(l.trust_score) - } else { - l.trust_score - } - }) - .sum() + if self.any_hexes_boosted() { + self.location_trust_scores.boosted_multiplier } else { - self.location_trust_scores - .iter() - .map(|l| l.trust_score) - .sum() - }; - - let trust_score_count = Decimal::from(self.location_trust_scores.len()); - trust_score_sum / trust_score_count + self.location_trust_scores.unboosted_multiplier + } } fn hex_boosting_multiplier(&self, hex: &CoveredHex) -> MaxOneMultplier { - let maybe_boost = if self.verified_radio_threshold { - hex.boosted.map_or(1, |boost| boost.get()) - } else { - 1 - }; - Decimal::from(maybe_boost) + // need to consider requirements from hip93 & hip84 before applying any boost + // hip93: if radio is wifi & location_trust score multiplier < 0.75, no boosting + if self.is_wifi() && self.location_trust_multiplier() < dec!(0.75) { + return dec!(1); + } + // hip84: if radio has not met minimum data and subscriber thresholds, no boosting + if !self.verified_radio_threshold { + return dec!(1); + } + + let boost = hex.boosted.map_or(1, |boost| boost.get()); + Decimal::from(boost) } fn speedtest_multiplier(&self) -> MaxOneMultplier { @@ -333,7 +380,21 @@ impl RewardableRadio { } fn any_hexes_boosted(&self) -> bool { - self.hexes.iter().any(|hex| hex.boosted.is_some()) + self.covered_hexes.any_boosted + } + + fn is_wifi(&self) -> bool { + matches!( + self.radio_type, + RadioType::IndoorWifi | RadioType::OutdoorWifi + ) + } + + fn is_cbrs(&self) -> bool { + matches!( + self.radio_type, + RadioType::IndoorCbrs | RadioType::OutdoorCbrs + ) } } @@ -345,69 +406,19 @@ mod tests { use super::*; use rust_decimal_macros::dec; - impl Assignments { - fn best() -> Self { - Self { - footfall: Assignment::A, - landtype: Assignment::A, - urbanized: Assignment::A, - } - } - } - - impl Speedtest { - fn best() -> Vec { - vec![ - Self { - upload_speed: BytesPs::mbps(15), - download_speed: BytesPs::mbps(150), - latency: Millis::new(15), - }, - Self { - upload_speed: BytesPs::mbps(15), - download_speed: BytesPs::mbps(150), - latency: Millis::new(15), - }, - ] - } - fn download(download: BytesPs) -> Self { - Self { - upload_speed: BytesPs::mbps(15), - download_speed: download, - latency: Millis::new(15), - } - } - } - - impl LocationTrust { - fn best() -> Vec { - vec![Self { - distance_to_asserted: Meters::new(1), - trust_score: dec!(1.0), - }] - } - - fn trust_score(trust_score: Decimal) -> Self { - Self { - distance_to_asserted: Meters::new(1), - trust_score, - } - } - } - #[test] fn speedtest() { let mut indoor_cbrs = RewardableRadio { radio_type: RadioType::IndoorCbrs, - speedtests: Speedtest::best(), - location_trust_scores: LocationTrust::best(), + speedtests: Speedtest::maximum(), + location_trust_scores: LocationTrustScores::maximum(), verified_radio_threshold: true, - hexes: vec![CoveredHex { + covered_hexes: CoveredHexes::new(vec![CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, - }], + }]), }; assert_eq!( @@ -474,10 +485,10 @@ mod tests { use Assignment::*; let indoor_cbrs = RewardableRadio { radio_type: RadioType::IndoorCbrs, - speedtests: Speedtest::best(), - location_trust_scores: LocationTrust::best(), + speedtests: Speedtest::maximum(), + location_trust_scores: LocationTrustScores::maximum(), verified_radio_threshold: true, - hexes: vec![ + covered_hexes: CoveredHexes::new(vec![ // yellow - POI ≥ 1 Urbanized local_hex(A, A, A), // 100 local_hex(A, B, A), // 100 @@ -512,7 +523,7 @@ mod tests { local_hex(C, A, C), // 0 local_hex(C, B, C), // 0 local_hex(C, C, C), // 0 - ], + ]), }; assert_eq!( @@ -525,35 +536,35 @@ mod tests { fn outdoor_radios_consider_top_3_ranked_hexes() { let outdoor_wifi = RewardableRadio { radio_type: RadioType::OutdoorWifi, - speedtests: Speedtest::best(), - location_trust_scores: LocationTrust::best(), + speedtests: Speedtest::maximum(), + location_trust_scores: LocationTrustScores::maximum(), verified_radio_threshold: true, - hexes: vec![ + covered_hexes: CoveredHexes::new(vec![ CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, }, CoveredHex { rank: Rank::new(2).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, }, CoveredHex { rank: Rank::new(3).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, }, CoveredHex { rank: Rank::new(42).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, }, - ], + ]), }; // rank 1 :: 1.00 * 16 == 16 @@ -570,29 +581,29 @@ mod tests { fn indoor_radios_only_consider_first_ranked_hexes() { let indoor_wifi = RewardableRadio { radio_type: RadioType::IndoorWifi, - speedtests: Speedtest::best(), - location_trust_scores: LocationTrust::best(), + speedtests: Speedtest::maximum(), + location_trust_scores: LocationTrustScores::maximum(), verified_radio_threshold: true, - hexes: vec![ + covered_hexes: CoveredHexes::new(vec![ CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, }, CoveredHex { rank: Rank::new(2).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, }, CoveredHex { rank: Rank::new(42).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, }, - ], + ]), }; assert_eq!( @@ -606,20 +617,20 @@ mod tests { // Location scores are averaged together let indoor_wifi = RewardableRadio { radio_type: RadioType::IndoorWifi, - speedtests: Speedtest::best(), - location_trust_scores: vec![ - LocationTrust::trust_score(dec!(0.1)), - LocationTrust::trust_score(dec!(0.2)), - LocationTrust::trust_score(dec!(0.3)), - LocationTrust::trust_score(dec!(0.4)), - ], + speedtests: Speedtest::maximum(), + location_trust_scores: LocationTrustScores::with_trust_scores(&[ + dec!(0.1), + dec!(0.2), + dec!(0.3), + dec!(0.4), + ]), verified_radio_threshold: true, - hexes: vec![CoveredHex { + covered_hexes: CoveredHexes::new(vec![CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, - }], + }]), }; // Location trust scores is 1/4 @@ -633,23 +644,23 @@ mod tests { fn boosted_hex() { let mut indoor_wifi = RewardableRadio { radio_type: RadioType::IndoorWifi, - speedtests: Speedtest::best(), - location_trust_scores: LocationTrust::best(), + speedtests: Speedtest::maximum(), + location_trust_scores: LocationTrustScores::maximum(), verified_radio_threshold: true, - hexes: vec![ + covered_hexes: CoveredHexes::new(vec![ CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, }, CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: Multiplier::new(4), }, - ], + ]), }; // The hex with a low signal_level is boosted to the same level as a // signal_level of High. @@ -670,110 +681,110 @@ mod tests { fn base_radio_coverage_points() { let outdoor_cbrs = RewardableRadio { radio_type: RadioType::OutdoorCbrs, - speedtests: Speedtest::best(), - location_trust_scores: LocationTrust::best(), + speedtests: Speedtest::maximum(), + location_trust_scores: LocationTrustScores::maximum(), verified_radio_threshold: true, - hexes: vec![ + covered_hexes: CoveredHexes::new(vec![ CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, }, CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Medium, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, }, CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, }, CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::None, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, }, - ], + ]), }; let indoor_cbrs = RewardableRadio { radio_type: RadioType::IndoorCbrs, - speedtests: Speedtest::best(), - location_trust_scores: LocationTrust::best(), + speedtests: Speedtest::maximum(), + location_trust_scores: LocationTrustScores::maximum(), verified_radio_threshold: true, - hexes: vec![ + covered_hexes: CoveredHexes::new(vec![ CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, }, CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, }, - ], + ]), }; let outdoor_wifi = RewardableRadio { radio_type: RadioType::OutdoorWifi, - speedtests: Speedtest::best(), - location_trust_scores: LocationTrust::best(), + speedtests: Speedtest::maximum(), + location_trust_scores: LocationTrustScores::maximum(), verified_radio_threshold: true, - hexes: vec![ + covered_hexes: CoveredHexes::new(vec![ CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, }, CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Medium, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, }, CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, }, CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::None, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, }, - ], + ]), }; let indoor_wifi = RewardableRadio { radio_type: RadioType::IndoorWifi, - speedtests: Speedtest::best(), - location_trust_scores: LocationTrust::best(), + speedtests: Speedtest::maximum(), + location_trust_scores: LocationTrustScores::maximum(), verified_radio_threshold: true, - hexes: vec![ + covered_hexes: CoveredHexes::new(vec![ CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, }, CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, - assignments: Assignments::best(), + assignments: Assignments::maximum(), boosted: None, }, - ], + ]), }; // When each radio contains a hex of every applicable signal_level, and @@ -795,4 +806,61 @@ mod tests { calculate_coverage_points(indoor_wifi).coverage_points ); } + + impl Assignments { + fn maximum() -> Self { + Self { + footfall: Assignment::A, + landtype: Assignment::A, + urbanized: Assignment::A, + } + } + } + + impl Speedtest { + fn maximum() -> Vec { + vec![ + Self { + upload_speed: BytesPs::mbps(15), + download_speed: BytesPs::mbps(150), + latency: Millis::new(15), + }, + Self { + upload_speed: BytesPs::mbps(15), + download_speed: BytesPs::mbps(150), + latency: Millis::new(15), + }, + ] + } + + fn download(download: BytesPs) -> Self { + Self { + upload_speed: BytesPs::mbps(15), + download_speed: download, + latency: Millis::new(15), + } + } + } + + impl LocationTrustScores { + fn maximum() -> Self { + Self::new(vec![LocationTrust { + distance_to_asserted: Meters::new(1), + trust_score: dec!(1.0), + }]) + } + + fn with_trust_scores(trust_scores: &[Decimal]) -> Self { + Self::new( + trust_scores + .to_owned() + .into_iter() + .map(|trust_score| LocationTrust { + distance_to_asserted: Meters::new(1), + trust_score, + }) + .collect(), + ) + } + } } From 92f5c69ceda84449482bd24dc7163d8005a80e7a Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 24 May 2024 17:32:37 -0700 Subject: [PATCH 027/115] break out location --- coverage_point_calculator/src/lib.rs | 116 ++++-------------- coverage_point_calculator/src/location.rs | 62 ++++++++++ .../tests/coverage_point_calculator.rs | 7 +- 3 files changed, 91 insertions(+), 94 deletions(-) create mode 100644 coverage_point_calculator/src/location.rs diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 87a58330a..4389df7e8 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -4,9 +4,7 @@ /// thorough explanation of many of them. It is not exhaustive, but a great /// place to start. /// -/// The coverage_points calculation in [`LocalRadio.coverage_points()`] are -/// comprised the following fields. -/// +/// ## Fields: /// - estimated_coverage_points /// - [HIP-74][modeled-coverage] /// - reduced cbrs radio coverage points [HIP-113][cbrs-experimental] @@ -15,27 +13,25 @@ /// - rank /// - [HIP-105][hex-limits] /// - hex_boost_multiplier -/// - [HIP-84][provider-boosting] +/// - must meet minimum subscriber thresholds [HIP-84][provider-boosting] +/// - Wifi Location trust score >0.75 for boosted hex eligibility [HIP-93][wifi-aps] /// - location_trust_score_multiplier /// - [HIP-98][qos-score] +/// - increase Boosted hex restriction, 30m -> 50m [HIP-93][boosted-hex-restriction] /// - speedtest_multiplier /// - [HIP-74][modeled-coverage] /// - added "Good" speedtest tier [HIP-98][qos-score] /// -/// [modeled-coverage]: -/// https://github.com/helium/HIP/blob/main/0074-mobile-poc-modeled-coverage-rewards.md#outdoor-radios -/// [cbrs-experimental]: -/// https://github.com/helium/HIP/blob/main/0113-reward-cbrs-as-experimental.md -/// [oracle-boosting]: -/// https://github.com/helium/HIP/blob/main/0103-oracle-hex-boosting.md -/// [hex-limits]: -/// https://github.com/helium/HIP/blob/main/0105-modification-of-mobile-subdao-hex-limits.md -/// [provider-boosting]: -/// https://github.com/helium/HIP/blob/main/0084-service-provider-hex-boosting.md#mechanics-and-price-of-boosting-hexes -/// [qos-score]: -/// https://github.com/helium/HIP/blob/main/0098-mobile-subdao-quality-of-service-requirements.md -/// [mobile-poc-blog]: -/// https://docs.helium.com/mobile/proof-of-coverage +/// ## References: +/// [modeled-coverage]: https://github.com/helium/HIP/blob/main/0074-mobile-poc-modeled-coverage-rewards.md#outdoor-radios +/// [cbrs-experimental]: https://github.com/helium/HIP/blob/main/0113-reward-cbrs-as-experimental.md +/// [oracle-boosting]: https://github.com/helium/HIP/blob/main/0103-oracle-hex-boosting.md +/// [hex-limits]: https://github.com/helium/HIP/blob/main/0105-modification-of-mobile-subdao-hex-limits.md +/// [provider-boosting]: https://github.com/helium/HIP/blob/main/0084-service-provider-hex-boosting.md +/// [qos-score]: https://github.com/helium/HIP/blob/main/0098-mobile-subdao-quality-of-service-requirements.md +/// [wifi-aps]: https://github.com/helium/HIP/blob/main/0093-addition-of-wifi-aps-to-mobile-subdao.md +/// [mobile-poc-blog]: https://docs.helium.com/mobile/proof-of-coverage +/// [boosted-hex-restriction]: https://github.com/helium/oracles/pull/808 /// /// To Integrate in Docs: /// @@ -46,21 +42,16 @@ /// for boosted hexes. /// https://github.com/helium/HIP/blob/8b1e814afa61a714b5ba63d3265e5897ab4c5116/0107-preventing-gaming-within-the-mobile-network.md /// -/// From Madninja: -/// A res 12 hex is 307m^2 which makes for a width of about 17m. -/// Since both the asserted location and the skyhook location are snapped to the -/// center of the res 12 hex they are in some distances are up over 30m (17 * 2 -/// > 30) away from where the hotspots actually are. In addition skyhook drift -/// can cause this to drift around a bit (we've seen 10-15m). To take both -/// snapping and some skyhook slop into account the distance between asserted -/// and inferred locations was increase from 30 to 50m -/// -pub mod speedtest; - -use crate::speedtest::{Speedtest, SpeedtestTier}; +use crate::{ + location::{LocationTrust, LocationTrustScores}, + speedtest::{Speedtest, SpeedtestTier}, +}; use rust_decimal::{Decimal, RoundingStrategy}; use rust_decimal_macros::dec; +pub mod location; +pub mod speedtest; + pub type Rank = std::num::NonZeroUsize; type Multiplier = std::num::NonZeroU32; pub type MaxOneMultplier = Decimal; @@ -132,66 +123,6 @@ pub fn make_rewardable_radio( } } -#[derive(Debug, Clone, PartialEq, PartialOrd)] -pub struct Meters(u32); - -impl Meters { - pub fn new(meters: u32) -> Self { - Self(meters) - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct LocationTrust { - pub distance_to_asserted: Meters, - pub trust_score: Decimal, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct LocationTrustScores { - boosted_multiplier: Decimal, - unboosted_multiplier: Decimal, - trust_scores: Vec, -} - -impl LocationTrustScores { - fn new(trust_scores: Vec) -> Self { - let boosted_multiplier = Self::boosted_multiplier(&trust_scores); - let unboosted_multiplier = Self::unboosted_multiplier(&trust_scores); - Self { - boosted_multiplier, - unboosted_multiplier, - trust_scores, - } - } - - fn boosted_multiplier(trust_scores: &[LocationTrust]) -> Decimal { - const RESTRICTIVE_MAX_DISTANCE: Meters = Meters(50); - // Cap multipliers to 0.25x when a radio covers _any_ boosted hex - // and it's distance to asserted is above the threshold. - let count = Decimal::from(trust_scores.len()); - let scores: Decimal = trust_scores - .iter() - .map(|l| { - if l.distance_to_asserted > RESTRICTIVE_MAX_DISTANCE { - dec!(0.25).min(l.trust_score) - } else { - l.trust_score - } - }) - .sum(); - - scores / count - } - - fn unboosted_multiplier(trust_scores: &[LocationTrust]) -> Decimal { - let count = Decimal::from(trust_scores.len()); - let scores: Decimal = trust_scores.iter().map(|l| l.trust_score).sum(); - - scores / count - } -} - #[derive(Debug, Clone, Copy, PartialEq)] pub enum RadioType { IndoorWifi, @@ -401,7 +332,10 @@ impl RewardableRadio { #[cfg(test)] mod tests { - use crate::speedtest::{BytesPs, Millis}; + use crate::{ + location::Meters, + speedtest::{BytesPs, Millis}, + }; use super::*; use rust_decimal_macros::dec; diff --git a/coverage_point_calculator/src/location.rs b/coverage_point_calculator/src/location.rs new file mode 100644 index 000000000..dff7edaaf --- /dev/null +++ b/coverage_point_calculator/src/location.rs @@ -0,0 +1,62 @@ +use rust_decimal::Decimal; +use rust_decimal_macros::dec; + +#[derive(Debug, Clone, PartialEq, PartialOrd)] +pub struct Meters(u32); + +impl Meters { + pub fn new(meters: u32) -> Self { + Self(meters) + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct LocationTrustScores { + pub boosted_multiplier: Decimal, + pub unboosted_multiplier: Decimal, + trust_scores: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct LocationTrust { + pub distance_to_asserted: Meters, + pub trust_score: Decimal, +} + +impl LocationTrustScores { + pub fn new(trust_scores: Vec) -> Self { + let boosted_multiplier = boosted_multiplier(&trust_scores); + let unboosted_multiplier = unboosted_multiplier(&trust_scores); + Self { + boosted_multiplier, + unboosted_multiplier, + trust_scores, + } + } +} + +fn boosted_multiplier(trust_scores: &[LocationTrust]) -> Decimal { + const RESTRICTIVE_MAX_DISTANCE: Meters = Meters(50); + // Cap multipliers to 0.25x when a radio covers _any_ boosted hex + // and it's distance to asserted is above the threshold. + let count = Decimal::from(trust_scores.len()); + let scores: Decimal = trust_scores + .iter() + .map(|l| { + if l.distance_to_asserted > RESTRICTIVE_MAX_DISTANCE { + dec!(0.25).min(l.trust_score) + } else { + l.trust_score + } + }) + .sum(); + + scores / count +} + +fn unboosted_multiplier(trust_scores: &[LocationTrust]) -> Decimal { + let count = Decimal::from(trust_scores.len()); + let scores: Decimal = trust_scores.iter().map(|l| l.trust_score).sum(); + + scores / count +} diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index 455ce5eb6..103421d05 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -1,10 +1,11 @@ use std::{collections::HashMap, num::NonZeroU32}; use coverage_point_calculator::{ - calculate_coverage_points, make_rewardable_radio, make_rewardable_radios, + calculate_coverage_points, + location::{LocationTrust, Meters}, + make_rewardable_radio, make_rewardable_radios, speedtest::{BytesPs, Millis, Speedtest}, - Assignment, Assignments, CoverageMap, CoveredHex, LocationTrust, Meters, Radio, RadioType, - Rank, SignalLevel, + Assignment, Assignments, CoverageMap, CoveredHex, Radio, RadioType, Rank, SignalLevel, }; use rust_decimal_macros::dec; From 347551f86b1e4f20383c9bab4b30d5098316cfcb Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 24 May 2024 17:40:48 -0700 Subject: [PATCH 028/115] to_mbps -> as_mbps Clippy wants the method to take ownership to follow the `to_*()` convention. --- coverage_point_calculator/src/speedtest.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/coverage_point_calculator/src/speedtest.rs b/coverage_point_calculator/src/speedtest.rs index ad0e1778f..fa83bffc8 100644 --- a/coverage_point_calculator/src/speedtest.rs +++ b/coverage_point_calculator/src/speedtest.rs @@ -15,7 +15,7 @@ impl BytesPs { Self(megabytes_per_second * 12500) } - fn to_mbps(&self) -> u64 { + fn as_mbps(&self) -> u64 { self.0 / 12500 } } @@ -97,7 +97,7 @@ impl SpeedtestTier { // I find this confusing fn from_download(bytes: &BytesPs) -> Self { - match bytes.to_mbps() { + match bytes.as_mbps() { 100.. => Self::Good, 75.. => Self::Acceptable, 50.. => Self::Degraded, @@ -107,7 +107,7 @@ impl SpeedtestTier { } fn from_upload(bytes: &BytesPs) -> Self { - match bytes.to_mbps() { + match bytes.as_mbps() { 10.. => Self::Good, 8.. => Self::Acceptable, 5.. => Self::Degraded, From d51612015b808af36556ee9c14f5c99c5b49231d Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 28 May 2024 14:49:35 -0700 Subject: [PATCH 029/115] Match HIP explicitly for latency values The original implementation allowed the upper limit as inclusive, and it was called out in PR as not matching the HIP. https://github.com/helium/oracles/pull/737 --- coverage_point_calculator/src/speedtest.rs | 27 +++++++--------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/coverage_point_calculator/src/speedtest.rs b/coverage_point_calculator/src/speedtest.rs index fa83bffc8..52fc27fe8 100644 --- a/coverage_point_calculator/src/speedtest.rs +++ b/coverage_point_calculator/src/speedtest.rs @@ -86,16 +86,6 @@ impl SpeedtestTier { } } - // FIXME: The modeled coverage blog post declares all comparisons as non-inclusive. - // And in the blog post, the UI shows what appear to be inclusive ranges. - // - // Speed Test Tier - // Acceptable 100+ Download, AND 10+ Upload, AND <50 Latency - // In the UI - // Acceptable: 0-50ms - // - // I find this confusing - fn from_download(bytes: &BytesPs) -> Self { match bytes.as_mbps() { 100.. => Self::Good, @@ -117,12 +107,11 @@ impl SpeedtestTier { } fn from_latency(Millis(millis): &Millis) -> Self { - // FIXME: comparison in mobile-verifier is non-inclusive match millis { - ..=50 => Self::Good, - ..=60 => Self::Acceptable, - ..=75 => Self::Degraded, - ..=100 => Self::Poor, + ..=49 => Self::Good, + ..=59 => Self::Acceptable, + ..=74 => Self::Degraded, + ..=99 => Self::Poor, _ => Self::Fail, } } @@ -150,10 +139,10 @@ mod tests { assert_eq!(Fail, SpeedtestTier::from_upload(&BytesPs::mbps(1))); // latency - assert_eq!(Good, SpeedtestTier::from_latency(&Millis::new(50))); - assert_eq!(Acceptable, SpeedtestTier::from_latency(&Millis::new(60))); - assert_eq!(Degraded, SpeedtestTier::from_latency(&Millis::new(75))); - assert_eq!(Poor, SpeedtestTier::from_latency(&Millis::new(100))); + assert_eq!(Good, SpeedtestTier::from_latency(&Millis::new(49))); + assert_eq!(Acceptable, SpeedtestTier::from_latency(&Millis::new(59))); + assert_eq!(Degraded, SpeedtestTier::from_latency(&Millis::new(74))); + assert_eq!(Poor, SpeedtestTier::from_latency(&Millis::new(99))); assert_eq!(Fail, SpeedtestTier::from_latency(&Millis::new(101))); } } From 6eed5817f67ed5b4188cfa29400c26b68caa4422 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 28 May 2024 14:50:05 -0700 Subject: [PATCH 030/115] add HIP specific tests --- coverage_point_calculator/src/lib.rs | 57 ++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 4389df7e8..39b6e161c 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -21,6 +21,7 @@ /// - speedtest_multiplier /// - [HIP-74][modeled-coverage] /// - added "Good" speedtest tier [HIP-98][qos-score] +/// - latency is explicitly under limit in HIP https://github.com/helium/oracles/pull/737 /// /// ## References: /// [modeled-coverage]: https://github.com/helium/HIP/blob/main/0074-mobile-poc-modeled-coverage-rewards.md#outdoor-radios @@ -340,6 +341,62 @@ mod tests { use super::*; use rust_decimal_macros::dec; + #[test] + fn hip_84_radio_meets_minimum_subscriber_threshold_for_boosted_hexes() { + let trusted_location = LocationTrustScores::with_trust_scores(&[dec!(1), dec!(1)]); + let untrusted_location = LocationTrustScores::with_trust_scores(&[dec!(0.1), dec!(0.2)]); + let mut wifi = RewardableRadio { + radio_type: RadioType::IndoorWifi, + speedtests: Speedtest::maximum(), + location_trust_scores: trusted_location, + verified_radio_threshold: true, + covered_hexes: CoveredHexes::new(vec![CoveredHex { + rank: Rank::new(1).unwrap(), + signal_level: SignalLevel::High, + assignments: Assignments::maximum(), + boosted: Multiplier::new(5), + }]), + }; + + let base_points = RadioType::IndoorWifi.base_coverage_points(&SignalLevel::High); + // Boosted Hex get's radio over the base_points + assert!(wifi.location_trust_multiplier() > dec!(0.75)); + assert!(calculate_coverage_points(wifi.clone()).coverage_points > base_points); + + // degraded location score get's radio under base_points + wifi.location_trust_scores = untrusted_location; + assert!(wifi.location_trust_multiplier() < dec!(0.75)); + assert!(calculate_coverage_points(wifi).coverage_points < base_points); + } + + #[test] + fn hip_93_wifi_with_low_location_score_receives_no_boosted_hexes() { + let trusted_location = LocationTrustScores::with_trust_scores(&[dec!(1), dec!(1)]); + let untrusted_location = LocationTrustScores::with_trust_scores(&[dec!(0.1), dec!(0.2)]); + let mut wifi = RewardableRadio { + radio_type: RadioType::IndoorWifi, + speedtests: Speedtest::maximum(), + location_trust_scores: trusted_location, + verified_radio_threshold: true, + covered_hexes: CoveredHexes::new(vec![CoveredHex { + rank: Rank::new(1).unwrap(), + signal_level: SignalLevel::High, + assignments: Assignments::maximum(), + boosted: Multiplier::new(5), + }]), + }; + + let base_points = RadioType::IndoorWifi.base_coverage_points(&SignalLevel::High); + // Boosted Hex get's radio over the base_points + assert!(wifi.location_trust_multiplier() > dec!(0.75)); + assert!(calculate_coverage_points(wifi.clone()).coverage_points > base_points); + + // degraded location score get's radio under base_points + wifi.location_trust_scores = untrusted_location; + assert!(wifi.location_trust_multiplier() < dec!(0.75)); + assert!(calculate_coverage_points(wifi).coverage_points < base_points); + } + #[test] fn speedtest() { let mut indoor_cbrs = RewardableRadio { From c4d842ac9c9d717288aa7b89de77078f9730a965 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 28 May 2024 14:55:17 -0700 Subject: [PATCH 031/115] Change verified radio threshold to enum Provides a named boolean to the status of meeting subscriber thresholds. Also provides a nice place to add new states in the case more HIPs are passed regarding subscribers thresholds. --- coverage_point_calculator/src/lib.rs | 42 ++++++++++++------- .../tests/coverage_point_calculator.rs | 9 ++-- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 39b6e161c..68b74fd13 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -62,7 +62,7 @@ pub trait Radio { fn radio_type(&self) -> RadioType; fn speedtests(&self) -> Vec; fn location_trust_scores(&self) -> Vec; - fn verified_radio_threshold(&self) -> bool; + fn verified_radio_threshold(&self) -> SubscriberThreshold; } pub trait CoverageMap { @@ -238,12 +238,18 @@ pub struct CoveragePoints { pub radio: RewardableRadio, } +#[derive(Debug, Clone, PartialEq)] +pub enum SubscriberThreshold { + Verified, + UnVerified, +} + #[derive(Debug, Clone, PartialEq)] pub struct RewardableRadio { pub radio_type: RadioType, pub speedtests: Vec, pub location_trust_scores: LocationTrustScores, - pub verified_radio_threshold: bool, + pub verified_radio_threshold: SubscriberThreshold, pub covered_hexes: CoveredHexes, } @@ -292,7 +298,7 @@ impl RewardableRadio { return dec!(1); } // hip84: if radio has not met minimum data and subscriber thresholds, no boosting - if !self.verified_radio_threshold { + if !self.subscriber_threshold_met() { return dec!(1); } @@ -328,6 +334,10 @@ impl RewardableRadio { RadioType::IndoorCbrs | RadioType::OutdoorCbrs ) } + + fn subscriber_threshold_met(&self) -> bool { + matches!(self.verified_radio_threshold, SubscriberThreshold::Verified) + } } #[cfg(test)] @@ -349,7 +359,7 @@ mod tests { radio_type: RadioType::IndoorWifi, speedtests: Speedtest::maximum(), location_trust_scores: trusted_location, - verified_radio_threshold: true, + verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, @@ -377,7 +387,7 @@ mod tests { radio_type: RadioType::IndoorWifi, speedtests: Speedtest::maximum(), location_trust_scores: trusted_location, - verified_radio_threshold: true, + verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, @@ -403,7 +413,7 @@ mod tests { radio_type: RadioType::IndoorCbrs, speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), - verified_radio_threshold: true, + verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, @@ -478,7 +488,7 @@ mod tests { radio_type: RadioType::IndoorCbrs, speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), - verified_radio_threshold: true, + verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ // yellow - POI ≥ 1 Urbanized local_hex(A, A, A), // 100 @@ -529,7 +539,7 @@ mod tests { radio_type: RadioType::OutdoorWifi, speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), - verified_radio_threshold: true, + verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ CoveredHex { rank: Rank::new(1).unwrap(), @@ -574,7 +584,7 @@ mod tests { radio_type: RadioType::IndoorWifi, speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), - verified_radio_threshold: true, + verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ CoveredHex { rank: Rank::new(1).unwrap(), @@ -615,7 +625,7 @@ mod tests { dec!(0.3), dec!(0.4), ]), - verified_radio_threshold: true, + verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![CoveredHex { rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, @@ -637,7 +647,7 @@ mod tests { radio_type: RadioType::IndoorWifi, speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), - verified_radio_threshold: true, + verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ CoveredHex { rank: Rank::new(1).unwrap(), @@ -661,7 +671,7 @@ mod tests { ); // When the radio is not verified for boosted rewards, the boost has no effect. - indoor_wifi.verified_radio_threshold = false; + indoor_wifi.verified_radio_threshold = SubscriberThreshold::UnVerified; assert_eq!( dec!(500), calculate_coverage_points(indoor_wifi).coverage_points @@ -674,7 +684,7 @@ mod tests { radio_type: RadioType::OutdoorCbrs, speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), - verified_radio_threshold: true, + verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ CoveredHex { rank: Rank::new(1).unwrap(), @@ -707,7 +717,7 @@ mod tests { radio_type: RadioType::IndoorCbrs, speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), - verified_radio_threshold: true, + verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ CoveredHex { rank: Rank::new(1).unwrap(), @@ -728,7 +738,7 @@ mod tests { radio_type: RadioType::OutdoorWifi, speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), - verified_radio_threshold: true, + verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ CoveredHex { rank: Rank::new(1).unwrap(), @@ -761,7 +771,7 @@ mod tests { radio_type: RadioType::IndoorWifi, speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), - verified_radio_threshold: true, + verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ CoveredHex { rank: Rank::new(1).unwrap(), diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index 103421d05..ad3350c87 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -6,6 +6,7 @@ use coverage_point_calculator::{ make_rewardable_radio, make_rewardable_radios, speedtest::{BytesPs, Millis, Speedtest}, Assignment, Assignments, CoverageMap, CoveredHex, Radio, RadioType, Rank, SignalLevel, + SubscriberThreshold, }; use rust_decimal_macros::dec; @@ -41,8 +42,8 @@ fn base_radio_coverage_points() { }] } - fn verified_radio_threshold(&self) -> bool { - true + fn verified_radio_threshold(&self) -> SubscriberThreshold { + SubscriberThreshold::Verified } } @@ -124,8 +125,8 @@ fn radio_unique_coverage() { }] } - fn verified_radio_threshold(&self) -> bool { - true + fn verified_radio_threshold(&self) -> SubscriberThreshold { + SubscriberThreshold::Verified } } From 073a0ee48eeb41782e73b4e7dcde84c2a8f81352 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 28 May 2024 15:06:30 -0700 Subject: [PATCH 032/115] more explicit name for precomputed multiplier values --- coverage_point_calculator/src/lib.rs | 4 ++-- coverage_point_calculator/src/location.rs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 68b74fd13..328be7939 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -285,9 +285,9 @@ impl RewardableRadio { } if self.any_hexes_boosted() { - self.location_trust_scores.boosted_multiplier + self.location_trust_scores.any_hex_boosted_multiplier } else { - self.location_trust_scores.unboosted_multiplier + self.location_trust_scores.no_boosted_hex_multiplier } } diff --git a/coverage_point_calculator/src/location.rs b/coverage_point_calculator/src/location.rs index dff7edaaf..d433d4c40 100644 --- a/coverage_point_calculator/src/location.rs +++ b/coverage_point_calculator/src/location.rs @@ -12,8 +12,8 @@ impl Meters { #[derive(Debug, Clone, PartialEq)] pub struct LocationTrustScores { - pub boosted_multiplier: Decimal, - pub unboosted_multiplier: Decimal, + pub any_hex_boosted_multiplier: Decimal, + pub no_boosted_hex_multiplier: Decimal, trust_scores: Vec, } @@ -28,8 +28,8 @@ impl LocationTrustScores { let boosted_multiplier = boosted_multiplier(&trust_scores); let unboosted_multiplier = unboosted_multiplier(&trust_scores); Self { - boosted_multiplier, - unboosted_multiplier, + any_hex_boosted_multiplier: boosted_multiplier, + no_boosted_hex_multiplier: unboosted_multiplier, trust_scores, } } From df0b365544a5ad4ef3a82591e14dcaf69898161b Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 29 May 2024 08:59:40 -0700 Subject: [PATCH 033/115] name calculcator consistently with other packages --- Cargo.lock | 2 +- coverage_point_calculator/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eeae46b57..52e33c8e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2131,7 +2131,7 @@ dependencies = [ ] [[package]] -name = "coverage_point_calculator" +name = "coverage-point-calculator" version = "0.1.0" dependencies = [ "hextree", diff --git a/coverage_point_calculator/Cargo.toml b/coverage_point_calculator/Cargo.toml index 46bbb1968..e276b880a 100644 --- a/coverage_point_calculator/Cargo.toml +++ b/coverage_point_calculator/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "coverage_point_calculator" +name = "coverage-point-calculator" version = "0.1.0" description = "Calculate Coverage Points for hotspots in the Mobile Network" authors.workspace = true From 84b10f39f96ea077ad96d2cfad88e4d2626a2fd0 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 29 May 2024 13:27:32 -0700 Subject: [PATCH 034/115] Add a user definable key for radios This may be hardcoded to a type in the future, but until we know that type, it's easy to make it generic and only enforce the radio returns the same type the coverage map is expecting --- coverage_point_calculator/src/lib.rs | 21 +++++++++--------- .../tests/coverage_point_calculator.rs | 22 ++++++++++++++----- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 328be7939..c5c35434b 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -54,19 +54,20 @@ pub mod location; pub mod speedtest; pub type Rank = std::num::NonZeroUsize; -type Multiplier = std::num::NonZeroU32; +pub type Multiplier = std::num::NonZeroU32; pub type MaxOneMultplier = Decimal; type Points = Decimal; -pub trait Radio { +pub trait Radio { + fn key(&self) -> Key; fn radio_type(&self) -> RadioType; fn speedtests(&self) -> Vec; fn location_trust_scores(&self) -> Vec; fn verified_radio_threshold(&self) -> SubscriberThreshold; } -pub trait CoverageMap { - fn hexes(&self, radio: &impl Radio) -> Vec; +pub trait CoverageMap { + fn hexes(&self, radio: &impl Radio) -> Vec; } pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { @@ -102,18 +103,18 @@ pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { } } -pub fn make_rewardable_radios<'a>( - radios: &'a [impl Radio], - coverage_map: &'a impl CoverageMap, +pub fn make_rewardable_radios<'a, K>( + radios: &'a [impl Radio], + coverage_map: &'a impl CoverageMap, ) -> impl Iterator + 'a { radios .iter() .map(|radio| make_rewardable_radio(radio, coverage_map)) } -pub fn make_rewardable_radio( - radio: &impl Radio, - coverage_map: &impl CoverageMap, +pub fn make_rewardable_radio( + radio: &impl Radio, + coverage_map: &impl CoverageMap, ) -> RewardableRadio { RewardableRadio { radio_type: radio.radio_type(), diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index ad3350c87..8c4e8887a 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -15,7 +15,11 @@ fn base_radio_coverage_points() { struct TestRadio(RadioType); struct TestCoverageMap; - impl Radio for TestRadio { + impl Radio<()> for TestRadio { + fn key(&self) -> () { + () + } + fn radio_type(&self) -> RadioType { self.0 } @@ -47,9 +51,10 @@ fn base_radio_coverage_points() { } } - impl CoverageMap for TestCoverageMap { - fn hexes(&self, _radio: &impl Radio) -> Vec { + impl CoverageMap<()> for TestCoverageMap { + fn hexes(&self, _radio: &impl Radio<()>) -> Vec { vec![CoveredHex { + cell: hextree::Cell::from_raw(0x8c2681a3064edff).unwrap(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignments: Assignments { @@ -98,7 +103,11 @@ fn radio_unique_coverage() { TestRadio(RadioType::OutdoorCbrs), ]; - impl Radio for TestRadio { + impl Radio<()> for TestRadio { + fn key(&self) -> () { + () + } + fn radio_type(&self) -> RadioType { self.0 } @@ -132,6 +141,7 @@ fn radio_unique_coverage() { // all radios will receive 400 coverage points let base_hex = CoveredHex { + cell: hextree::Cell::from_raw(0x8c2681a3064edff).unwrap(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignments: Assignments { @@ -152,8 +162,8 @@ fn radio_unique_coverage() { struct TestCoverageMap<'a>(HashMap<&'a str, Vec>); let coverage_map = TestCoverageMap(map); - impl CoverageMap for TestCoverageMap<'_> { - fn hexes(&self, radio: &impl Radio) -> Vec { + impl CoverageMap<()> for TestCoverageMap<'_> { + fn hexes(&self, radio: &impl Radio<()>) -> Vec { let key = match radio.radio_type() { RadioType::IndoorWifi => "indoor_wifi", RadioType::OutdoorWifi => "outdoor_wifi", From af08ee91ab8cfc3a0c42aec1d6ce432f6ee97101 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 29 May 2024 17:56:03 -0700 Subject: [PATCH 035/115] add cell for covered hex --- coverage_point_calculator/src/lib.rs | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index c5c35434b..76d306572 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -268,10 +268,18 @@ impl CoveredHexes { hexes: covered_hexes, } } + + pub fn boosted_hexes(&self) -> impl Iterator { + self.hexes + .clone() + .into_iter() + .filter(|hex| hex.boosted.is_some()) + } } #[derive(Debug, Clone, PartialEq)] pub struct CoveredHex { + pub cell: hextree::Cell, pub rank: Rank, pub signal_level: SignalLevel, pub assignments: Assignments, @@ -352,6 +360,10 @@ mod tests { use super::*; use rust_decimal_macros::dec; + fn hex_location() -> hextree::Cell { + hextree::Cell::from_raw(0x8c2681a3064edff).unwrap() + } + #[test] fn hip_84_radio_meets_minimum_subscriber_threshold_for_boosted_hexes() { let trusted_location = LocationTrustScores::with_trust_scores(&[dec!(1), dec!(1)]); @@ -362,6 +374,7 @@ mod tests { location_trust_scores: trusted_location, verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![CoveredHex { + cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignments: Assignments::maximum(), @@ -390,6 +403,7 @@ mod tests { location_trust_scores: trusted_location, verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![CoveredHex { + cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignments: Assignments::maximum(), @@ -416,6 +430,7 @@ mod tests { location_trust_scores: LocationTrustScores::maximum(), verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![CoveredHex { + cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignments: Assignments::maximum(), @@ -473,6 +488,7 @@ mod tests { urbanized: Assignment, ) -> CoveredHex { CoveredHex { + cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignments: Assignments { @@ -543,24 +559,28 @@ mod tests { verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ CoveredHex { + cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignments: Assignments::maximum(), boosted: None, }, CoveredHex { + cell: hex_location(), rank: Rank::new(2).unwrap(), signal_level: SignalLevel::High, assignments: Assignments::maximum(), boosted: None, }, CoveredHex { + cell: hex_location(), rank: Rank::new(3).unwrap(), signal_level: SignalLevel::High, assignments: Assignments::maximum(), boosted: None, }, CoveredHex { + cell: hex_location(), rank: Rank::new(42).unwrap(), signal_level: SignalLevel::High, assignments: Assignments::maximum(), @@ -588,18 +608,21 @@ mod tests { verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ CoveredHex { + cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignments: Assignments::maximum(), boosted: None, }, CoveredHex { + cell: hex_location(), rank: Rank::new(2).unwrap(), signal_level: SignalLevel::High, assignments: Assignments::maximum(), boosted: None, }, CoveredHex { + cell: hex_location(), rank: Rank::new(42).unwrap(), signal_level: SignalLevel::High, assignments: Assignments::maximum(), @@ -628,6 +651,7 @@ mod tests { ]), verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![CoveredHex { + cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignments: Assignments::maximum(), @@ -651,12 +675,14 @@ mod tests { verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ CoveredHex { + cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignments: Assignments::maximum(), boosted: None, }, CoveredHex { + cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, assignments: Assignments::maximum(), @@ -688,24 +714,28 @@ mod tests { verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ CoveredHex { + cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignments: Assignments::maximum(), boosted: None, }, CoveredHex { + cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Medium, assignments: Assignments::maximum(), boosted: None, }, CoveredHex { + cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, assignments: Assignments::maximum(), boosted: None, }, CoveredHex { + cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::None, assignments: Assignments::maximum(), @@ -721,12 +751,14 @@ mod tests { verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ CoveredHex { + cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignments: Assignments::maximum(), boosted: None, }, CoveredHex { + cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, assignments: Assignments::maximum(), @@ -742,24 +774,28 @@ mod tests { verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ CoveredHex { + cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignments: Assignments::maximum(), boosted: None, }, CoveredHex { + cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Medium, assignments: Assignments::maximum(), boosted: None, }, CoveredHex { + cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, assignments: Assignments::maximum(), boosted: None, }, CoveredHex { + cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::None, assignments: Assignments::maximum(), @@ -775,12 +811,14 @@ mod tests { verified_radio_threshold: SubscriberThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ CoveredHex { + cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, assignments: Assignments::maximum(), boosted: None, }, CoveredHex { + cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, assignments: Assignments::maximum(), From 913005d8e56ced06440dfe033b72883720340987 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 29 May 2024 17:56:39 -0700 Subject: [PATCH 036/115] helpers for mobile rewards report I don't know if i like all the helpers in here. but I'm not sure where to put them yet. --- coverage_point_calculator/src/lib.rs | 67 +++++++++++++++++++--------- 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 76d306572..31729c02d 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -71,26 +71,7 @@ pub trait CoverageMap { } pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { - let radio_type = &radio.radio_type; - - let rank_multipliers = radio_type.rank_multipliers(); - let max_rank = rank_multipliers.len(); - - let hex_points = radio - .covered_hexes - .hexes - .iter() - .filter(|hex| hex.rank.get() <= max_rank) - .map(|hex| { - let base_coverage_points = radio_type.base_coverage_points(&hex.signal_level); - let assignments_multiplier = hex.assignments.multiplier(); - let rank_multiplier = rank_multipliers[hex.rank.get() - 1]; - let hex_boost_multiplier = radio.hex_boosting_multiplier(hex); - - base_coverage_points * assignments_multiplier * rank_multiplier * hex_boost_multiplier - }); - - let base_points = hex_points.sum::(); + let base_points = radio.hex_coverage_points(); let location_score = radio.location_trust_multiplier(); let speedtest = radio.speedtest_multiplier(); @@ -239,6 +220,27 @@ pub struct CoveragePoints { pub radio: RewardableRadio, } +impl CoveragePoints { + // FIXME: Find a better way to communicate why values are sometimes multiplied by 1k + // Used to put into the proto, hence * dec!(1000) + pub fn reward_share_location_trust_multiplier(&self) -> u32 { + use rust_decimal::prelude::ToPrimitive; + let multiplier = self.radio.location_trust_multiplier() * dec!(1000); + multiplier.to_u32().unwrap_or_default() + } + + // Used to put into the proto, hence * dec!(1000) + pub fn reward_share_speedtest_multiplier(&self) -> u32 { + use rust_decimal::prelude::ToPrimitive; + let multiplier = self.radio.speedtest_multiplier() * dec!(1000); + multiplier.to_u32().unwrap_or_default() + } + + pub fn speedtest_multiplier(&self) -> Decimal { + self.radio.speedtest_multiplier() + } +} + #[derive(Debug, Clone, PartialEq)] pub enum SubscriberThreshold { Verified, @@ -254,6 +256,31 @@ pub struct RewardableRadio { pub covered_hexes: CoveredHexes, } +impl RewardableRadio { + // These points need to be reported in the proto pre-(location, speedtest) multipliers + pub fn hex_coverage_points(&self) -> Decimal { + let rank_multipliers = self.radio_type.rank_multipliers(); + let max_rank = rank_multipliers.len(); + + self.covered_hexes + .hexes + .iter() + .filter(|hex| hex.rank.get() <= max_rank) + .map(|hex| { + let base_coverage_points = self.radio_type.base_coverage_points(&hex.signal_level); + let assignments_multiplier = hex.assignments.multiplier(); + let rank_multiplier = rank_multipliers[hex.rank.get() - 1]; + let hex_boost_multiplier = self.hex_boosting_multiplier(hex); + + base_coverage_points + * assignments_multiplier + * rank_multiplier + * hex_boost_multiplier + }) + .sum() + } +} + #[derive(Debug, Clone, PartialEq)] pub struct CoveredHexes { any_boosted: bool, From 29b1dc688f3698798e69c5f4666933986370d3d5 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 30 May 2024 15:47:50 -0700 Subject: [PATCH 037/115] put all the boosted hex filtering in one place --- coverage_point_calculator/src/lib.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 31729c02d..653e161a9 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -279,6 +279,19 @@ impl RewardableRadio { }) .sum() } + + pub fn boosted_hexes(&self) -> impl Iterator { + let verified = self.verified_radio_threshold == SubscriberThreshold::Verified; + let trust_score_past_limit = self.location_trust_multiplier() > dec!(0.75); + + self.covered_hexes + .hexes + .clone() + .into_iter() + .filter(move |_| verified) + .filter(move |_| trust_score_past_limit) + .filter(|hex| hex.boosted.is_some_and(|boost| boost.get() > 1)) + } } #[derive(Debug, Clone, PartialEq)] @@ -295,13 +308,6 @@ impl CoveredHexes { hexes: covered_hexes, } } - - pub fn boosted_hexes(&self) -> impl Iterator { - self.hexes - .clone() - .into_iter() - .filter(|hex| hex.boosted.is_some()) - } } #[derive(Debug, Clone, PartialEq)] From 34fd895585f1ca264def9754f823caca9256a1e9 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 31 May 2024 10:42:34 -0700 Subject: [PATCH 038/115] coverage map trait takes key directly we're constructing a temporary rewardable radio to implement rewardable radio. If we make one directly, the coverage map doesn't have to care about the entire radio, just the key type. --- coverage_point_calculator/src/lib.rs | 20 +++++++++++++++++-- .../tests/coverage_point_calculator.rs | 14 ++++++------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 653e161a9..d1d2099ed 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -67,7 +67,7 @@ pub trait Radio { } pub trait CoverageMap { - fn hexes(&self, radio: &impl Radio) -> Vec; + fn hexes(&self, radio: &Key) -> Vec; } pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { @@ -102,7 +102,7 @@ pub fn make_rewardable_radio( speedtests: radio.speedtests(), location_trust_scores: LocationTrustScores::new(radio.location_trust_scores()), verified_radio_threshold: radio.verified_radio_threshold(), - covered_hexes: CoveredHexes::new(coverage_map.hexes(radio)), + covered_hexes: CoveredHexes::new(coverage_map.hexes(&radio.key())), } } @@ -257,6 +257,22 @@ pub struct RewardableRadio { } impl RewardableRadio { + pub fn new( + radio_type: RadioType, + speedtests: Vec, + location_trust_scores: Vec, + verified_radio_threshold: SubscriberThreshold, + covered_hexes: Vec, + ) -> Self { + Self { + radio_type, + speedtests, + location_trust_scores: LocationTrustScores::new(location_trust_scores), + verified_radio_threshold, + covered_hexes: CoveredHexes::new(covered_hexes), + } + } + // These points need to be reported in the proto pre-(location, speedtest) multipliers pub fn hex_coverage_points(&self) -> Decimal { let rank_multipliers = self.radio_type.rank_multipliers(); diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index 8c4e8887a..64476be43 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -52,7 +52,7 @@ fn base_radio_coverage_points() { } impl CoverageMap<()> for TestCoverageMap { - fn hexes(&self, _radio: &impl Radio<()>) -> Vec { + fn hexes(&self, _radio: &()) -> Vec { vec![CoveredHex { cell: hextree::Cell::from_raw(0x8c2681a3064edff).unwrap(), rank: Rank::new(1).unwrap(), @@ -103,9 +103,9 @@ fn radio_unique_coverage() { TestRadio(RadioType::OutdoorCbrs), ]; - impl Radio<()> for TestRadio { - fn key(&self) -> () { - () + impl Radio for TestRadio { + fn key(&self) -> RadioType { + self.0 } fn radio_type(&self) -> RadioType { @@ -162,9 +162,9 @@ fn radio_unique_coverage() { struct TestCoverageMap<'a>(HashMap<&'a str, Vec>); let coverage_map = TestCoverageMap(map); - impl CoverageMap<()> for TestCoverageMap<'_> { - fn hexes(&self, radio: &impl Radio<()>) -> Vec { - let key = match radio.radio_type() { + impl CoverageMap for TestCoverageMap<'_> { + fn hexes(&self, key: &RadioType) -> Vec { + let key = match key { RadioType::IndoorWifi => "indoor_wifi", RadioType::OutdoorWifi => "outdoor_wifi", RadioType::IndoorCbrs => "indoor_cbrs", From b3f79249b0bc862a7717124535856ba0af44eab3 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 31 May 2024 14:36:50 -0700 Subject: [PATCH 039/115] SubscriberThreshold -> RadioThreshold Thresholds have already moved away from being solely about subscribers. And this makes clearer the fact that a radio being considered "valid" can have many factors, not only subscribers. --- coverage_point_calculator/src/lib.rs | 44 +++++++++---------- .../tests/coverage_point_calculator.rs | 12 ++--- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index d1d2099ed..e279b7432 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -63,7 +63,7 @@ pub trait Radio { fn radio_type(&self) -> RadioType; fn speedtests(&self) -> Vec; fn location_trust_scores(&self) -> Vec; - fn verified_radio_threshold(&self) -> SubscriberThreshold; + fn verified_radio_threshold(&self) -> RadioThreshold; } pub trait CoverageMap { @@ -101,7 +101,7 @@ pub fn make_rewardable_radio( radio_type: radio.radio_type(), speedtests: radio.speedtests(), location_trust_scores: LocationTrustScores::new(radio.location_trust_scores()), - verified_radio_threshold: radio.verified_radio_threshold(), + radio_threshold: radio.verified_radio_threshold(), covered_hexes: CoveredHexes::new(coverage_map.hexes(&radio.key())), } } @@ -242,7 +242,7 @@ impl CoveragePoints { } #[derive(Debug, Clone, PartialEq)] -pub enum SubscriberThreshold { +pub enum RadioThreshold { Verified, UnVerified, } @@ -252,7 +252,7 @@ pub struct RewardableRadio { pub radio_type: RadioType, pub speedtests: Vec, pub location_trust_scores: LocationTrustScores, - pub verified_radio_threshold: SubscriberThreshold, + pub radio_threshold: RadioThreshold, pub covered_hexes: CoveredHexes, } @@ -261,14 +261,14 @@ impl RewardableRadio { radio_type: RadioType, speedtests: Vec, location_trust_scores: Vec, - verified_radio_threshold: SubscriberThreshold, + verified_radio_threshold: RadioThreshold, covered_hexes: Vec, ) -> Self { Self { radio_type, speedtests, location_trust_scores: LocationTrustScores::new(location_trust_scores), - verified_radio_threshold, + radio_threshold: verified_radio_threshold, covered_hexes: CoveredHexes::new(covered_hexes), } } @@ -356,7 +356,7 @@ impl RewardableRadio { return dec!(1); } // hip84: if radio has not met minimum data and subscriber thresholds, no boosting - if !self.subscriber_threshold_met() { + if !self.radio_threshold_met() { return dec!(1); } @@ -393,8 +393,8 @@ impl RewardableRadio { ) } - fn subscriber_threshold_met(&self) -> bool { - matches!(self.verified_radio_threshold, SubscriberThreshold::Verified) + fn radio_threshold_met(&self) -> bool { + matches!(self.radio_threshold, RadioThreshold::Verified) } } @@ -421,7 +421,7 @@ mod tests { radio_type: RadioType::IndoorWifi, speedtests: Speedtest::maximum(), location_trust_scores: trusted_location, - verified_radio_threshold: SubscriberThreshold::Verified, + radio_threshold: RadioThreshold::Verified, covered_hexes: CoveredHexes::new(vec![CoveredHex { cell: hex_location(), rank: Rank::new(1).unwrap(), @@ -450,7 +450,7 @@ mod tests { radio_type: RadioType::IndoorWifi, speedtests: Speedtest::maximum(), location_trust_scores: trusted_location, - verified_radio_threshold: SubscriberThreshold::Verified, + radio_threshold: RadioThreshold::Verified, covered_hexes: CoveredHexes::new(vec![CoveredHex { cell: hex_location(), rank: Rank::new(1).unwrap(), @@ -477,7 +477,7 @@ mod tests { radio_type: RadioType::IndoorCbrs, speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), - verified_radio_threshold: SubscriberThreshold::Verified, + radio_threshold: RadioThreshold::Verified, covered_hexes: CoveredHexes::new(vec![CoveredHex { cell: hex_location(), rank: Rank::new(1).unwrap(), @@ -554,7 +554,7 @@ mod tests { radio_type: RadioType::IndoorCbrs, speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), - verified_radio_threshold: SubscriberThreshold::Verified, + radio_threshold: RadioThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ // yellow - POI ≥ 1 Urbanized local_hex(A, A, A), // 100 @@ -605,7 +605,7 @@ mod tests { radio_type: RadioType::OutdoorWifi, speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), - verified_radio_threshold: SubscriberThreshold::Verified, + radio_threshold: RadioThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ CoveredHex { cell: hex_location(), @@ -654,7 +654,7 @@ mod tests { radio_type: RadioType::IndoorWifi, speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), - verified_radio_threshold: SubscriberThreshold::Verified, + radio_threshold: RadioThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ CoveredHex { cell: hex_location(), @@ -698,7 +698,7 @@ mod tests { dec!(0.3), dec!(0.4), ]), - verified_radio_threshold: SubscriberThreshold::Verified, + radio_threshold: RadioThreshold::Verified, covered_hexes: CoveredHexes::new(vec![CoveredHex { cell: hex_location(), rank: Rank::new(1).unwrap(), @@ -721,7 +721,7 @@ mod tests { radio_type: RadioType::IndoorWifi, speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), - verified_radio_threshold: SubscriberThreshold::Verified, + radio_threshold: RadioThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ CoveredHex { cell: hex_location(), @@ -747,7 +747,7 @@ mod tests { ); // When the radio is not verified for boosted rewards, the boost has no effect. - indoor_wifi.verified_radio_threshold = SubscriberThreshold::UnVerified; + indoor_wifi.radio_threshold = RadioThreshold::UnVerified; assert_eq!( dec!(500), calculate_coverage_points(indoor_wifi).coverage_points @@ -760,7 +760,7 @@ mod tests { radio_type: RadioType::OutdoorCbrs, speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), - verified_radio_threshold: SubscriberThreshold::Verified, + radio_threshold: RadioThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ CoveredHex { cell: hex_location(), @@ -797,7 +797,7 @@ mod tests { radio_type: RadioType::IndoorCbrs, speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), - verified_radio_threshold: SubscriberThreshold::Verified, + radio_threshold: RadioThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ CoveredHex { cell: hex_location(), @@ -820,7 +820,7 @@ mod tests { radio_type: RadioType::OutdoorWifi, speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), - verified_radio_threshold: SubscriberThreshold::Verified, + radio_threshold: RadioThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ CoveredHex { cell: hex_location(), @@ -857,7 +857,7 @@ mod tests { radio_type: RadioType::IndoorWifi, speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), - verified_radio_threshold: SubscriberThreshold::Verified, + radio_threshold: RadioThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ CoveredHex { cell: hex_location(), diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index 64476be43..52aaea4b3 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -5,8 +5,8 @@ use coverage_point_calculator::{ location::{LocationTrust, Meters}, make_rewardable_radio, make_rewardable_radios, speedtest::{BytesPs, Millis, Speedtest}, - Assignment, Assignments, CoverageMap, CoveredHex, Radio, RadioType, Rank, SignalLevel, - SubscriberThreshold, + Assignment, Assignments, CoverageMap, CoveredHex, Radio, RadioThreshold, RadioType, Rank, + SignalLevel, }; use rust_decimal_macros::dec; @@ -46,8 +46,8 @@ fn base_radio_coverage_points() { }] } - fn verified_radio_threshold(&self) -> SubscriberThreshold { - SubscriberThreshold::Verified + fn verified_radio_threshold(&self) -> RadioThreshold { + RadioThreshold::Verified } } @@ -134,8 +134,8 @@ fn radio_unique_coverage() { }] } - fn verified_radio_threshold(&self) -> SubscriberThreshold { - SubscriberThreshold::Verified + fn verified_radio_threshold(&self) -> RadioThreshold { + RadioThreshold::Verified } } From 3fdaf606d12c9b956797b777ae77ceacc968a645 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 31 May 2024 14:48:11 -0700 Subject: [PATCH 040/115] move boosted hexes iter to the mobile-verifier The mobile-verifier needs to be able to report on only the boosted hexes. The calculator is not concerned with that, but it can provide the necessary hooks for the mobile-verifier to create the boosted hexes report. --- coverage_point_calculator/src/lib.rs | 29 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index e279b7432..ef4b27fd5 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -296,17 +296,16 @@ impl RewardableRadio { .sum() } - pub fn boosted_hexes(&self) -> impl Iterator { - let verified = self.verified_radio_threshold == SubscriberThreshold::Verified; - let trust_score_past_limit = self.location_trust_multiplier() > dec!(0.75); + pub fn iter_covered_hexes(&self) -> impl Iterator { + self.covered_hexes.hexes.clone().into_iter() + } - self.covered_hexes - .hexes - .clone() - .into_iter() - .filter(move |_| verified) - .filter(move |_| trust_score_past_limit) - .filter(|hex| hex.boosted.is_some_and(|boost| boost.get() > 1)) + pub fn eligible_for_boosted_hexes(&self) -> bool { + self.location_trust_multiplier() > dec!(0.75) + } + + pub fn radio_threshold_met(&self) -> bool { + matches!(self.radio_threshold, RadioThreshold::Verified) } } @@ -335,6 +334,12 @@ pub struct CoveredHex { pub boosted: Option, } +impl CoveredHex { + pub fn is_boosted(&self) -> bool { + self.boosted.is_some_and(|boost| boost.get() > 1) + } +} + impl RewardableRadio { fn location_trust_multiplier(&self) -> Decimal { // CBRS radios are always trusted because they have internal GPS @@ -392,10 +397,6 @@ impl RewardableRadio { RadioType::IndoorCbrs | RadioType::OutdoorCbrs ) } - - fn radio_threshold_met(&self) -> bool { - matches!(self.radio_threshold, RadioThreshold::Verified) - } } #[cfg(test)] From 69568bb5c1ada3398f094373d2b60d865ed896d0 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 31 May 2024 17:48:31 -0700 Subject: [PATCH 041/115] use hex_assignments crate --- Cargo.lock | 1 + coverage_point_calculator/Cargo.toml | 7 +- coverage_point_calculator/src/lib.rs | 129 +++++------------- .../tests/coverage_point_calculator.rs | 8 +- 4 files changed, 46 insertions(+), 99 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 52e33c8e7..5d187c9e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2134,6 +2134,7 @@ dependencies = [ name = "coverage-point-calculator" version = "0.1.0" dependencies = [ + "hex-assignments", "hextree", "rust_decimal", "rust_decimal_macros", diff --git a/coverage_point_calculator/Cargo.toml b/coverage_point_calculator/Cargo.toml index e276b880a..999bffd4f 100644 --- a/coverage_point_calculator/Cargo.toml +++ b/coverage_point_calculator/Cargo.toml @@ -7,6 +7,7 @@ license.workspace = true edition.workspace = true [dependencies] -hextree.workspace = true -rust_decimal.workspace = true -rust_decimal_macros.workspace = true +hextree = { workspace = true } +rust_decimal = { workspace = true } +rust_decimal_macros = { workspace = true } +hex-assignments = { path = "../hex_assignments" } diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index ef4b27fd5..cd4b61596 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -160,60 +160,6 @@ pub enum SignalLevel { None, } -#[derive(Debug, Clone, PartialEq)] -pub struct Assignments { - pub footfall: Assignment, - pub landtype: Assignment, - pub urbanized: Assignment, -} - -#[derive(Debug, Clone, PartialEq)] -pub enum Assignment { - A, - B, - C, -} - -impl Assignments { - fn multiplier(&self) -> MaxOneMultplier { - let Assignments { - footfall, - urbanized, - landtype, - } = self; - - use Assignment::*; - match (footfall, landtype, urbanized) { - // yellow - POI ≥ 1 Urbanized - (A, A, A) => dec!(1.00), - (A, B, A) => dec!(1.00), - (A, C, A) => dec!(1.00), - // orange - POI ≥ 1 Not Urbanized - (A, A, B) => dec!(1.00), - (A, B, B) => dec!(1.00), - (A, C, B) => dec!(1.00), - // light green - Point of Interest Urbanized - (B, A, A) => dec!(0.70), - (B, B, A) => dec!(0.70), - (B, C, A) => dec!(0.70), - // dark green - Point of Interest Not Urbanized - (B, A, B) => dec!(0.50), - (B, B, B) => dec!(0.50), - (B, C, B) => dec!(0.50), - // light blue - No POI Urbanized - (C, A, A) => dec!(0.40), - (C, B, A) => dec!(0.30), - (C, C, A) => dec!(0.05), - // dark blue - No POI Not Urbanized - (C, A, B) => dec!(0.20), - (C, B, B) => dec!(0.15), - (C, C, B) => dec!(0.03), - // gray - Outside of USA - (_, _, C) => dec!(0.00), - } - } -} - #[derive(Debug)] pub struct CoveragePoints { pub coverage_points: Decimal, @@ -284,7 +230,7 @@ impl RewardableRadio { .filter(|hex| hex.rank.get() <= max_rank) .map(|hex| { let base_coverage_points = self.radio_type.base_coverage_points(&hex.signal_level); - let assignments_multiplier = hex.assignments.multiplier(); + let assignments_multiplier = hex.assignments.boosting_multiplier(); let rank_multiplier = rank_multipliers[hex.rank.get() - 1]; let hex_boost_multiplier = self.hex_boosting_multiplier(hex); @@ -330,7 +276,7 @@ pub struct CoveredHex { pub cell: hextree::Cell, pub rank: Rank, pub signal_level: SignalLevel, - pub assignments: Assignments, + pub assignments: HexAssignments, pub boosted: Option, } @@ -408,12 +354,21 @@ mod tests { }; use super::*; + use hex_assignments::Assignment; use rust_decimal_macros::dec; fn hex_location() -> hextree::Cell { hextree::Cell::from_raw(0x8c2681a3064edff).unwrap() } + fn assignments_maximum() -> HexAssignments { + HexAssignments { + footfall: Assignment::A, + landtype: Assignment::A, + urbanized: Assignment::A, + } + } + #[test] fn hip_84_radio_meets_minimum_subscriber_threshold_for_boosted_hexes() { let trusted_location = LocationTrustScores::with_trust_scores(&[dec!(1), dec!(1)]); @@ -427,7 +382,7 @@ mod tests { cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: Multiplier::new(5), }]), }; @@ -456,7 +411,7 @@ mod tests { cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: Multiplier::new(5), }]), }; @@ -483,7 +438,7 @@ mod tests { cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }]), }; @@ -541,7 +496,7 @@ mod tests { cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments { + assignments: HexAssignments { footfall, landtype, urbanized, @@ -612,28 +567,28 @@ mod tests { cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }, CoveredHex { cell: hex_location(), rank: Rank::new(2).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }, CoveredHex { cell: hex_location(), rank: Rank::new(3).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }, CoveredHex { cell: hex_location(), rank: Rank::new(42).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }, ]), @@ -661,21 +616,21 @@ mod tests { cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }, CoveredHex { cell: hex_location(), rank: Rank::new(2).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }, CoveredHex { cell: hex_location(), rank: Rank::new(42).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }, ]), @@ -704,7 +659,7 @@ mod tests { cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }]), }; @@ -728,14 +683,14 @@ mod tests { cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }, CoveredHex { cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: Multiplier::new(4), }, ]), @@ -767,28 +722,28 @@ mod tests { cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }, CoveredHex { cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Medium, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }, CoveredHex { cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }, CoveredHex { cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::None, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }, ]), @@ -804,14 +759,14 @@ mod tests { cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }, CoveredHex { cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }, ]), @@ -827,28 +782,28 @@ mod tests { cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }, CoveredHex { cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Medium, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }, CoveredHex { cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }, CoveredHex { cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::None, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }, ]), @@ -864,14 +819,14 @@ mod tests { cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }, CoveredHex { cell: hex_location(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::Low, - assignments: Assignments::maximum(), + assignments: assignments_maximum(), boosted: None, }, ]), @@ -897,16 +852,6 @@ mod tests { ); } - impl Assignments { - fn maximum() -> Self { - Self { - footfall: Assignment::A, - landtype: Assignment::A, - urbanized: Assignment::A, - } - } - } - impl Speedtest { fn maximum() -> Vec { vec![ diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index 52aaea4b3..1998983a4 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -5,9 +5,9 @@ use coverage_point_calculator::{ location::{LocationTrust, Meters}, make_rewardable_radio, make_rewardable_radios, speedtest::{BytesPs, Millis, Speedtest}, - Assignment, Assignments, CoverageMap, CoveredHex, Radio, RadioThreshold, RadioType, Rank, - SignalLevel, + CoverageMap, CoveredHex, Radio, RadioThreshold, RadioType, Rank, SignalLevel, }; +use hex_assignments::{assignment::HexAssignments, Assignment}; use rust_decimal_macros::dec; #[test] @@ -57,7 +57,7 @@ fn base_radio_coverage_points() { cell: hextree::Cell::from_raw(0x8c2681a3064edff).unwrap(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments { + assignments: HexAssignments { footfall: Assignment::A, landtype: Assignment::A, urbanized: Assignment::A, @@ -144,7 +144,7 @@ fn radio_unique_coverage() { cell: hextree::Cell::from_raw(0x8c2681a3064edff).unwrap(), rank: Rank::new(1).unwrap(), signal_level: SignalLevel::High, - assignments: Assignments { + assignments: HexAssignments { footfall: Assignment::A, landtype: Assignment::A, urbanized: Assignment::A, From 7547025138cac9e46f1b4273989e4becfe0c8163 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 31 May 2024 18:00:28 -0700 Subject: [PATCH 042/115] do not handle cleaning values for reports that belongs in mobile-verifier --- coverage_point_calculator/src/lib.rs | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index cd4b61596..3f9d45e50 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -166,27 +166,6 @@ pub struct CoveragePoints { pub radio: RewardableRadio, } -impl CoveragePoints { - // FIXME: Find a better way to communicate why values are sometimes multiplied by 1k - // Used to put into the proto, hence * dec!(1000) - pub fn reward_share_location_trust_multiplier(&self) -> u32 { - use rust_decimal::prelude::ToPrimitive; - let multiplier = self.radio.location_trust_multiplier() * dec!(1000); - multiplier.to_u32().unwrap_or_default() - } - - // Used to put into the proto, hence * dec!(1000) - pub fn reward_share_speedtest_multiplier(&self) -> u32 { - use rust_decimal::prelude::ToPrimitive; - let multiplier = self.radio.speedtest_multiplier() * dec!(1000); - multiplier.to_u32().unwrap_or_default() - } - - pub fn speedtest_multiplier(&self) -> Decimal { - self.radio.speedtest_multiplier() - } -} - #[derive(Debug, Clone, PartialEq)] pub enum RadioThreshold { Verified, @@ -287,7 +266,7 @@ impl CoveredHex { } impl RewardableRadio { - fn location_trust_multiplier(&self) -> Decimal { + pub fn location_trust_multiplier(&self) -> Decimal { // CBRS radios are always trusted because they have internal GPS if self.is_cbrs() { return dec!(1); @@ -315,7 +294,7 @@ impl RewardableRadio { Decimal::from(boost) } - fn speedtest_multiplier(&self) -> MaxOneMultplier { + pub fn speedtest_multiplier(&self) -> MaxOneMultplier { const MIN_REQUIRED_SPEEDTEST_SAMPLES: usize = 2; if self.speedtests.len() < MIN_REQUIRED_SPEEDTEST_SAMPLES { From 6f6d06abaabbd090b36da63163ee7f2d664c5fe4 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 31 May 2024 18:09:45 -0700 Subject: [PATCH 043/115] clippy --- coverage_point_calculator/src/lib.rs | 3 ++- coverage_point_calculator/tests/coverage_point_calculator.rs | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 3f9d45e50..d29b5b181 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -868,7 +868,8 @@ mod tests { Self::new( trust_scores .to_owned() - .into_iter() + .iter() + .copied() .map(|trust_score| LocationTrust { distance_to_asserted: Meters::new(1), trust_score, diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index 1998983a4..b3de1db05 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -16,9 +16,7 @@ fn base_radio_coverage_points() { struct TestCoverageMap; impl Radio<()> for TestRadio { - fn key(&self) -> () { - () - } + fn key(&self) {} fn radio_type(&self) -> RadioType { self.0 From 0f01422bcdd753df3da57c0a0076c686b2d49970 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Mon, 3 Jun 2024 12:12:27 -0700 Subject: [PATCH 044/115] include hex_assignments crate Thought I got this with a previous commit, apparently not. Also add some spacing for readability --- coverage_point_calculator/src/lib.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index d29b5b181..01b3a1262 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -5,19 +5,24 @@ /// place to start. /// /// ## Fields: -/// - estimated_coverage_points +/// - modeled_coverage_points /// - [HIP-74][modeled-coverage] /// - reduced cbrs radio coverage points [HIP-113][cbrs-experimental] +/// /// - assignment_multiplier /// - [HIP-103][oracle-boosting] +/// /// - rank /// - [HIP-105][hex-limits] +/// /// - hex_boost_multiplier /// - must meet minimum subscriber thresholds [HIP-84][provider-boosting] /// - Wifi Location trust score >0.75 for boosted hex eligibility [HIP-93][wifi-aps] +/// /// - location_trust_score_multiplier /// - [HIP-98][qos-score] /// - increase Boosted hex restriction, 30m -> 50m [HIP-93][boosted-hex-restriction] +/// /// - speedtest_multiplier /// - [HIP-74][modeled-coverage] /// - added "Good" speedtest tier [HIP-98][qos-score] @@ -47,6 +52,7 @@ use crate::{ location::{LocationTrust, LocationTrustScores}, speedtest::{Speedtest, SpeedtestTier}, }; +use hex_assignments::assignment::HexAssignments; use rust_decimal::{Decimal, RoundingStrategy}; use rust_decimal_macros::dec; From 31959588fbd96e26da4a7507d1fcdd36194fda1b Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Mon, 3 Jun 2024 12:13:00 -0700 Subject: [PATCH 045/115] Include coverage-map to start integration --- Cargo.lock | 1 + coverage_point_calculator/Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 5d187c9e6..cbbfe23f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2134,6 +2134,7 @@ dependencies = [ name = "coverage-point-calculator" version = "0.1.0" dependencies = [ + "coverage-map", "hex-assignments", "hextree", "rust_decimal", diff --git a/coverage_point_calculator/Cargo.toml b/coverage_point_calculator/Cargo.toml index 999bffd4f..6cd9e79c8 100644 --- a/coverage_point_calculator/Cargo.toml +++ b/coverage_point_calculator/Cargo.toml @@ -11,3 +11,4 @@ hextree = { workspace = true } rust_decimal = { workspace = true } rust_decimal_macros = { workspace = true } hex-assignments = { path = "../hex_assignments" } +coverage-map = { path = "../coverage_map" } From 3f721e1e4c7d74cd0d132acbbc304fc64051a056 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Mon, 3 Jun 2024 12:19:54 -0700 Subject: [PATCH 046/115] Use SignalLevel from coverage_map --- coverage_point_calculator/src/lib.rs | 9 +-------- .../tests/coverage_point_calculator.rs | 3 ++- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 01b3a1262..843adfca1 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -52,6 +52,7 @@ use crate::{ location::{LocationTrust, LocationTrustScores}, speedtest::{Speedtest, SpeedtestTier}, }; +use coverage_map::SignalLevel; use hex_assignments::assignment::HexAssignments; use rust_decimal::{Decimal, RoundingStrategy}; use rust_decimal_macros::dec; @@ -158,14 +159,6 @@ impl RadioType { } } -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum SignalLevel { - High, - Medium, - Low, - None, -} - #[derive(Debug)] pub struct CoveragePoints { pub coverage_points: Decimal, diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index b3de1db05..3d6f15b02 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -1,11 +1,12 @@ use std::{collections::HashMap, num::NonZeroU32}; +use coverage_map::SignalLevel; use coverage_point_calculator::{ calculate_coverage_points, location::{LocationTrust, Meters}, make_rewardable_radio, make_rewardable_radios, speedtest::{BytesPs, Millis, Speedtest}, - CoverageMap, CoveredHex, Radio, RadioThreshold, RadioType, Rank, SignalLevel, + CoverageMap, CoveredHex, Radio, RadioThreshold, RadioType, Rank, }; use hex_assignments::{assignment::HexAssignments, Assignment}; use rust_decimal_macros::dec; From 38584b6de1282c0b4218de4822f236ec8bcec2c7 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 4 Jun 2024 11:50:55 -0700 Subject: [PATCH 047/115] Remove unused radio trait --- coverage_point_calculator/src/lib.rs | 35 +--- .../tests/coverage_point_calculator.rs | 163 ++++++++---------- 2 files changed, 73 insertions(+), 125 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 843adfca1..22957112b 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -65,16 +65,9 @@ pub type Multiplier = std::num::NonZeroU32; pub type MaxOneMultplier = Decimal; type Points = Decimal; -pub trait Radio { - fn key(&self) -> Key; - fn radio_type(&self) -> RadioType; - fn speedtests(&self) -> Vec; - fn location_trust_scores(&self) -> Vec; - fn verified_radio_threshold(&self) -> RadioThreshold; -} - -pub trait CoverageMap { - fn hexes(&self, radio: &Key) -> Vec; +// TODO: Going the way of D.O.D.O - Neil Stephenson +pub trait CoverageMapExt { + fn hexes(&self, radio: &Key, radio_type: &RadioType) -> Vec; } pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { @@ -91,28 +84,6 @@ pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { } } -pub fn make_rewardable_radios<'a, K>( - radios: &'a [impl Radio], - coverage_map: &'a impl CoverageMap, -) -> impl Iterator + 'a { - radios - .iter() - .map(|radio| make_rewardable_radio(radio, coverage_map)) -} - -pub fn make_rewardable_radio( - radio: &impl Radio, - coverage_map: &impl CoverageMap, -) -> RewardableRadio { - RewardableRadio { - radio_type: radio.radio_type(), - speedtests: radio.speedtests(), - location_trust_scores: LocationTrustScores::new(radio.location_trust_scores()), - radio_threshold: radio.verified_radio_threshold(), - covered_hexes: CoveredHexes::new(coverage_map.hexes(&radio.key())), - } -} - #[derive(Debug, Clone, Copy, PartialEq)] pub enum RadioType { IndoorWifi, diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index 3d6f15b02..48503c888 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -4,53 +4,34 @@ use coverage_map::SignalLevel; use coverage_point_calculator::{ calculate_coverage_points, location::{LocationTrust, Meters}, - make_rewardable_radio, make_rewardable_radios, speedtest::{BytesPs, Millis, Speedtest}, - CoverageMap, CoveredHex, Radio, RadioThreshold, RadioType, Rank, + CoverageMapExt, CoveredHex, RadioThreshold, RadioType, Rank, RewardableRadio, }; use hex_assignments::{assignment::HexAssignments, Assignment}; use rust_decimal_macros::dec; #[test] fn base_radio_coverage_points() { - struct TestRadio(RadioType); - struct TestCoverageMap; - - impl Radio<()> for TestRadio { - fn key(&self) {} - - fn radio_type(&self) -> RadioType { - self.0 - } - - fn speedtests(&self) -> Vec { - vec![ - Speedtest { - upload_speed: BytesPs::mbps(15), - download_speed: BytesPs::mbps(150), - latency: Millis::new(15), - }, - Speedtest { - upload_speed: BytesPs::mbps(15), - download_speed: BytesPs::mbps(150), - latency: Millis::new(15), - }, - ] - } - - fn location_trust_scores(&self) -> Vec { - vec![LocationTrust { - distance_to_asserted: Meters::new(1), - trust_score: dec!(1.0), - }] - } + let speedtests = vec![ + Speedtest { + upload_speed: BytesPs::mbps(15), + download_speed: BytesPs::mbps(150), + latency: Millis::new(15), + }, + Speedtest { + upload_speed: BytesPs::mbps(15), + download_speed: BytesPs::mbps(150), + latency: Millis::new(15), + }, + ]; + let location_trust_scores = vec![LocationTrust { + distance_to_asserted: Meters::new(1), + trust_score: dec!(1.0), + }]; - fn verified_radio_threshold(&self) -> RadioThreshold { - RadioThreshold::Verified - } - } + struct TestCoverageMap; - impl CoverageMap<()> for TestCoverageMap { + impl CoverageMapExt<()> for TestCoverageMap { fn hexes(&self, _radio: &()) -> Vec { vec![CoveredHex { cell: hextree::Cell::from_raw(0x8c2681a3064edff).unwrap(), @@ -66,26 +47,29 @@ fn base_radio_coverage_points() { } } + let mut radios = vec![]; for radio_type in [ RadioType::IndoorWifi, RadioType::IndoorCbrs, RadioType::OutdoorWifi, RadioType::OutdoorCbrs, ] { - let radio = make_rewardable_radio(&TestRadio(radio_type), &TestCoverageMap); + let radio = RewardableRadio::new( + radio_type, + speedtests.clone(), + location_trust_scores.clone(), + RadioThreshold::Verified, + TestCoverageMap.hexes(&()), + ); + radios.push(radio.clone()); println!( "{radio_type:?} \t--> {}", calculate_coverage_points(radio).coverage_points ); } - let radios = vec![ - TestRadio(RadioType::IndoorWifi), - TestRadio(RadioType::IndoorCbrs), - TestRadio(RadioType::OutdoorWifi), - TestRadio(RadioType::OutdoorCbrs), - ]; - let output = make_rewardable_radios(&radios, &TestCoverageMap) + let output = radios + .into_iter() .map(|r| (r.radio_type, calculate_coverage_points(r).coverage_points)) .collect::>(); println!("{output:#?}"); @@ -93,51 +77,6 @@ fn base_radio_coverage_points() { #[test] fn radio_unique_coverage() { - struct TestRadio(RadioType); - - let radios = vec![ - TestRadio(RadioType::IndoorWifi), - TestRadio(RadioType::IndoorCbrs), - TestRadio(RadioType::OutdoorWifi), - TestRadio(RadioType::OutdoorCbrs), - ]; - - impl Radio for TestRadio { - fn key(&self) -> RadioType { - self.0 - } - - fn radio_type(&self) -> RadioType { - self.0 - } - - fn speedtests(&self) -> Vec { - vec![ - Speedtest { - upload_speed: BytesPs::mbps(15), - download_speed: BytesPs::mbps(150), - latency: Millis::new(15), - }, - Speedtest { - upload_speed: BytesPs::mbps(15), - download_speed: BytesPs::mbps(150), - latency: Millis::new(15), - }, - ] - } - - fn location_trust_scores(&self) -> Vec { - vec![LocationTrust { - distance_to_asserted: Meters::new(1), - trust_score: dec!(1.0), - }] - } - - fn verified_radio_threshold(&self) -> RadioThreshold { - RadioThreshold::Verified - } - } - // all radios will receive 400 coverage points let base_hex = CoveredHex { cell: hextree::Cell::from_raw(0x8c2681a3064edff).unwrap(), @@ -161,7 +100,7 @@ fn radio_unique_coverage() { struct TestCoverageMap<'a>(HashMap<&'a str, Vec>); let coverage_map = TestCoverageMap(map); - impl CoverageMap for TestCoverageMap<'_> { + impl CoverageMapExt for TestCoverageMap<'_> { fn hexes(&self, key: &RadioType) -> Vec { let key = match key { RadioType::IndoorWifi => "indoor_wifi", @@ -173,8 +112,46 @@ fn radio_unique_coverage() { } } - let coverage_points = make_rewardable_radios(&radios, &coverage_map) + let default_speedtests = vec![ + Speedtest { + upload_speed: BytesPs::mbps(15), + download_speed: BytesPs::mbps(150), + latency: Millis::new(15), + }, + Speedtest { + upload_speed: BytesPs::mbps(15), + download_speed: BytesPs::mbps(150), + latency: Millis::new(15), + }, + ]; + let default_location_trust_scores = vec![LocationTrust { + distance_to_asserted: Meters::new(1), + trust_score: dec!(1.0), + }]; + + let mut radios = vec![]; + for radio_type in [ + RadioType::IndoorWifi, + RadioType::IndoorCbrs, + RadioType::OutdoorWifi, + RadioType::OutdoorCbrs, + ] { + radios.push(RewardableRadio::new( + radio_type, + default_speedtests.clone(), + default_location_trust_scores.clone(), + RadioThreshold::Verified, + coverage_map.hexes(&radio_type), + )); + } + + let coverage_points = radios + .into_iter() .map(|r| (r.radio_type, calculate_coverage_points(r).coverage_points)) .collect::>(); + + // let coverage_points = make_rewardable_radios(&radios, &coverage_map) + // .map(|r| (r.radio_type, calculate_coverage_points(r).coverage_points)) + // .collect::>(); println!("{coverage_points:#?}") } From b9f985e07700254d9e6b1040e95d605e4540f934 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 4 Jun 2024 11:58:50 -0700 Subject: [PATCH 048/115] remove coveragemap trait the coverage map is going to be used before entering this crate, so we don't need to put a restriction around it. The restriction will be that we only accept types provided from that crate. --- coverage_point_calculator/src/lib.rs | 5 -- .../tests/coverage_point_calculator.rs | 55 ++++++++----------- 2 files changed, 22 insertions(+), 38 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 22957112b..fcee96a0e 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -65,11 +65,6 @@ pub type Multiplier = std::num::NonZeroU32; pub type MaxOneMultplier = Decimal; type Points = Decimal; -// TODO: Going the way of D.O.D.O - Neil Stephenson -pub trait CoverageMapExt { - fn hexes(&self, radio: &Key, radio_type: &RadioType) -> Vec; -} - pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { let base_points = radio.hex_coverage_points(); let location_score = radio.location_trust_multiplier(); diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index 48503c888..debc1f9a2 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -5,7 +5,7 @@ use coverage_point_calculator::{ calculate_coverage_points, location::{LocationTrust, Meters}, speedtest::{BytesPs, Millis, Speedtest}, - CoverageMapExt, CoveredHex, RadioThreshold, RadioType, Rank, RewardableRadio, + CoveredHex, RadioThreshold, RadioType, Rank, RewardableRadio, }; use hex_assignments::{assignment::HexAssignments, Assignment}; use rust_decimal_macros::dec; @@ -29,23 +29,17 @@ fn base_radio_coverage_points() { trust_score: dec!(1.0), }]; - struct TestCoverageMap; - - impl CoverageMapExt<()> for TestCoverageMap { - fn hexes(&self, _radio: &()) -> Vec { - vec![CoveredHex { - cell: hextree::Cell::from_raw(0x8c2681a3064edff).unwrap(), - rank: Rank::new(1).unwrap(), - signal_level: SignalLevel::High, - assignments: HexAssignments { - footfall: Assignment::A, - landtype: Assignment::A, - urbanized: Assignment::A, - }, - boosted: NonZeroU32::new(0), - }] - } - } + let hexes = vec![CoveredHex { + cell: hextree::Cell::from_raw(0x8c2681a3064edff).unwrap(), + rank: Rank::new(1).unwrap(), + signal_level: SignalLevel::High, + assignments: HexAssignments { + footfall: Assignment::A, + landtype: Assignment::A, + urbanized: Assignment::A, + }, + boosted: NonZeroU32::new(0), + }]; let mut radios = vec![]; for radio_type in [ @@ -59,7 +53,7 @@ fn base_radio_coverage_points() { speedtests.clone(), location_trust_scores.clone(), RadioThreshold::Verified, - TestCoverageMap.hexes(&()), + hexes.clone(), ); radios.push(radio.clone()); println!( @@ -97,19 +91,14 @@ fn radio_unique_coverage() { map.insert("outdoor_wifi", hex.clone().take(25).collect()); map.insert("outdoor_cbrs", hex.clone().take(100).collect()); - struct TestCoverageMap<'a>(HashMap<&'a str, Vec>); - let coverage_map = TestCoverageMap(map); - - impl CoverageMapExt for TestCoverageMap<'_> { - fn hexes(&self, key: &RadioType) -> Vec { - let key = match key { - RadioType::IndoorWifi => "indoor_wifi", - RadioType::OutdoorWifi => "outdoor_wifi", - RadioType::IndoorCbrs => "indoor_cbrs", - RadioType::OutdoorCbrs => "outdoor_cbrs", - }; - self.0.get(key).unwrap().clone() - } + fn hexes(coverage_map: &HashMap<&str, Vec>, key: &RadioType) -> Vec { + let key = match key { + RadioType::IndoorWifi => "indoor_wifi", + RadioType::OutdoorWifi => "outdoor_wifi", + RadioType::IndoorCbrs => "indoor_cbrs", + RadioType::OutdoorCbrs => "outdoor_cbrs", + }; + coverage_map.get(key).unwrap().clone() } let default_speedtests = vec![ @@ -141,7 +130,7 @@ fn radio_unique_coverage() { default_speedtests.clone(), default_location_trust_scores.clone(), RadioThreshold::Verified, - coverage_map.hexes(&radio_type), + hexes(&map, &radio_type), )); } From a62b2765b787a76a6314bec42e045e78e247b68d Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 4 Jun 2024 14:51:03 -0700 Subject: [PATCH 049/115] use RankedCoverage from coverage map requires pulling in helium-crypto as a dev dependency for testing --- Cargo.lock | 1 + coverage_point_calculator/Cargo.toml | 3 + coverage_point_calculator/src/lib.rs | 258 ++++++++++-------- .../tests/coverage_point_calculator.rs | 37 ++- 4 files changed, 182 insertions(+), 117 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cbbfe23f3..505faf358 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2135,6 +2135,7 @@ name = "coverage-point-calculator" version = "0.1.0" dependencies = [ "coverage-map", + "helium-crypto", "hex-assignments", "hextree", "rust_decimal", diff --git a/coverage_point_calculator/Cargo.toml b/coverage_point_calculator/Cargo.toml index 6cd9e79c8..062acb6b4 100644 --- a/coverage_point_calculator/Cargo.toml +++ b/coverage_point_calculator/Cargo.toml @@ -12,3 +12,6 @@ rust_decimal = { workspace = true } rust_decimal_macros = { workspace = true } hex-assignments = { path = "../hex_assignments" } coverage-map = { path = "../coverage_map" } + +[dev-dependencies] +helium-crypto = { workspace = true } diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index fcee96a0e..80aca0115 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -52,15 +52,13 @@ use crate::{ location::{LocationTrust, LocationTrustScores}, speedtest::{Speedtest, SpeedtestTier}, }; -use coverage_map::SignalLevel; -use hex_assignments::assignment::HexAssignments; +use coverage_map::{RankedCoverage, SignalLevel}; use rust_decimal::{Decimal, RoundingStrategy}; use rust_decimal_macros::dec; pub mod location; pub mod speedtest; -pub type Rank = std::num::NonZeroUsize; pub type Multiplier = std::num::NonZeroU32; pub type MaxOneMultplier = Decimal; type Points = Decimal; @@ -137,7 +135,7 @@ pub enum RadioThreshold { UnVerified, } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct RewardableRadio { pub radio_type: RadioType, pub speedtests: Vec, @@ -152,7 +150,7 @@ impl RewardableRadio { speedtests: Vec, location_trust_scores: Vec, verified_radio_threshold: RadioThreshold, - covered_hexes: Vec, + covered_hexes: Vec, ) -> Self { Self { radio_type, @@ -171,11 +169,11 @@ impl RewardableRadio { self.covered_hexes .hexes .iter() - .filter(|hex| hex.rank.get() <= max_rank) + .filter(|hex| hex.rank <= max_rank) .map(|hex| { let base_coverage_points = self.radio_type.base_coverage_points(&hex.signal_level); let assignments_multiplier = hex.assignments.boosting_multiplier(); - let rank_multiplier = rank_multipliers[hex.rank.get() - 1]; + let rank_multiplier = rank_multipliers[hex.rank - 1]; let hex_boost_multiplier = self.hex_boosting_multiplier(hex); base_coverage_points @@ -186,7 +184,7 @@ impl RewardableRadio { .sum() } - pub fn iter_covered_hexes(&self) -> impl Iterator { + pub fn iter_covered_hexes(&self) -> impl Iterator { self.covered_hexes.hexes.clone().into_iter() } @@ -199,14 +197,14 @@ impl RewardableRadio { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct CoveredHexes { any_boosted: bool, - hexes: Vec, + hexes: Vec, } impl CoveredHexes { - fn new(covered_hexes: Vec) -> Self { + fn new(covered_hexes: Vec) -> Self { let any_boosted = covered_hexes.iter().any(|hex| hex.boosted.is_some()); Self { any_boosted, @@ -215,21 +213,6 @@ impl CoveredHexes { } } -#[derive(Debug, Clone, PartialEq)] -pub struct CoveredHex { - pub cell: hextree::Cell, - pub rank: Rank, - pub signal_level: SignalLevel, - pub assignments: HexAssignments, - pub boosted: Option, -} - -impl CoveredHex { - pub fn is_boosted(&self) -> bool { - self.boosted.is_some_and(|boost| boost.get() > 1) - } -} - impl RewardableRadio { pub fn location_trust_multiplier(&self) -> Decimal { // CBRS radios are always trusted because they have internal GPS @@ -244,7 +227,7 @@ impl RewardableRadio { } } - fn hex_boosting_multiplier(&self, hex: &CoveredHex) -> MaxOneMultplier { + fn hex_boosting_multiplier(&self, hex: &RankedCoverage) -> MaxOneMultplier { // need to consider requirements from hip93 & hip84 before applying any boost // hip93: if radio is wifi & location_trust score multiplier < 0.75, no boosting if self.is_wifi() && self.location_trust_multiplier() < dec!(0.75) { @@ -292,13 +275,15 @@ impl RewardableRadio { #[cfg(test)] mod tests { + use std::str::FromStr; + use crate::{ location::Meters, speedtest::{BytesPs, Millis}, }; use super::*; - use hex_assignments::Assignment; + use hex_assignments::{assignment::HexAssignments, Assignment}; use rust_decimal_macros::dec; fn hex_location() -> hextree::Cell { @@ -322,9 +307,11 @@ mod tests { speedtests: Speedtest::maximum(), location_trust_scores: trusted_location, radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new(vec![CoveredHex { - cell: hex_location(), - rank: Rank::new(1).unwrap(), + covered_hexes: CoveredHexes::new(vec![RankedCoverage { + cbsd_id: None, + hotspot_key: pubkey(), + hex: hex_location(), + rank: 1, signal_level: SignalLevel::High, assignments: assignments_maximum(), boosted: Multiplier::new(5), @@ -351,9 +338,11 @@ mod tests { speedtests: Speedtest::maximum(), location_trust_scores: trusted_location, radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new(vec![CoveredHex { - cell: hex_location(), - rank: Rank::new(1).unwrap(), + covered_hexes: CoveredHexes::new(vec![RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, signal_level: SignalLevel::High, assignments: assignments_maximum(), boosted: Multiplier::new(5), @@ -378,9 +367,11 @@ mod tests { speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new(vec![CoveredHex { - cell: hex_location(), - rank: Rank::new(1).unwrap(), + covered_hexes: CoveredHexes::new(vec![RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, signal_level: SignalLevel::High, assignments: assignments_maximum(), boosted: None, @@ -435,10 +426,12 @@ mod tests { footfall: Assignment, landtype: Assignment, urbanized: Assignment, - ) -> CoveredHex { - CoveredHex { - cell: hex_location(), - rank: Rank::new(1).unwrap(), + ) -> RankedCoverage { + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, signal_level: SignalLevel::High, assignments: HexAssignments { footfall, @@ -507,30 +500,38 @@ mod tests { location_trust_scores: LocationTrustScores::maximum(), radio_threshold: RadioThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ - CoveredHex { - cell: hex_location(), - rank: Rank::new(1).unwrap(), + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, signal_level: SignalLevel::High, assignments: assignments_maximum(), boosted: None, }, - CoveredHex { - cell: hex_location(), - rank: Rank::new(2).unwrap(), + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 2, signal_level: SignalLevel::High, assignments: assignments_maximum(), boosted: None, }, - CoveredHex { - cell: hex_location(), - rank: Rank::new(3).unwrap(), + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 3, signal_level: SignalLevel::High, assignments: assignments_maximum(), boosted: None, }, - CoveredHex { - cell: hex_location(), - rank: Rank::new(42).unwrap(), + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 42, signal_level: SignalLevel::High, assignments: assignments_maximum(), boosted: None, @@ -556,23 +557,29 @@ mod tests { location_trust_scores: LocationTrustScores::maximum(), radio_threshold: RadioThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ - CoveredHex { - cell: hex_location(), - rank: Rank::new(1).unwrap(), + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, signal_level: SignalLevel::High, assignments: assignments_maximum(), boosted: None, }, - CoveredHex { - cell: hex_location(), - rank: Rank::new(2).unwrap(), + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 2, signal_level: SignalLevel::High, assignments: assignments_maximum(), boosted: None, }, - CoveredHex { - cell: hex_location(), - rank: Rank::new(42).unwrap(), + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 42, signal_level: SignalLevel::High, assignments: assignments_maximum(), boosted: None, @@ -599,9 +606,11 @@ mod tests { dec!(0.4), ]), radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new(vec![CoveredHex { - cell: hex_location(), - rank: Rank::new(1).unwrap(), + covered_hexes: CoveredHexes::new(vec![RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, signal_level: SignalLevel::High, assignments: assignments_maximum(), boosted: None, @@ -623,16 +632,20 @@ mod tests { location_trust_scores: LocationTrustScores::maximum(), radio_threshold: RadioThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ - CoveredHex { - cell: hex_location(), - rank: Rank::new(1).unwrap(), + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, signal_level: SignalLevel::High, assignments: assignments_maximum(), boosted: None, }, - CoveredHex { - cell: hex_location(), - rank: Rank::new(1).unwrap(), + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, signal_level: SignalLevel::Low, assignments: assignments_maximum(), boosted: Multiplier::new(4), @@ -662,30 +675,38 @@ mod tests { location_trust_scores: LocationTrustScores::maximum(), radio_threshold: RadioThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ - CoveredHex { - cell: hex_location(), - rank: Rank::new(1).unwrap(), + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, signal_level: SignalLevel::High, assignments: assignments_maximum(), boosted: None, }, - CoveredHex { - cell: hex_location(), - rank: Rank::new(1).unwrap(), + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, signal_level: SignalLevel::Medium, assignments: assignments_maximum(), boosted: None, }, - CoveredHex { - cell: hex_location(), - rank: Rank::new(1).unwrap(), + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, signal_level: SignalLevel::Low, assignments: assignments_maximum(), boosted: None, }, - CoveredHex { - cell: hex_location(), - rank: Rank::new(1).unwrap(), + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, signal_level: SignalLevel::None, assignments: assignments_maximum(), boosted: None, @@ -699,16 +720,20 @@ mod tests { location_trust_scores: LocationTrustScores::maximum(), radio_threshold: RadioThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ - CoveredHex { - cell: hex_location(), - rank: Rank::new(1).unwrap(), + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, signal_level: SignalLevel::High, assignments: assignments_maximum(), boosted: None, }, - CoveredHex { - cell: hex_location(), - rank: Rank::new(1).unwrap(), + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, signal_level: SignalLevel::Low, assignments: assignments_maximum(), boosted: None, @@ -722,30 +747,38 @@ mod tests { location_trust_scores: LocationTrustScores::maximum(), radio_threshold: RadioThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ - CoveredHex { - cell: hex_location(), - rank: Rank::new(1).unwrap(), + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, signal_level: SignalLevel::High, assignments: assignments_maximum(), boosted: None, }, - CoveredHex { - cell: hex_location(), - rank: Rank::new(1).unwrap(), + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, signal_level: SignalLevel::Medium, assignments: assignments_maximum(), boosted: None, }, - CoveredHex { - cell: hex_location(), - rank: Rank::new(1).unwrap(), + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, signal_level: SignalLevel::Low, assignments: assignments_maximum(), boosted: None, }, - CoveredHex { - cell: hex_location(), - rank: Rank::new(1).unwrap(), + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, signal_level: SignalLevel::None, assignments: assignments_maximum(), boosted: None, @@ -759,16 +792,20 @@ mod tests { location_trust_scores: LocationTrustScores::maximum(), radio_threshold: RadioThreshold::Verified, covered_hexes: CoveredHexes::new(vec![ - CoveredHex { - cell: hex_location(), - rank: Rank::new(1).unwrap(), + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, signal_level: SignalLevel::High, assignments: assignments_maximum(), boosted: None, }, - CoveredHex { - cell: hex_location(), - rank: Rank::new(1).unwrap(), + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, signal_level: SignalLevel::Low, assignments: assignments_maximum(), boosted: None, @@ -843,4 +880,11 @@ mod tests { ) } } + + fn pubkey() -> helium_crypto::PublicKeyBinary { + helium_crypto::PublicKeyBinary::from_str( + "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6", + ) + .expect("failed owner parse") + } } diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index debc1f9a2..fdfcccf23 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -1,11 +1,11 @@ -use std::{collections::HashMap, num::NonZeroU32}; +use std::{collections::HashMap, num::NonZeroU32, str::FromStr}; -use coverage_map::SignalLevel; +use coverage_map::{RankedCoverage, SignalLevel}; use coverage_point_calculator::{ calculate_coverage_points, location::{LocationTrust, Meters}, speedtest::{BytesPs, Millis, Speedtest}, - CoveredHex, RadioThreshold, RadioType, Rank, RewardableRadio, + RadioThreshold, RadioType, RewardableRadio, }; use hex_assignments::{assignment::HexAssignments, Assignment}; use rust_decimal_macros::dec; @@ -29,9 +29,16 @@ fn base_radio_coverage_points() { trust_score: dec!(1.0), }]; - let hexes = vec![CoveredHex { - cell: hextree::Cell::from_raw(0x8c2681a3064edff).unwrap(), - rank: Rank::new(1).unwrap(), + let pubkey = helium_crypto::PublicKeyBinary::from_str( + "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6", + ) + .unwrap(); + + let hexes = vec![RankedCoverage { + hotspot_key: pubkey, + cbsd_id: None, + hex: hextree::Cell::from_raw(0x8c2681a3064edff).unwrap(), + rank: 1, signal_level: SignalLevel::High, assignments: HexAssignments { footfall: Assignment::A, @@ -71,10 +78,17 @@ fn base_radio_coverage_points() { #[test] fn radio_unique_coverage() { + let pubkey = helium_crypto::PublicKeyBinary::from_str( + "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6", + ) + .unwrap(); + // all radios will receive 400 coverage points - let base_hex = CoveredHex { - cell: hextree::Cell::from_raw(0x8c2681a3064edff).unwrap(), - rank: Rank::new(1).unwrap(), + let base_hex = RankedCoverage { + hotspot_key: pubkey, + cbsd_id: None, + hex: hextree::Cell::from_raw(0x8c2681a3064edff).unwrap(), + rank: 1, signal_level: SignalLevel::High, assignments: HexAssignments { footfall: Assignment::A, @@ -91,7 +105,10 @@ fn radio_unique_coverage() { map.insert("outdoor_wifi", hex.clone().take(25).collect()); map.insert("outdoor_cbrs", hex.clone().take(100).collect()); - fn hexes(coverage_map: &HashMap<&str, Vec>, key: &RadioType) -> Vec { + fn hexes( + coverage_map: &HashMap<&str, Vec>, + key: &RadioType, + ) -> Vec { let key = match key { RadioType::IndoorWifi => "indoor_wifi", RadioType::OutdoorWifi => "outdoor_wifi", From 36af33efe8a08e868a4e6468b0fa193ee108dbe7 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 4 Jun 2024 15:33:34 -0700 Subject: [PATCH 050/115] do not panic for incorrect signal levels return a calculator error if a rewardable radio cannot be constructed. --- Cargo.lock | 1 + coverage_point_calculator/Cargo.toml | 1 + coverage_point_calculator/src/lib.rs | 656 ++++++++++-------- .../tests/coverage_point_calculator.rs | 24 +- 4 files changed, 377 insertions(+), 305 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 505faf358..62ebf03c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2140,6 +2140,7 @@ dependencies = [ "hextree", "rust_decimal", "rust_decimal_macros", + "thiserror", ] [[package]] diff --git a/coverage_point_calculator/Cargo.toml b/coverage_point_calculator/Cargo.toml index 062acb6b4..53b634230 100644 --- a/coverage_point_calculator/Cargo.toml +++ b/coverage_point_calculator/Cargo.toml @@ -10,6 +10,7 @@ edition.workspace = true hextree = { workspace = true } rust_decimal = { workspace = true } rust_decimal_macros = { workspace = true } +thiserror = { workspace = true } hex-assignments = { path = "../hex_assignments" } coverage-map = { path = "../coverage_map" } diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 80aca0115..64254095d 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -59,9 +59,15 @@ use rust_decimal_macros::dec; pub mod location; pub mod speedtest; +pub type Result = std::result::Result; +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("signal level {0:?} not allowed for {1:?}")] + InvalidSignalLevel(SignalLevel, RadioType), +} + pub type Multiplier = std::num::NonZeroU32; pub type MaxOneMultplier = Decimal; -type Points = Decimal; pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { let base_points = radio.hex_coverage_points(); @@ -86,12 +92,12 @@ pub enum RadioType { } impl RadioType { - fn base_coverage_points(&self, signal_level: &SignalLevel) -> Points { - match self { + fn base_coverage_points(&self, signal_level: &SignalLevel) -> Result { + let mult = match self { RadioType::IndoorWifi => match signal_level { SignalLevel::High => dec!(400), SignalLevel::Low => dec!(100), - other => panic!("indoor wifi radios cannot have {other:?} signal levels"), + other => return Err(Error::InvalidSignalLevel(*other, *self)), }, RadioType::OutdoorWifi => match signal_level { SignalLevel::High => dec!(16), @@ -102,7 +108,7 @@ impl RadioType { RadioType::IndoorCbrs => match signal_level { SignalLevel::High => dec!(100), SignalLevel::Low => dec!(25), - other => panic!("indoor cbrs radios cannot have {other:?} signal levels"), + other => return Err(Error::InvalidSignalLevel(*other, *self)), }, RadioType::OutdoorCbrs => match signal_level { SignalLevel::High => dec!(4), @@ -110,7 +116,8 @@ impl RadioType { SignalLevel::Low => dec!(1), SignalLevel::None => dec!(0), }, - } + }; + Ok(mult) } fn rank_multipliers(&self) -> Vec { @@ -151,14 +158,14 @@ impl RewardableRadio { location_trust_scores: Vec, verified_radio_threshold: RadioThreshold, covered_hexes: Vec, - ) -> Self { - Self { + ) -> Result { + Ok(Self { radio_type, speedtests, location_trust_scores: LocationTrustScores::new(location_trust_scores), radio_threshold: verified_radio_threshold, - covered_hexes: CoveredHexes::new(covered_hexes), - } + covered_hexes: CoveredHexes::new(&radio_type, covered_hexes)?, + }) } // These points need to be reported in the proto pre-(location, speedtest) multipliers @@ -171,15 +178,20 @@ impl RewardableRadio { .iter() .filter(|hex| hex.rank <= max_rank) .map(|hex| { - let base_coverage_points = self.radio_type.base_coverage_points(&hex.signal_level); + let base_coverage_points = self + .radio_type + .base_coverage_points(&hex.signal_level) + .expect("base coverage points"); let assignments_multiplier = hex.assignments.boosting_multiplier(); let rank_multiplier = rank_multipliers[hex.rank - 1]; let hex_boost_multiplier = self.hex_boosting_multiplier(hex); - base_coverage_points + let total = base_coverage_points * assignments_multiplier * rank_multiplier - * hex_boost_multiplier + * hex_boost_multiplier; + + total }) .sum() } @@ -204,12 +216,18 @@ pub struct CoveredHexes { } impl CoveredHexes { - fn new(covered_hexes: Vec) -> Self { + fn new(radio_type: &RadioType, covered_hexes: Vec) -> Result { let any_boosted = covered_hexes.iter().any(|hex| hex.boosted.is_some()); - Self { + // verify all hexes can obtain a base coverage point + covered_hexes + .iter() + .map(|hex| radio_type.base_coverage_points(&hex.signal_level)) + .collect::>>()?; + + Ok(Self { any_boosted, hexes: covered_hexes, - } + }) } } @@ -307,18 +325,24 @@ mod tests { speedtests: Speedtest::maximum(), location_trust_scores: trusted_location, radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new(vec![RankedCoverage { - cbsd_id: None, - hotspot_key: pubkey(), - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: Multiplier::new(5), - }]), + covered_hexes: CoveredHexes::new( + &RadioType::IndoorWifi, + vec![RankedCoverage { + cbsd_id: None, + hotspot_key: pubkey(), + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: Multiplier::new(5), + }], + ) + .unwrap(), }; - let base_points = RadioType::IndoorWifi.base_coverage_points(&SignalLevel::High); + let base_points = RadioType::IndoorWifi + .base_coverage_points(&SignalLevel::High) + .unwrap(); // Boosted Hex get's radio over the base_points assert!(wifi.location_trust_multiplier() > dec!(0.75)); assert!(calculate_coverage_points(wifi.clone()).coverage_points > base_points); @@ -338,18 +362,24 @@ mod tests { speedtests: Speedtest::maximum(), location_trust_scores: trusted_location, radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new(vec![RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: Multiplier::new(5), - }]), + covered_hexes: CoveredHexes::new( + &RadioType::IndoorWifi, + vec![RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: Multiplier::new(5), + }], + ) + .unwrap(), }; - let base_points = RadioType::IndoorWifi.base_coverage_points(&SignalLevel::High); + let base_points = RadioType::IndoorWifi + .base_coverage_points(&SignalLevel::High) + .unwrap(); // Boosted Hex get's radio over the base_points assert!(wifi.location_trust_multiplier() > dec!(0.75)); assert!(calculate_coverage_points(wifi.clone()).coverage_points > base_points); @@ -367,15 +397,19 @@ mod tests { speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new(vec![RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: Some("serial".to_string()), - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }]), + covered_hexes: CoveredHexes::new( + &RadioType::IndoorCbrs, + vec![RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }], + ) + .unwrap(), }; assert_eq!( @@ -448,42 +482,46 @@ mod tests { speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new(vec![ - // yellow - POI ≥ 1 Urbanized - local_hex(A, A, A), // 100 - local_hex(A, B, A), // 100 - local_hex(A, C, A), // 100 - // orange - POI ≥ 1 Not Urbanized - local_hex(A, A, B), // 100 - local_hex(A, B, B), // 100 - local_hex(A, C, B), // 100 - // light green - Point of Interest Urbanized - local_hex(B, A, A), // 70 - local_hex(B, B, A), // 70 - local_hex(B, C, A), // 70 - // dark green - Point of Interest Not Urbanized - local_hex(B, A, B), // 50 - local_hex(B, B, B), // 50 - local_hex(B, C, B), // 50 - // light blue - No POI Urbanized - local_hex(C, A, A), // 40 - local_hex(C, B, A), // 30 - local_hex(C, C, A), // 5 - // dark blue - No POI Not Urbanized - local_hex(C, A, B), // 20 - local_hex(C, B, B), // 15 - local_hex(C, C, B), // 3 - // gray - Outside of USA - local_hex(A, A, C), // 0 - local_hex(A, B, C), // 0 - local_hex(A, C, C), // 0 - local_hex(B, A, C), // 0 - local_hex(B, B, C), // 0 - local_hex(B, C, C), // 0 - local_hex(C, A, C), // 0 - local_hex(C, B, C), // 0 - local_hex(C, C, C), // 0 - ]), + covered_hexes: CoveredHexes::new( + &RadioType::IndoorCbrs, + vec![ + // yellow - POI ≥ 1 Urbanized + local_hex(A, A, A), // 100 + local_hex(A, B, A), // 100 + local_hex(A, C, A), // 100 + // orange - POI ≥ 1 Not Urbanized + local_hex(A, A, B), // 100 + local_hex(A, B, B), // 100 + local_hex(A, C, B), // 100 + // light green - Point of Interest Urbanized + local_hex(B, A, A), // 70 + local_hex(B, B, A), // 70 + local_hex(B, C, A), // 70 + // dark green - Point of Interest Not Urbanized + local_hex(B, A, B), // 50 + local_hex(B, B, B), // 50 + local_hex(B, C, B), // 50 + // light blue - No POI Urbanized + local_hex(C, A, A), // 40 + local_hex(C, B, A), // 30 + local_hex(C, C, A), // 5 + // dark blue - No POI Not Urbanized + local_hex(C, A, B), // 20 + local_hex(C, B, B), // 15 + local_hex(C, C, B), // 3 + // gray - Outside of USA + local_hex(A, A, C), // 0 + local_hex(A, B, C), // 0 + local_hex(A, C, C), // 0 + local_hex(B, A, C), // 0 + local_hex(B, B, C), // 0 + local_hex(B, C, C), // 0 + local_hex(C, A, C), // 0 + local_hex(C, B, C), // 0 + local_hex(C, C, C), // 0 + ], + ) + .unwrap(), }; assert_eq!( @@ -499,44 +537,48 @@ mod tests { speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new(vec![ - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 2, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 3, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 42, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - ]), + covered_hexes: CoveredHexes::new( + &RadioType::OutdoorWifi, + vec![ + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 2, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 3, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 42, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + ], + ) + .unwrap(), }; // rank 1 :: 1.00 * 16 == 16 @@ -556,35 +598,39 @@ mod tests { speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new(vec![ - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 2, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 42, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - ]), + covered_hexes: CoveredHexes::new( + &RadioType::IndoorWifi, + vec![ + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 2, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 42, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + ], + ) + .unwrap(), }; assert_eq!( @@ -606,15 +652,19 @@ mod tests { dec!(0.4), ]), radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new(vec![RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }]), + covered_hexes: CoveredHexes::new( + &RadioType::IndoorWifi, + vec![RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }], + ) + .unwrap(), }; // Location trust scores is 1/4 @@ -631,26 +681,30 @@ mod tests { speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new(vec![ - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::Low, - assignments: assignments_maximum(), - boosted: Multiplier::new(4), - }, - ]), + covered_hexes: CoveredHexes::new( + &RadioType::IndoorWifi, + vec![ + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::Low, + assignments: assignments_maximum(), + boosted: Multiplier::new(4), + }, + ], + ) + .unwrap(), }; // The hex with a low signal_level is boosted to the same level as a // signal_level of High. @@ -674,44 +728,48 @@ mod tests { speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new(vec![ - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: Some("serial".to_string()), - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: Some("serial".to_string()), - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::Medium, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: Some("serial".to_string()), - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::Low, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: Some("serial".to_string()), - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::None, - assignments: assignments_maximum(), - boosted: None, - }, - ]), + covered_hexes: CoveredHexes::new( + &RadioType::OutdoorCbrs, + vec![ + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::Medium, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::Low, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::None, + assignments: assignments_maximum(), + boosted: None, + }, + ], + ) + .unwrap(), }; let indoor_cbrs = RewardableRadio { @@ -719,26 +777,30 @@ mod tests { speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new(vec![ - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: Some("serial".to_string()), - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: Some("serial".to_string()), - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::Low, - assignments: assignments_maximum(), - boosted: None, - }, - ]), + covered_hexes: CoveredHexes::new( + &RadioType::IndoorCbrs, + vec![ + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::Low, + assignments: assignments_maximum(), + boosted: None, + }, + ], + ) + .unwrap(), }; let outdoor_wifi = RewardableRadio { @@ -746,44 +808,48 @@ mod tests { speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new(vec![ - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::Medium, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::Low, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::None, - assignments: assignments_maximum(), - boosted: None, - }, - ]), + covered_hexes: CoveredHexes::new( + &RadioType::OutdoorWifi, + vec![ + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::Medium, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::Low, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::None, + assignments: assignments_maximum(), + boosted: None, + }, + ], + ) + .unwrap(), }; let indoor_wifi = RewardableRadio { @@ -791,26 +857,30 @@ mod tests { speedtests: Speedtest::maximum(), location_trust_scores: LocationTrustScores::maximum(), radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new(vec![ - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::Low, - assignments: assignments_maximum(), - boosted: None, - }, - ]), + covered_hexes: CoveredHexes::new( + &RadioType::IndoorWifi, + vec![ + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::Low, + assignments: assignments_maximum(), + boosted: None, + }, + ], + ) + .unwrap(), }; // When each radio contains a hex of every applicable signal_level, and diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index fdfcccf23..80e75deb6 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -61,7 +61,8 @@ fn base_radio_coverage_points() { location_trust_scores.clone(), RadioThreshold::Verified, hexes.clone(), - ); + ) + .unwrap(); radios.push(radio.clone()); println!( "{radio_type:?} \t--> {}", @@ -142,22 +143,21 @@ fn radio_unique_coverage() { RadioType::OutdoorWifi, RadioType::OutdoorCbrs, ] { - radios.push(RewardableRadio::new( - radio_type, - default_speedtests.clone(), - default_location_trust_scores.clone(), - RadioThreshold::Verified, - hexes(&map, &radio_type), - )); + radios.push( + RewardableRadio::new( + radio_type, + default_speedtests.clone(), + default_location_trust_scores.clone(), + RadioThreshold::Verified, + hexes(&map, &radio_type), + ) + .unwrap(), + ); } let coverage_points = radios .into_iter() .map(|r| (r.radio_type, calculate_coverage_points(r).coverage_points)) .collect::>(); - - // let coverage_points = make_rewardable_radios(&radios, &coverage_map) - // .map(|r| (r.radio_type, calculate_coverage_points(r).coverage_points)) - // .collect::>(); println!("{coverage_points:#?}") } From 821be75fc9e2826246711a8dec0d8ef64197add8 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 4 Jun 2024 16:39:42 -0700 Subject: [PATCH 051/115] starting to reorder things for readability --- coverage_point_calculator/src/lib.rs | 192 +++++++++++++-------------- 1 file changed, 95 insertions(+), 97 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 64254095d..4a6954f6e 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -60,13 +60,28 @@ pub mod location; pub mod speedtest; pub type Result = std::result::Result; + +#[derive(Debug, Clone)] +pub struct RewardableRadio { + pub radio_type: RadioType, + pub speedtests: Vec, + pub location_trust_scores: LocationTrustScores, + pub radio_threshold: RadioThreshold, + pub covered_hexes: CoveredHexes, +} + +#[derive(Debug)] +pub struct CoveragePoints { + pub coverage_points: Decimal, + pub radio: RewardableRadio, +} + #[derive(thiserror::Error, Debug)] pub enum Error { #[error("signal level {0:?} not allowed for {1:?}")] InvalidSignalLevel(SignalLevel, RadioType), } -pub type Multiplier = std::num::NonZeroU32; pub type MaxOneMultplier = Decimal; pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { @@ -83,74 +98,6 @@ pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { } } -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum RadioType { - IndoorWifi, - OutdoorWifi, - IndoorCbrs, - OutdoorCbrs, -} - -impl RadioType { - fn base_coverage_points(&self, signal_level: &SignalLevel) -> Result { - let mult = match self { - RadioType::IndoorWifi => match signal_level { - SignalLevel::High => dec!(400), - SignalLevel::Low => dec!(100), - other => return Err(Error::InvalidSignalLevel(*other, *self)), - }, - RadioType::OutdoorWifi => match signal_level { - SignalLevel::High => dec!(16), - SignalLevel::Medium => dec!(8), - SignalLevel::Low => dec!(4), - SignalLevel::None => dec!(0), - }, - RadioType::IndoorCbrs => match signal_level { - SignalLevel::High => dec!(100), - SignalLevel::Low => dec!(25), - other => return Err(Error::InvalidSignalLevel(*other, *self)), - }, - RadioType::OutdoorCbrs => match signal_level { - SignalLevel::High => dec!(4), - SignalLevel::Medium => dec!(2), - SignalLevel::Low => dec!(1), - SignalLevel::None => dec!(0), - }, - }; - Ok(mult) - } - - fn rank_multipliers(&self) -> Vec { - match self { - RadioType::IndoorWifi => vec![dec!(1)], - RadioType::IndoorCbrs => vec![dec!(1)], - RadioType::OutdoorWifi => vec![dec!(1), dec!(0.5), dec!(0.25)], - RadioType::OutdoorCbrs => vec![dec!(1), dec!(0.5), dec!(0.25)], - } - } -} - -#[derive(Debug)] -pub struct CoveragePoints { - pub coverage_points: Decimal, - pub radio: RewardableRadio, -} - -#[derive(Debug, Clone, PartialEq)] -pub enum RadioThreshold { - Verified, - UnVerified, -} - -#[derive(Debug, Clone)] -pub struct RewardableRadio { - pub radio_type: RadioType, - pub speedtests: Vec, - pub location_trust_scores: LocationTrustScores, - pub radio_threshold: RadioThreshold, - pub covered_hexes: CoveredHexes, -} - impl RewardableRadio { pub fn new( radio_type: RadioType, @@ -207,31 +154,7 @@ impl RewardableRadio { pub fn radio_threshold_met(&self) -> bool { matches!(self.radio_threshold, RadioThreshold::Verified) } -} -#[derive(Debug, Clone)] -pub struct CoveredHexes { - any_boosted: bool, - hexes: Vec, -} - -impl CoveredHexes { - fn new(radio_type: &RadioType, covered_hexes: Vec) -> Result { - let any_boosted = covered_hexes.iter().any(|hex| hex.boosted.is_some()); - // verify all hexes can obtain a base coverage point - covered_hexes - .iter() - .map(|hex| radio_type.base_coverage_points(&hex.signal_level)) - .collect::>>()?; - - Ok(Self { - any_boosted, - hexes: covered_hexes, - }) - } -} - -impl RewardableRadio { pub fn location_trust_multiplier(&self) -> Decimal { // CBRS radios are always trusted because they have internal GPS if self.is_cbrs() { @@ -290,10 +213,85 @@ impl RewardableRadio { } } +#[derive(Debug, Clone, PartialEq)] +pub enum RadioThreshold { + Verified, + UnVerified, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum RadioType { + IndoorWifi, + OutdoorWifi, + IndoorCbrs, + OutdoorCbrs, +} + +impl RadioType { + fn base_coverage_points(&self, signal_level: &SignalLevel) -> Result { + let mult = match self { + RadioType::IndoorWifi => match signal_level { + SignalLevel::High => dec!(400), + SignalLevel::Low => dec!(100), + other => return Err(Error::InvalidSignalLevel(*other, *self)), + }, + RadioType::OutdoorWifi => match signal_level { + SignalLevel::High => dec!(16), + SignalLevel::Medium => dec!(8), + SignalLevel::Low => dec!(4), + SignalLevel::None => dec!(0), + }, + RadioType::IndoorCbrs => match signal_level { + SignalLevel::High => dec!(100), + SignalLevel::Low => dec!(25), + other => return Err(Error::InvalidSignalLevel(*other, *self)), + }, + RadioType::OutdoorCbrs => match signal_level { + SignalLevel::High => dec!(4), + SignalLevel::Medium => dec!(2), + SignalLevel::Low => dec!(1), + SignalLevel::None => dec!(0), + }, + }; + Ok(mult) + } + + fn rank_multipliers(&self) -> Vec { + match self { + RadioType::IndoorWifi => vec![dec!(1)], + RadioType::IndoorCbrs => vec![dec!(1)], + RadioType::OutdoorWifi => vec![dec!(1), dec!(0.5), dec!(0.25)], + RadioType::OutdoorCbrs => vec![dec!(1), dec!(0.5), dec!(0.25)], + } + } +} + +#[derive(Debug, Clone)] +pub struct CoveredHexes { + any_boosted: bool, + hexes: Vec, +} + +impl CoveredHexes { + fn new(radio_type: &RadioType, covered_hexes: Vec) -> Result { + let any_boosted = covered_hexes.iter().any(|hex| hex.boosted.is_some()); + // verify all hexes can obtain a base coverage point + covered_hexes + .iter() + .map(|hex| radio_type.base_coverage_points(&hex.signal_level)) + .collect::>>()?; + + Ok(Self { + any_boosted, + hexes: covered_hexes, + }) + } +} + #[cfg(test)] mod tests { - use std::str::FromStr; + use std::{num::NonZeroU32, str::FromStr}; use crate::{ location::Meters, @@ -334,7 +332,7 @@ mod tests { rank: 1, signal_level: SignalLevel::High, assignments: assignments_maximum(), - boosted: Multiplier::new(5), + boosted: NonZeroU32::new(5), }], ) .unwrap(), @@ -371,7 +369,7 @@ mod tests { rank: 1, signal_level: SignalLevel::High, assignments: assignments_maximum(), - boosted: Multiplier::new(5), + boosted: NonZeroU32::new(5), }], ) .unwrap(), @@ -700,7 +698,7 @@ mod tests { rank: 1, signal_level: SignalLevel::Low, assignments: assignments_maximum(), - boosted: Multiplier::new(4), + boosted: NonZeroU32::new(4), }, ], ) From dd613bce5c7ee8ff333894a6986c5a6be9c349e3 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 5 Jun 2024 12:55:26 -0700 Subject: [PATCH 052/115] Cleanup after overview Despire data given as input, only output data used for calculation. Enforce the rules of speedtests, min of 2, max of 6, ordered newest to oldest. BoostedHexes contain the boosted value they used. Location Trust Scores contain the trust score based on boosted hexes. --- Cargo.lock | 1 + coverage_point_calculator/Cargo.toml | 1 + coverage_point_calculator/src/lib.rs | 1068 +++++++++-------- coverage_point_calculator/src/location.rs | 141 ++- coverage_point_calculator/src/speedtest.rs | 37 + .../tests/coverage_point_calculator.rs | 21 +- 6 files changed, 740 insertions(+), 529 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62ebf03c1..c6eb61e79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2134,6 +2134,7 @@ dependencies = [ name = "coverage-point-calculator" version = "0.1.0" dependencies = [ + "chrono", "coverage-map", "helium-crypto", "hex-assignments", diff --git a/coverage_point_calculator/Cargo.toml b/coverage_point_calculator/Cargo.toml index 53b634230..38d76d8f7 100644 --- a/coverage_point_calculator/Cargo.toml +++ b/coverage_point_calculator/Cargo.toml @@ -7,6 +7,7 @@ license.workspace = true edition.workspace = true [dependencies] +chrono = { workspace = true } hextree = { workspace = true } rust_decimal = { workspace = true } rust_decimal_macros = { workspace = true } diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 4a6954f6e..a81e1f48a 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -50,21 +50,25 @@ /// use crate::{ location::{LocationTrust, LocationTrustScores}, - speedtest::{Speedtest, SpeedtestTier}, + speedtest::Speedtest, }; use coverage_map::{RankedCoverage, SignalLevel}; +use hex_assignments::assignment::HexAssignments; use rust_decimal::{Decimal, RoundingStrategy}; use rust_decimal_macros::dec; +use speedtest::Speedtests; pub mod location; pub mod speedtest; pub type Result = std::result::Result; +pub type MaxOneMultplier = Decimal; +/// Input Radio to calculation #[derive(Debug, Clone)] pub struct RewardableRadio { pub radio_type: RadioType, - pub speedtests: Vec, + pub speedtests: Speedtests, pub location_trust_scores: LocationTrustScores, pub radio_threshold: RadioThreshold, pub covered_hexes: CoveredHexes, @@ -72,8 +76,22 @@ pub struct RewardableRadio { #[derive(Debug)] pub struct CoveragePoints { - pub coverage_points: Decimal, - pub radio: RewardableRadio, + /// Value used when calculating poc_reward + pub total_coverage_points: Decimal, + /// Coverage Points collected from each Covered Hex + /// vvv turn into function call + pub hex_coverage_points: Decimal, + /// Location Trust Multiplier, maximum of 1 + pub location_trust_multiplier: Decimal, + /// Speedtest Mulitplier, maximum of 1 + pub speedtest_multiplier: Decimal, + // --- + pub radio_type: RadioType, + pub radio_threshold: RadioThreshold, + pub speedtests: Vec, + pub location_trust_scores: Vec, + pub covered_hexes: Vec, + pub eligible_for_boosted_hexes: bool, } #[derive(thiserror::Error, Debug)] @@ -82,19 +100,41 @@ pub enum Error { InvalidSignalLevel(SignalLevel, RadioType), } -pub type MaxOneMultplier = Decimal; - pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { - let base_points = radio.hex_coverage_points(); - let location_score = radio.location_trust_multiplier(); - let speedtest = radio.speedtest_multiplier(); + let hex_coverage_points = radio.accumulate_hex_coverage_points(); + let location_trust_multiplier = radio.location_trust_multiplier(); + let speedtest_multiplier = radio.speedtest_multiplier(); - let coverage_points = base_points * location_score * speedtest; - let coverage_points = coverage_points.round_dp_with_strategy(2, RoundingStrategy::ToZero); + let coverage_points = hex_coverage_points * location_trust_multiplier * speedtest_multiplier; + let total_coverage_points = coverage_points.round_dp_with_strategy(2, RoundingStrategy::ToZero); CoveragePoints { - coverage_points, - radio, + total_coverage_points, + hex_coverage_points, + location_trust_multiplier, + speedtest_multiplier, + // Radio information + eligible_for_boosted_hexes: radio.eligible_for_boosted_hexes(), + radio_type: radio.radio_type, + radio_threshold: radio.radio_threshold, + // TODO: only return the speedtests used for calculation + speedtests: radio.speedtests.speedtests, + location_trust_scores: radio.location_trust_scores.trust_scores, + covered_hexes: radio.covered_hexes.hexes, + } +} + +impl CoveragePoints { + pub fn iter_boosted_hexes(&self) -> impl Iterator { + let verified = self.radio_threshold.threshold_met(); + let eligible = self.eligible_for_boosted_hexes; + + self.covered_hexes + .clone() + .into_iter() + .filter(move |_| verified) + .filter(move |_| eligible) + .filter(|hex| hex.boosted_multiplier.is_some()) } } @@ -106,53 +146,62 @@ impl RewardableRadio { verified_radio_threshold: RadioThreshold, covered_hexes: Vec, ) -> Result { + let any_boosted_hexes = any_boosted(&covered_hexes); + let location_trust_scores = if any_boosted_hexes { + LocationTrustScores::new_with_boosted_hexes(location_trust_scores) + } else { + LocationTrustScores::new(location_trust_scores) + }; + + let location_multiplier = location_trust_scores.multiplier; + let mut eligible_for_boosted_hexes = true; + if radio_type.is_wifi() && location_multiplier < dec!(0.75) { + eligible_for_boosted_hexes = false; + } + if !verified_radio_threshold.threshold_met() { + eligible_for_boosted_hexes = false; + } + + let speedtests = Speedtests::new(speedtests); + Ok(Self { radio_type, speedtests, - location_trust_scores: LocationTrustScores::new(location_trust_scores), + location_trust_scores, radio_threshold: verified_radio_threshold, - covered_hexes: CoveredHexes::new(&radio_type, covered_hexes)?, + covered_hexes: CoveredHexes::new( + &radio_type, + eligible_for_boosted_hexes, + covered_hexes, + )?, }) } // These points need to be reported in the proto pre-(location, speedtest) multipliers - pub fn hex_coverage_points(&self) -> Decimal { - let rank_multipliers = self.radio_type.rank_multipliers(); - let max_rank = rank_multipliers.len(); - + pub fn accumulate_hex_coverage_points(&self) -> Decimal { self.covered_hexes .hexes .iter() - .filter(|hex| hex.rank <= max_rank) - .map(|hex| { - let base_coverage_points = self - .radio_type - .base_coverage_points(&hex.signal_level) - .expect("base coverage points"); - let assignments_multiplier = hex.assignments.boosting_multiplier(); - let rank_multiplier = rank_multipliers[hex.rank - 1]; - let hex_boost_multiplier = self.hex_boosting_multiplier(hex); - - let total = base_coverage_points - * assignments_multiplier - * rank_multiplier - * hex_boost_multiplier; - - total - }) + .map(|hex| hex.calculated_coverage_points) .sum() } - pub fn iter_covered_hexes(&self) -> impl Iterator { + pub fn iter_covered_hexes(&self) -> impl Iterator { self.covered_hexes.hexes.clone().into_iter() } - pub fn eligible_for_boosted_hexes(&self) -> bool { - self.location_trust_multiplier() > dec!(0.75) - } + fn eligible_for_boosted_hexes(&self) -> bool { + // hip93: if radio is wifi & location_trust score multiplier < 0.75, no boosting + if self.is_wifi() { + return self.location_trust_multiplier() > dec!(0.75); + } + + // hip84: if radio has not met minimum data and subscriber thresholds, no boosting + if !self.radio_threshold.threshold_met() { + return false; + } - pub fn radio_threshold_met(&self) -> bool { - matches!(self.radio_threshold, RadioThreshold::Verified) + true } pub fn location_trust_multiplier(&self) -> Decimal { @@ -161,41 +210,11 @@ impl RewardableRadio { return dec!(1); } - if self.any_hexes_boosted() { - self.location_trust_scores.any_hex_boosted_multiplier - } else { - self.location_trust_scores.no_boosted_hex_multiplier - } - } - - fn hex_boosting_multiplier(&self, hex: &RankedCoverage) -> MaxOneMultplier { - // need to consider requirements from hip93 & hip84 before applying any boost - // hip93: if radio is wifi & location_trust score multiplier < 0.75, no boosting - if self.is_wifi() && self.location_trust_multiplier() < dec!(0.75) { - return dec!(1); - } - // hip84: if radio has not met minimum data and subscriber thresholds, no boosting - if !self.radio_threshold_met() { - return dec!(1); - } - - let boost = hex.boosted.map_or(1, |boost| boost.get()); - Decimal::from(boost) + self.location_trust_scores.multiplier } pub fn speedtest_multiplier(&self) -> MaxOneMultplier { - const MIN_REQUIRED_SPEEDTEST_SAMPLES: usize = 2; - - if self.speedtests.len() < MIN_REQUIRED_SPEEDTEST_SAMPLES { - return SpeedtestTier::Fail.multiplier(); - } - - let speedtest_avg = Speedtest::avg(&self.speedtests); - speedtest_avg.multiplier() - } - - fn any_hexes_boosted(&self) -> bool { - self.covered_hexes.any_boosted + self.speedtests.multiplier } fn is_wifi(&self) -> bool { @@ -213,10 +232,78 @@ impl RewardableRadio { } } -#[derive(Debug, Clone, PartialEq)] -pub enum RadioThreshold { - Verified, - UnVerified, +#[derive(Debug, Clone)] +pub struct CoveredHexes { + hexes: Vec, +} + +#[derive(Debug, Clone)] +pub struct CoveredHex { + pub hex: hextree::Cell, + // -- + base_coverage_points: Decimal, + calculated_coverage_points: Decimal, + // oracle boosted + assignemnts: HexAssignments, + assignment_multiplier: Decimal, + // -- + rank: usize, + rank_multiplier: Decimal, + // provider boosted + pub boosted_multiplier: Option, +} + +fn any_boosted(ranked_coverage: &[RankedCoverage]) -> bool { + ranked_coverage.iter().any(|hex| hex.boosted.is_some()) +} + +impl CoveredHexes { + fn new( + radio_type: &RadioType, + eligible_for_boosted_hexes: bool, + ranked_coverage: Vec, + ) -> Result { + let rank_multipliers = radio_type.rank_multipliers(); + + // verify all hexes can obtain a base coverage point + let covered_hexes = ranked_coverage + .into_iter() + .map(|ranked| { + let coverage_points = radio_type.base_coverage_points(&ranked.signal_level)?; + let assignment_multiplier = ranked.assignments.boosting_multiplier(); + let rank_multiplier = rank_multipliers + .get(ranked.rank - 1) + .cloned() + .unwrap_or(dec!(0)); + + let boosted_multiplier = if eligible_for_boosted_hexes { + ranked.boosted.map(|boost| boost.get()).map(Decimal::from) + } else { + None + }; + + let calculated_coverage_points = coverage_points + * assignment_multiplier + * rank_multiplier + * boosted_multiplier.unwrap_or(dec!(1)); + + Ok(CoveredHex { + hex: ranked.hex, + base_coverage_points: coverage_points, + calculated_coverage_points, + assignemnts: ranked.assignments, + assignment_multiplier, + rank: ranked.rank, + rank_multiplier, + boosted_multiplier, + }) + }) + .collect::>>()?; + + Ok(Self { + hexes: covered_hexes, + }) + } } #[derive(Debug, Clone, Copy, PartialEq)] @@ -264,27 +351,21 @@ impl RadioType { RadioType::OutdoorCbrs => vec![dec!(1), dec!(0.5), dec!(0.25)], } } -} -#[derive(Debug, Clone)] -pub struct CoveredHexes { - any_boosted: bool, - hexes: Vec, + fn is_wifi(&self) -> bool { + matches!(self, Self::IndoorWifi | Self::OutdoorWifi) + } } -impl CoveredHexes { - fn new(radio_type: &RadioType, covered_hexes: Vec) -> Result { - let any_boosted = covered_hexes.iter().any(|hex| hex.boosted.is_some()); - // verify all hexes can obtain a base coverage point - covered_hexes - .iter() - .map(|hex| radio_type.base_coverage_points(&hex.signal_level)) - .collect::>>()?; +#[derive(Debug, Clone, PartialEq)] +pub enum RadioThreshold { + Verified, + UnVerified, +} - Ok(Self { - any_boosted, - hexes: covered_hexes, - }) +impl RadioThreshold { + fn threshold_met(&self) -> bool { + matches!(self, Self::Verified) } } @@ -299,6 +380,7 @@ mod tests { }; use super::*; + use chrono::Utc; use hex_assignments::{assignment::HexAssignments, Assignment}; use rust_decimal_macros::dec; @@ -316,18 +398,15 @@ mod tests { #[test] fn hip_84_radio_meets_minimum_subscriber_threshold_for_boosted_hexes() { - let trusted_location = LocationTrustScores::with_trust_scores(&[dec!(1), dec!(1)]); - let untrusted_location = LocationTrustScores::with_trust_scores(&[dec!(0.1), dec!(0.2)]); - let mut wifi = RewardableRadio { - radio_type: RadioType::IndoorWifi, - speedtests: Speedtest::maximum(), - location_trust_scores: trusted_location, - radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new( - &RadioType::IndoorWifi, + let make_wifi = |location_trust_scores: Vec| { + RewardableRadio::new( + RadioType::IndoorWifi, + Speedtest::maximum(), + location_trust_scores, + RadioThreshold::Verified, vec![RankedCoverage { - cbsd_id: None, hotspot_key: pubkey(), + cbsd_id: None, hex: hex_location(), rank: 1, signal_level: SignalLevel::High, @@ -335,33 +414,35 @@ mod tests { boosted: NonZeroU32::new(5), }], ) - .unwrap(), + .expect("indoor wifi with location scores") }; let base_points = RadioType::IndoorWifi .base_coverage_points(&SignalLevel::High) .unwrap(); // Boosted Hex get's radio over the base_points - assert!(wifi.location_trust_multiplier() > dec!(0.75)); - assert!(calculate_coverage_points(wifi.clone()).coverage_points > base_points); + let trusted_location = LocationTrustScores::with_trust_scores(&[dec!(1), dec!(1)]); + let trusted_wifi = make_wifi(trusted_location.trust_scores); + assert!(trusted_wifi.location_trust_multiplier() > dec!(0.75)); + assert!( + calculate_coverage_points(trusted_wifi.clone()).total_coverage_points > base_points + ); // degraded location score get's radio under base_points - wifi.location_trust_scores = untrusted_location; - assert!(wifi.location_trust_multiplier() < dec!(0.75)); - assert!(calculate_coverage_points(wifi).coverage_points < base_points); + let untrusted_location = LocationTrustScores::with_trust_scores(&[dec!(0.1), dec!(0.2)]); + let untrusted_wifi = make_wifi(untrusted_location.trust_scores); + assert!(untrusted_wifi.location_trust_multiplier() < dec!(0.75)); + assert!(calculate_coverage_points(untrusted_wifi).total_coverage_points < base_points); } #[test] fn hip_93_wifi_with_low_location_score_receives_no_boosted_hexes() { - let trusted_location = LocationTrustScores::with_trust_scores(&[dec!(1), dec!(1)]); - let untrusted_location = LocationTrustScores::with_trust_scores(&[dec!(0.1), dec!(0.2)]); - let mut wifi = RewardableRadio { - radio_type: RadioType::IndoorWifi, - speedtests: Speedtest::maximum(), - location_trust_scores: trusted_location, - radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new( - &RadioType::IndoorWifi, + let make_wifi = |location_trust_scores: Vec| { + RewardableRadio::new( + RadioType::IndoorWifi, + Speedtest::maximum(), + location_trust_scores, + RadioThreshold::Verified, vec![RankedCoverage { hotspot_key: pubkey(), cbsd_id: None, @@ -372,31 +453,35 @@ mod tests { boosted: NonZeroU32::new(5), }], ) - .unwrap(), + .expect("indoor wifi with location scores") }; let base_points = RadioType::IndoorWifi .base_coverage_points(&SignalLevel::High) .unwrap(); // Boosted Hex get's radio over the base_points - assert!(wifi.location_trust_multiplier() > dec!(0.75)); - assert!(calculate_coverage_points(wifi.clone()).coverage_points > base_points); + let trusted_location = LocationTrustScores::with_trust_scores(&[dec!(1), dec!(1)]); + let trusted_wifi = make_wifi(trusted_location.trust_scores); + assert!(trusted_wifi.location_trust_multiplier() > dec!(0.75)); + assert!( + calculate_coverage_points(trusted_wifi.clone()).total_coverage_points > base_points + ); // degraded location score get's radio under base_points - wifi.location_trust_scores = untrusted_location; - assert!(wifi.location_trust_multiplier() < dec!(0.75)); - assert!(calculate_coverage_points(wifi).coverage_points < base_points); + let untrusted_location = LocationTrustScores::with_trust_scores(&[dec!(0.1), dec!(0.2)]); + let untrusted_wifi = make_wifi(untrusted_location.trust_scores); + assert!(untrusted_wifi.location_trust_multiplier() < dec!(0.75)); + assert!(calculate_coverage_points(untrusted_wifi).total_coverage_points < base_points); } #[test] fn speedtest() { - let mut indoor_cbrs = RewardableRadio { - radio_type: RadioType::IndoorCbrs, - speedtests: Speedtest::maximum(), - location_trust_scores: LocationTrustScores::maximum(), - radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new( - &RadioType::IndoorCbrs, + let make_indoor_cbrs = |speedtests: Vec| { + RewardableRadio::new( + RadioType::IndoorCbrs, + speedtests, + LocationTrustScores::maximum().trust_scores, + RadioThreshold::Verified, vec![RankedCoverage { hotspot_key: pubkey(), cbsd_id: Some("serial".to_string()), @@ -407,48 +492,49 @@ mod tests { boosted: None, }], ) - .unwrap(), + .expect("indoor cbrs with speedtests") }; + let indoor_cbrs = make_indoor_cbrs(Speedtest::maximum()); assert_eq!( dec!(100), - calculate_coverage_points(indoor_cbrs.clone()).coverage_points + calculate_coverage_points(indoor_cbrs).total_coverage_points ); - indoor_cbrs.speedtests = vec![ + let indoor_cbrs = make_indoor_cbrs(vec![ Speedtest::download(BytesPs::mbps(88)), Speedtest::download(BytesPs::mbps(88)), - ]; + ]); assert_eq!( dec!(75), - calculate_coverage_points(indoor_cbrs.clone()).coverage_points + calculate_coverage_points(indoor_cbrs.clone()).total_coverage_points ); - indoor_cbrs.speedtests = vec![ + let indoor_cbrs = make_indoor_cbrs(vec![ Speedtest::download(BytesPs::mbps(62)), Speedtest::download(BytesPs::mbps(62)), - ]; + ]); assert_eq!( dec!(50), - calculate_coverage_points(indoor_cbrs.clone()).coverage_points + calculate_coverage_points(indoor_cbrs).total_coverage_points ); - indoor_cbrs.speedtests = vec![ + let indoor_cbrs = make_indoor_cbrs(vec![ Speedtest::download(BytesPs::mbps(42)), Speedtest::download(BytesPs::mbps(42)), - ]; + ]); assert_eq!( dec!(25), - calculate_coverage_points(indoor_cbrs.clone()).coverage_points + calculate_coverage_points(indoor_cbrs).total_coverage_points ); - indoor_cbrs.speedtests = vec![ + let indoor_cbrs = make_indoor_cbrs(vec![ Speedtest::download(BytesPs::mbps(25)), Speedtest::download(BytesPs::mbps(25)), - ]; + ]); assert_eq!( dec!(0), - calculate_coverage_points(indoor_cbrs).coverage_points + calculate_coverage_points(indoor_cbrs).total_coverage_points ); } @@ -475,109 +561,103 @@ mod tests { } use Assignment::*; - let indoor_cbrs = RewardableRadio { - radio_type: RadioType::IndoorCbrs, - speedtests: Speedtest::maximum(), - location_trust_scores: LocationTrustScores::maximum(), - radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new( - &RadioType::IndoorCbrs, - vec![ - // yellow - POI ≥ 1 Urbanized - local_hex(A, A, A), // 100 - local_hex(A, B, A), // 100 - local_hex(A, C, A), // 100 - // orange - POI ≥ 1 Not Urbanized - local_hex(A, A, B), // 100 - local_hex(A, B, B), // 100 - local_hex(A, C, B), // 100 - // light green - Point of Interest Urbanized - local_hex(B, A, A), // 70 - local_hex(B, B, A), // 70 - local_hex(B, C, A), // 70 - // dark green - Point of Interest Not Urbanized - local_hex(B, A, B), // 50 - local_hex(B, B, B), // 50 - local_hex(B, C, B), // 50 - // light blue - No POI Urbanized - local_hex(C, A, A), // 40 - local_hex(C, B, A), // 30 - local_hex(C, C, A), // 5 - // dark blue - No POI Not Urbanized - local_hex(C, A, B), // 20 - local_hex(C, B, B), // 15 - local_hex(C, C, B), // 3 - // gray - Outside of USA - local_hex(A, A, C), // 0 - local_hex(A, B, C), // 0 - local_hex(A, C, C), // 0 - local_hex(B, A, C), // 0 - local_hex(B, B, C), // 0 - local_hex(B, C, C), // 0 - local_hex(C, A, C), // 0 - local_hex(C, B, C), // 0 - local_hex(C, C, C), // 0 - ], - ) - .unwrap(), - }; + let indoor_cbrs = RewardableRadio::new( + RadioType::IndoorCbrs, + Speedtest::maximum(), + LocationTrustScores::maximum().trust_scores, + RadioThreshold::Verified, + vec![ + // yellow - POI ≥ 1 Urbanized + local_hex(A, A, A), // 100 + local_hex(A, B, A), // 100 + local_hex(A, C, A), // 100 + // orange - POI ≥ 1 Not Urbanized + local_hex(A, A, B), // 100 + local_hex(A, B, B), // 100 + local_hex(A, C, B), // 100 + // light green - Point of Interest Urbanized + local_hex(B, A, A), // 70 + local_hex(B, B, A), // 70 + local_hex(B, C, A), // 70 + // dark green - Point of Interest Not Urbanized + local_hex(B, A, B), // 50 + local_hex(B, B, B), // 50 + local_hex(B, C, B), // 50 + // light blue - No POI Urbanized + local_hex(C, A, A), // 40 + local_hex(C, B, A), // 30 + local_hex(C, C, A), // 5 + // dark blue - No POI Not Urbanized + local_hex(C, A, B), // 20 + local_hex(C, B, B), // 15 + local_hex(C, C, B), // 3 + // gray - Outside of USA + local_hex(A, A, C), // 0 + local_hex(A, B, C), // 0 + local_hex(A, C, C), // 0 + local_hex(B, A, C), // 0 + local_hex(B, B, C), // 0 + local_hex(B, C, C), // 0 + local_hex(C, A, C), // 0 + local_hex(C, B, C), // 0 + local_hex(C, C, C), // 0 + ], + ) + .expect("indoor cbrs"); assert_eq!( dec!(1073), - calculate_coverage_points(indoor_cbrs).coverage_points + calculate_coverage_points(indoor_cbrs).total_coverage_points ); } #[test] fn outdoor_radios_consider_top_3_ranked_hexes() { - let outdoor_wifi = RewardableRadio { - radio_type: RadioType::OutdoorWifi, - speedtests: Speedtest::maximum(), - location_trust_scores: LocationTrustScores::maximum(), - radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new( - &RadioType::OutdoorWifi, - vec![ - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 2, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 3, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 42, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - ], - ) - .unwrap(), - }; + let outdoor_wifi = RewardableRadio::new( + RadioType::OutdoorWifi, + Speedtest::maximum(), + LocationTrustScores::maximum().trust_scores, + RadioThreshold::Verified, + vec![ + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 2, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 3, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 42, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + ], + ) + .expect("outdoor wifi"); // rank 1 :: 1.00 * 16 == 16 // rank 2 :: 0.50 * 16 == 8 @@ -585,319 +665,302 @@ mod tests { // rank 42 :: 0.00 * 16 == 0 assert_eq!( dec!(28), - calculate_coverage_points(outdoor_wifi).coverage_points + calculate_coverage_points(outdoor_wifi).total_coverage_points ); } #[test] fn indoor_radios_only_consider_first_ranked_hexes() { - let indoor_wifi = RewardableRadio { - radio_type: RadioType::IndoorWifi, - speedtests: Speedtest::maximum(), - location_trust_scores: LocationTrustScores::maximum(), - radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new( - &RadioType::IndoorWifi, - vec![ - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 2, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 42, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - ], - ) - .unwrap(), - }; + let indoor_wifi = RewardableRadio::new( + RadioType::IndoorWifi, + Speedtest::maximum(), + LocationTrustScores::maximum().trust_scores, + RadioThreshold::Verified, + vec![ + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 2, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 42, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + ], + ) + .expect("indoor wifi"); assert_eq!( dec!(400), - calculate_coverage_points(indoor_wifi).coverage_points + calculate_coverage_points(indoor_wifi).total_coverage_points ); } #[test] fn location_trust_score_multiplier() { // Location scores are averaged together - let indoor_wifi = RewardableRadio { - radio_type: RadioType::IndoorWifi, - speedtests: Speedtest::maximum(), - location_trust_scores: LocationTrustScores::with_trust_scores(&[ - dec!(0.1), - dec!(0.2), - dec!(0.3), - dec!(0.4), - ]), - radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new( - &RadioType::IndoorWifi, - vec![RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }], - ) - .unwrap(), - }; + let indoor_wifi = RewardableRadio::new( + RadioType::IndoorWifi, + Speedtest::maximum(), + LocationTrustScores::with_trust_scores(&[dec!(0.1), dec!(0.2), dec!(0.3), dec!(0.4)]) + .trust_scores, + RadioThreshold::Verified, + vec![RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }], + ) + .expect("indoor wifi"); // Location trust scores is 1/4 assert_eq!( dec!(100), - calculate_coverage_points(indoor_wifi).coverage_points + calculate_coverage_points(indoor_wifi).total_coverage_points ); } #[test] fn boosted_hex() { - let mut indoor_wifi = RewardableRadio { - radio_type: RadioType::IndoorWifi, - speedtests: Speedtest::maximum(), - location_trust_scores: LocationTrustScores::maximum(), - radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new( - &RadioType::IndoorWifi, - vec![ - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::Low, - assignments: assignments_maximum(), - boosted: NonZeroU32::new(4), - }, - ], - ) - .unwrap(), - }; + let covered_hexes = vec![ + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::Low, + assignments: assignments_maximum(), + boosted: NonZeroU32::new(4), + }, + ]; + let indoor_wifi = RewardableRadio::new( + RadioType::IndoorWifi, + Speedtest::maximum(), + LocationTrustScores::maximum().trust_scores, + RadioThreshold::Verified, + covered_hexes.clone(), + ) + .expect("verified indoor wifi"); // The hex with a low signal_level is boosted to the same level as a // signal_level of High. assert_eq!( dec!(800), - calculate_coverage_points(indoor_wifi.clone()).coverage_points + calculate_coverage_points(indoor_wifi.clone()).total_coverage_points ); // When the radio is not verified for boosted rewards, the boost has no effect. - indoor_wifi.radio_threshold = RadioThreshold::UnVerified; + let indoor_wifi = RewardableRadio::new( + RadioType::IndoorWifi, + Speedtest::maximum(), + LocationTrustScores::maximum().trust_scores, + RadioThreshold::UnVerified, + covered_hexes, + ) + .expect("unverified indoor wifi"); assert_eq!( dec!(500), - calculate_coverage_points(indoor_wifi).coverage_points + calculate_coverage_points(indoor_wifi).total_coverage_points ); } #[test] fn base_radio_coverage_points() { - let outdoor_cbrs = RewardableRadio { - radio_type: RadioType::OutdoorCbrs, - speedtests: Speedtest::maximum(), - location_trust_scores: LocationTrustScores::maximum(), - radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new( - &RadioType::OutdoorCbrs, - vec![ - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: Some("serial".to_string()), - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: Some("serial".to_string()), - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::Medium, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: Some("serial".to_string()), - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::Low, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: Some("serial".to_string()), - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::None, - assignments: assignments_maximum(), - boosted: None, - }, - ], - ) - .unwrap(), - }; + let outdoor_cbrs = RewardableRadio::new( + RadioType::OutdoorCbrs, + Speedtest::maximum(), + LocationTrustScores::maximum().trust_scores, + RadioThreshold::Verified, + vec![ + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::Medium, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::Low, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::None, + assignments: assignments_maximum(), + boosted: None, + }, + ], + ) + .expect("outdoor cbrs"); - let indoor_cbrs = RewardableRadio { - radio_type: RadioType::IndoorCbrs, - speedtests: Speedtest::maximum(), - location_trust_scores: LocationTrustScores::maximum(), - radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new( - &RadioType::IndoorCbrs, - vec![ - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: Some("serial".to_string()), - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: Some("serial".to_string()), - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::Low, - assignments: assignments_maximum(), - boosted: None, - }, - ], - ) - .unwrap(), - }; + let indoor_cbrs = RewardableRadio::new( + RadioType::IndoorCbrs, + Speedtest::maximum(), + LocationTrustScores::maximum().trust_scores, + RadioThreshold::Verified, + vec![ + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::Low, + assignments: assignments_maximum(), + boosted: None, + }, + ], + ) + .expect("indoor cbrs"); - let outdoor_wifi = RewardableRadio { - radio_type: RadioType::OutdoorWifi, - speedtests: Speedtest::maximum(), - location_trust_scores: LocationTrustScores::maximum(), - radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new( - &RadioType::OutdoorWifi, - vec![ - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::Medium, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::Low, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::None, - assignments: assignments_maximum(), - boosted: None, - }, - ], - ) - .unwrap(), - }; + let outdoor_wifi = RewardableRadio::new( + RadioType::OutdoorWifi, + Speedtest::maximum(), + LocationTrustScores::maximum().trust_scores, + RadioThreshold::Verified, + vec![ + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::Medium, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::Low, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::None, + assignments: assignments_maximum(), + boosted: None, + }, + ], + ) + .expect("outdoor wifi"); - let indoor_wifi = RewardableRadio { - radio_type: RadioType::IndoorWifi, - speedtests: Speedtest::maximum(), - location_trust_scores: LocationTrustScores::maximum(), - radio_threshold: RadioThreshold::Verified, - covered_hexes: CoveredHexes::new( - &RadioType::IndoorWifi, - vec![ - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::Low, - assignments: assignments_maximum(), - boosted: None, - }, - ], - ) - .unwrap(), - }; + let indoor_wifi = RewardableRadio::new( + RadioType::IndoorWifi, + Speedtest::maximum(), + LocationTrustScores::maximum().trust_scores, + RadioThreshold::Verified, + vec![ + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }, + RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::Low, + assignments: assignments_maximum(), + boosted: None, + }, + ], + ) + .expect("indoor wifi"); // When each radio contains a hex of every applicable signal_level, and // multipliers are break even. These are the accumulated coverage points. assert_eq!( dec!(7), - calculate_coverage_points(outdoor_cbrs).coverage_points + calculate_coverage_points(outdoor_cbrs).total_coverage_points ); assert_eq!( dec!(125), - calculate_coverage_points(indoor_cbrs).coverage_points + calculate_coverage_points(indoor_cbrs).total_coverage_points ); assert_eq!( dec!(28), - calculate_coverage_points(outdoor_wifi).coverage_points + calculate_coverage_points(outdoor_wifi).total_coverage_points ); assert_eq!( dec!(500), - calculate_coverage_points(indoor_wifi).coverage_points + calculate_coverage_points(indoor_wifi).total_coverage_points ); } @@ -908,11 +971,13 @@ mod tests { upload_speed: BytesPs::mbps(15), download_speed: BytesPs::mbps(150), latency: Millis::new(15), + timestamp: Utc::now(), }, Self { upload_speed: BytesPs::mbps(15), download_speed: BytesPs::mbps(150), latency: Millis::new(15), + timestamp: Utc::now(), }, ] } @@ -922,6 +987,7 @@ mod tests { upload_speed: BytesPs::mbps(15), download_speed: download, latency: Millis::new(15), + timestamp: Utc::now(), } } } diff --git a/coverage_point_calculator/src/location.rs b/coverage_point_calculator/src/location.rs index d433d4c40..d3fb1138e 100644 --- a/coverage_point_calculator/src/location.rs +++ b/coverage_point_calculator/src/location.rs @@ -1,6 +1,8 @@ use rust_decimal::Decimal; use rust_decimal_macros::dec; +const RESTRICTIVE_MAX_DISTANCE: Meters = Meters(50); + #[derive(Debug, Clone, PartialEq, PartialOrd)] pub struct Meters(u32); @@ -12,9 +14,8 @@ impl Meters { #[derive(Debug, Clone, PartialEq)] pub struct LocationTrustScores { - pub any_hex_boosted_multiplier: Decimal, - pub no_boosted_hex_multiplier: Decimal, - trust_scores: Vec, + pub multiplier: Decimal, + pub trust_scores: Vec, } #[derive(Debug, Clone, PartialEq)] @@ -25,38 +26,128 @@ pub struct LocationTrust { impl LocationTrustScores { pub fn new(trust_scores: Vec) -> Self { - let boosted_multiplier = boosted_multiplier(&trust_scores); - let unboosted_multiplier = unboosted_multiplier(&trust_scores); Self { - any_hex_boosted_multiplier: boosted_multiplier, - no_boosted_hex_multiplier: unboosted_multiplier, + multiplier: multiplier(&trust_scores), trust_scores, } } -} -fn boosted_multiplier(trust_scores: &[LocationTrust]) -> Decimal { - const RESTRICTIVE_MAX_DISTANCE: Meters = Meters(50); - // Cap multipliers to 0.25x when a radio covers _any_ boosted hex - // and it's distance to asserted is above the threshold. - let count = Decimal::from(trust_scores.len()); - let scores: Decimal = trust_scores - .iter() - .map(|l| { - if l.distance_to_asserted > RESTRICTIVE_MAX_DISTANCE { - dec!(0.25).min(l.trust_score) - } else { - l.trust_score - } - }) - .sum(); + pub fn new_with_boosted_hexes(trust_scores: Vec) -> Self { + let trust_scores: Vec<_> = trust_scores + .into_iter() + .map(|l| LocationTrust { + trust_score: l.boosted_trust_score(), + distance_to_asserted: l.distance_to_asserted, + }) + .collect(); - scores / count + Self { + multiplier: multiplier(&trust_scores), + trust_scores, + } + } +} +impl LocationTrust { + fn boosted_trust_score(&self) -> Decimal { + // Cap multipliers to 0.25x when a radio covers _any_ boosted hex + // and it's distance to asserted is above the threshold. + if self.distance_to_asserted > RESTRICTIVE_MAX_DISTANCE { + dec!(0.25).min(self.trust_score) + } else { + self.trust_score + } + } } -fn unboosted_multiplier(trust_scores: &[LocationTrust]) -> Decimal { +fn multiplier(trust_scores: &[LocationTrust]) -> Decimal { let count = Decimal::from(trust_scores.len()); let scores: Decimal = trust_scores.iter().map(|l| l.trust_score).sum(); scores / count } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn boosted_hexes_within_distance_retain_trust_score() { + let lts = LocationTrustScores::new_with_boosted_hexes(vec![LocationTrust { + distance_to_asserted: Meters(49), + trust_score: dec!(1), + }]); + + assert_eq!( + LocationTrustScores { + multiplier: dec!(1), + trust_scores: vec![LocationTrust { + distance_to_asserted: Meters(49), + trust_score: dec!(1) + }] + }, + lts + ); + } + + #[test] + fn boosted_hexes_past_distance_reduce_trust_score() { + let lts = LocationTrustScores::new_with_boosted_hexes(vec![LocationTrust { + distance_to_asserted: Meters(51), + trust_score: dec!(1), + }]); + + assert_eq!( + LocationTrustScores { + multiplier: dec!(0.25), + trust_scores: vec![LocationTrust { + distance_to_asserted: Meters(51), + trust_score: dec!(0.25) + }] + }, + lts + ); + } + + #[test] + fn multiplier_is_average_of_scores() { + // All locations within max distance + let boosted_trust_scores = LocationTrustScores::new_with_boosted_hexes(vec![ + LocationTrust { + distance_to_asserted: Meters(49), + trust_score: dec!(0.5), + }, + LocationTrust { + distance_to_asserted: Meters(49), + trust_score: dec!(0.5), + }, + ]); + assert_eq!(dec!(0.5), boosted_trust_scores.multiplier); + + // 1 location within max distance, 1 location outside + let boosted_over_limit_trust_scores = LocationTrustScores::new_with_boosted_hexes(vec![ + LocationTrust { + distance_to_asserted: Meters(49), + trust_score: dec!(0.5), + }, + LocationTrust { + distance_to_asserted: Meters(51), + trust_score: dec!(0.5), + }, + ]); + let mult = (dec!(0.5) + dec!(0.25)) / dec!(2); + assert_eq!(mult, boosted_over_limit_trust_scores.multiplier); + + // All locations outside boosted distance restriction, but no boosted hexes + let unboosted_trust_scores = LocationTrustScores::new(vec![ + LocationTrust { + distance_to_asserted: Meters(100), + trust_score: dec!(0.5), + }, + LocationTrust { + distance_to_asserted: Meters(100), + trust_score: dec!(0.5), + }, + ]); + assert_eq!(dec!(0.5), unboosted_trust_scores.multiplier); + } +} diff --git a/coverage_point_calculator/src/speedtest.rs b/coverage_point_calculator/src/speedtest.rs index 52fc27fe8..02b5c56a6 100644 --- a/coverage_point_calculator/src/speedtest.rs +++ b/coverage_point_calculator/src/speedtest.rs @@ -1,8 +1,12 @@ +use chrono::{DateTime, Utc}; use rust_decimal::Decimal; use rust_decimal_macros::dec; use crate::MaxOneMultplier; +const MIN_REQUIRED_SPEEDTEST_SAMPLES: usize = 2; +const MAX_REQUIRED_SPEEDTEST_SAMPLES: usize = 6; + #[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)] pub struct BytesPs(u64); @@ -29,11 +33,43 @@ impl Millis { } } +#[derive(Debug, Clone)] +pub struct Speedtests { + pub multiplier: Decimal, + pub speedtests: Vec, +} + +impl Speedtests { + pub fn new(speedtests: Vec) -> Self { + let mut sorted_speedtests = speedtests; + sorted_speedtests.sort_by_key(|test| test.timestamp); + + let multiplier = if sorted_speedtests.len() < MIN_REQUIRED_SPEEDTEST_SAMPLES { + SpeedtestTier::Fail.multiplier() + } else { + Speedtest::avg(&sorted_speedtests).multiplier() + }; + + Self { + multiplier, + speedtests: sorted_speedtests + .into_iter() + .take(MAX_REQUIRED_SPEEDTEST_SAMPLES) + .collect(), + } + } + + pub fn avg(&self) -> Speedtest { + Speedtest::avg(&self.speedtests) + } +} + #[derive(Debug, Default, Clone, Copy, PartialEq)] pub struct Speedtest { pub upload_speed: BytesPs, pub download_speed: BytesPs, pub latency: Millis, + pub timestamp: DateTime, } impl Speedtest { @@ -62,6 +98,7 @@ impl Speedtest { upload_speed: BytesPs::new(upload / count as u64), download_speed: BytesPs::new(download / count as u64), latency: Millis::new(latency / count as u32), + timestamp: Utc::now(), } } } diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index 80e75deb6..64f8eb98c 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -1,5 +1,6 @@ use std::{collections::HashMap, num::NonZeroU32, str::FromStr}; +use chrono::Utc; use coverage_map::{RankedCoverage, SignalLevel}; use coverage_point_calculator::{ calculate_coverage_points, @@ -17,11 +18,13 @@ fn base_radio_coverage_points() { upload_speed: BytesPs::mbps(15), download_speed: BytesPs::mbps(150), latency: Millis::new(15), + timestamp: Utc::now(), }, Speedtest { upload_speed: BytesPs::mbps(15), download_speed: BytesPs::mbps(150), latency: Millis::new(15), + timestamp: Utc::now(), }, ]; let location_trust_scores = vec![LocationTrust { @@ -66,13 +69,18 @@ fn base_radio_coverage_points() { radios.push(radio.clone()); println!( "{radio_type:?} \t--> {}", - calculate_coverage_points(radio).coverage_points + calculate_coverage_points(radio).total_coverage_points ); } let output = radios .into_iter() - .map(|r| (r.radio_type, calculate_coverage_points(r).coverage_points)) + .map(|r| { + ( + r.radio_type, + calculate_coverage_points(r).total_coverage_points, + ) + }) .collect::>(); println!("{output:#?}"); } @@ -124,11 +132,13 @@ fn radio_unique_coverage() { upload_speed: BytesPs::mbps(15), download_speed: BytesPs::mbps(150), latency: Millis::new(15), + timestamp: Utc::now(), }, Speedtest { upload_speed: BytesPs::mbps(15), download_speed: BytesPs::mbps(150), latency: Millis::new(15), + timestamp: Utc::now(), }, ]; let default_location_trust_scores = vec![LocationTrust { @@ -157,7 +167,12 @@ fn radio_unique_coverage() { let coverage_points = radios .into_iter() - .map(|r| (r.radio_type, calculate_coverage_points(r).coverage_points)) + .map(|r| { + ( + r.radio_type, + calculate_coverage_points(r).total_coverage_points, + ) + }) .collect::>(); println!("{coverage_points:#?}") } From 0b6df3de0a8659d9869ef265753ab79970ada827 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 5 Jun 2024 12:58:14 -0700 Subject: [PATCH 053/115] Restrict construction of RewardableRadio Make the fields private so users have to go through the constructor to get a radio, and cannot change values of fields after the fact, as it will not trigger anything to be recalculated. --- coverage_point_calculator/src/lib.rs | 14 +++++++++----- .../tests/coverage_point_calculator.rs | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index a81e1f48a..0a620039c 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -67,11 +67,11 @@ pub type MaxOneMultplier = Decimal; /// Input Radio to calculation #[derive(Debug, Clone)] pub struct RewardableRadio { - pub radio_type: RadioType, - pub speedtests: Speedtests, - pub location_trust_scores: LocationTrustScores, - pub radio_threshold: RadioThreshold, - pub covered_hexes: CoveredHexes, + radio_type: RadioType, + speedtests: Speedtests, + location_trust_scores: LocationTrustScores, + radio_threshold: RadioThreshold, + covered_hexes: CoveredHexes, } #[derive(Debug)] @@ -177,6 +177,10 @@ impl RewardableRadio { }) } + pub fn radio_type(&self) -> RadioType { + self.radio_type + } + // These points need to be reported in the proto pre-(location, speedtest) multipliers pub fn accumulate_hex_coverage_points(&self) -> Decimal { self.covered_hexes diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index 64f8eb98c..c6f36a4b7 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -77,7 +77,7 @@ fn base_radio_coverage_points() { .into_iter() .map(|r| { ( - r.radio_type, + r.radio_type(), calculate_coverage_points(r).total_coverage_points, ) }) @@ -169,7 +169,7 @@ fn radio_unique_coverage() { .into_iter() .map(|r| { ( - r.radio_type, + r.radio_type(), calculate_coverage_points(r).total_coverage_points, ) }) From a6ee1024af075b7bb1f697ce4e8e250a95ed3721 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 5 Jun 2024 13:41:30 -0700 Subject: [PATCH 054/115] enforce maximum used speedtetss --- coverage_point_calculator/src/speedtest.rs | 65 ++++++++++++++++++++-- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/coverage_point_calculator/src/speedtest.rs b/coverage_point_calculator/src/speedtest.rs index 02b5c56a6..ae8fa05cc 100644 --- a/coverage_point_calculator/src/speedtest.rs +++ b/coverage_point_calculator/src/speedtest.rs @@ -5,7 +5,7 @@ use rust_decimal_macros::dec; use crate::MaxOneMultplier; const MIN_REQUIRED_SPEEDTEST_SAMPLES: usize = 2; -const MAX_REQUIRED_SPEEDTEST_SAMPLES: usize = 6; +const MAX_ALLOWED_SPEEDTEST_SAMPLES: usize = 6; #[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)] pub struct BytesPs(u64); @@ -41,8 +41,14 @@ pub struct Speedtests { impl Speedtests { pub fn new(speedtests: Vec) -> Self { + // sort Newest to Oldest let mut sorted_speedtests = speedtests; - sorted_speedtests.sort_by_key(|test| test.timestamp); + sorted_speedtests.sort_by_key(|test| std::cmp::Reverse(test.timestamp)); + + let sorted_speedtests: Vec<_> = sorted_speedtests + .into_iter() + .take(MAX_ALLOWED_SPEEDTEST_SAMPLES) + .collect(); let multiplier = if sorted_speedtests.len() < MIN_REQUIRED_SPEEDTEST_SAMPLES { SpeedtestTier::Fail.multiplier() @@ -52,10 +58,7 @@ impl Speedtests { Self { multiplier, - speedtests: sorted_speedtests - .into_iter() - .take(MAX_REQUIRED_SPEEDTEST_SAMPLES) - .collect(), + speedtests: sorted_speedtests, } } @@ -182,4 +185,54 @@ mod tests { assert_eq!(Poor, SpeedtestTier::from_latency(&Millis::new(99))); assert_eq!(Fail, SpeedtestTier::from_latency(&Millis::new(101))); } + + #[test] + fn restrict_to_maximum_speedtests_allowed() { + let base = Speedtest { + upload_speed: BytesPs::mbps(15), + download_speed: BytesPs::mbps(150), + latency: Millis::new(15), + timestamp: Utc::now(), + }; + let speedtests = std::iter::repeat(base).take(10).collect(); + let speedtests = Speedtests::new(speedtests); + + assert_eq!(MAX_ALLOWED_SPEEDTEST_SAMPLES, speedtests.speedtests.len()); + } + + #[test] + fn speedtests_ordered_newest_to_oldest() { + let make_speedtest = |timestamp: DateTime, latency: Millis| Speedtest { + upload_speed: BytesPs::mbps(15), + download_speed: BytesPs::mbps(150), + latency, + timestamp, + }; + + let speedtests = Speedtests::new(vec![ + make_speedtest(date(2024, 4, 6), Millis::new(15)), + make_speedtest(date(2024, 4, 5), Millis::new(15)), + make_speedtest(date(2024, 4, 4), Millis::new(15)), + make_speedtest(date(2024, 4, 3), Millis::new(15)), + make_speedtest(date(2024, 4, 2), Millis::new(15)), + make_speedtest(date(2024, 4, 1), Millis::new(15)), + // + make_speedtest(date(2022, 4, 6), Millis::new(999)), + make_speedtest(date(2022, 4, 5), Millis::new(999)), + make_speedtest(date(2022, 4, 4), Millis::new(999)), + make_speedtest(date(2022, 4, 3), Millis::new(999)), + make_speedtest(date(2022, 4, 2), Millis::new(999)), + make_speedtest(date(2022, 4, 1), Millis::new(999)), + ]); + println!("{speedtests:?}"); + assert_eq!(dec!(1), speedtests.multiplier); + } + + fn date(year: i32, month: u32, day: u32) -> DateTime { + chrono::NaiveDate::from_ymd_opt(year, month, day) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap() + .and_utc() + } } From 7821a86d6a9a700f8d85a583cbb70818d86e37c3 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 5 Jun 2024 13:41:57 -0700 Subject: [PATCH 055/115] move multiplier calculation into location crate needs to know about radio type --- coverage_point_calculator/src/location.rs | 114 +++++++++++++--------- 1 file changed, 69 insertions(+), 45 deletions(-) diff --git a/coverage_point_calculator/src/location.rs b/coverage_point_calculator/src/location.rs index d3fb1138e..d73b08e96 100644 --- a/coverage_point_calculator/src/location.rs +++ b/coverage_point_calculator/src/location.rs @@ -1,6 +1,8 @@ use rust_decimal::Decimal; use rust_decimal_macros::dec; +use crate::RadioType; + const RESTRICTIVE_MAX_DISTANCE: Meters = Meters(50); #[derive(Debug, Clone, PartialEq, PartialOrd)] @@ -25,14 +27,24 @@ pub struct LocationTrust { } impl LocationTrustScores { - pub fn new(trust_scores: Vec) -> Self { + pub fn new(radio_type: &RadioType, trust_scores: Vec) -> Self { + // CBRS radios are always trusted because they have internal GPS + let multiplier = if radio_type.is_cbrs() { + dec!(1) + } else { + multiplier(&trust_scores) + }; + Self { - multiplier: multiplier(&trust_scores), + multiplier, trust_scores, } } - pub fn new_with_boosted_hexes(trust_scores: Vec) -> Self { + pub fn new_with_boosted_hexes( + radio_type: &RadioType, + trust_scores: Vec, + ) -> Self { let trust_scores: Vec<_> = trust_scores .into_iter() .map(|l| LocationTrust { @@ -41,10 +53,7 @@ impl LocationTrustScores { }) .collect(); - Self { - multiplier: multiplier(&trust_scores), - trust_scores, - } + Self::new(radio_type, trust_scores) } } impl LocationTrust { @@ -72,10 +81,13 @@ mod tests { #[test] fn boosted_hexes_within_distance_retain_trust_score() { - let lts = LocationTrustScores::new_with_boosted_hexes(vec![LocationTrust { - distance_to_asserted: Meters(49), - trust_score: dec!(1), - }]); + let lts = LocationTrustScores::new_with_boosted_hexes( + &RadioType::IndoorWifi, + vec![LocationTrust { + distance_to_asserted: Meters(49), + trust_score: dec!(1), + }], + ); assert_eq!( LocationTrustScores { @@ -91,10 +103,13 @@ mod tests { #[test] fn boosted_hexes_past_distance_reduce_trust_score() { - let lts = LocationTrustScores::new_with_boosted_hexes(vec![LocationTrust { - distance_to_asserted: Meters(51), - trust_score: dec!(1), - }]); + let lts = LocationTrustScores::new_with_boosted_hexes( + &RadioType::IndoorWifi, + vec![LocationTrust { + distance_to_asserted: Meters(51), + trust_score: dec!(1), + }], + ); assert_eq!( LocationTrustScores { @@ -111,43 +126,52 @@ mod tests { #[test] fn multiplier_is_average_of_scores() { // All locations within max distance - let boosted_trust_scores = LocationTrustScores::new_with_boosted_hexes(vec![ - LocationTrust { - distance_to_asserted: Meters(49), - trust_score: dec!(0.5), - }, - LocationTrust { - distance_to_asserted: Meters(49), - trust_score: dec!(0.5), - }, - ]); + let boosted_trust_scores = LocationTrustScores::new_with_boosted_hexes( + &RadioType::IndoorWifi, + vec![ + LocationTrust { + distance_to_asserted: Meters(49), + trust_score: dec!(0.5), + }, + LocationTrust { + distance_to_asserted: Meters(49), + trust_score: dec!(0.5), + }, + ], + ); assert_eq!(dec!(0.5), boosted_trust_scores.multiplier); // 1 location within max distance, 1 location outside - let boosted_over_limit_trust_scores = LocationTrustScores::new_with_boosted_hexes(vec![ - LocationTrust { - distance_to_asserted: Meters(49), - trust_score: dec!(0.5), - }, - LocationTrust { - distance_to_asserted: Meters(51), - trust_score: dec!(0.5), - }, - ]); + let boosted_over_limit_trust_scores = LocationTrustScores::new_with_boosted_hexes( + &RadioType::IndoorWifi, + vec![ + LocationTrust { + distance_to_asserted: Meters(49), + trust_score: dec!(0.5), + }, + LocationTrust { + distance_to_asserted: Meters(51), + trust_score: dec!(0.5), + }, + ], + ); let mult = (dec!(0.5) + dec!(0.25)) / dec!(2); assert_eq!(mult, boosted_over_limit_trust_scores.multiplier); // All locations outside boosted distance restriction, but no boosted hexes - let unboosted_trust_scores = LocationTrustScores::new(vec![ - LocationTrust { - distance_to_asserted: Meters(100), - trust_score: dec!(0.5), - }, - LocationTrust { - distance_to_asserted: Meters(100), - trust_score: dec!(0.5), - }, - ]); + let unboosted_trust_scores = LocationTrustScores::new( + &RadioType::IndoorWifi, + vec![ + LocationTrust { + distance_to_asserted: Meters(100), + trust_score: dec!(0.5), + }, + LocationTrust { + distance_to_asserted: Meters(100), + trust_score: dec!(0.5), + }, + ], + ); assert_eq!(dec!(0.5), unboosted_trust_scores.multiplier); } } From efa1c37f6fc95893f9794fb6d9a002e90e56bd9b Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 5 Jun 2024 13:42:52 -0700 Subject: [PATCH 056/115] bring in speedtest and location changes use contstructors when we can, that's when calculations are done for multipliers --- coverage_point_calculator/src/lib.rs | 219 ++++++++++++--------------- 1 file changed, 98 insertions(+), 121 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 0a620039c..e29421455 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -72,6 +72,7 @@ pub struct RewardableRadio { location_trust_scores: LocationTrustScores, radio_threshold: RadioThreshold, covered_hexes: CoveredHexes, + eligible_for_boosted_hexes: bool, } #[derive(Debug)] @@ -101,9 +102,9 @@ pub enum Error { } pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { - let hex_coverage_points = radio.accumulate_hex_coverage_points(); - let location_trust_multiplier = radio.location_trust_multiplier(); - let speedtest_multiplier = radio.speedtest_multiplier(); + let hex_coverage_points = radio.covered_hexes.accumulated_calculated_coverage_points(); + let location_trust_multiplier = radio.location_trust_scores.multiplier; + let speedtest_multiplier = radio.speedtests.multiplier; let coverage_points = hex_coverage_points * location_trust_multiplier * speedtest_multiplier; let total_coverage_points = coverage_points.round_dp_with_strategy(2, RoundingStrategy::ToZero); @@ -114,25 +115,22 @@ pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { location_trust_multiplier, speedtest_multiplier, // Radio information - eligible_for_boosted_hexes: radio.eligible_for_boosted_hexes(), radio_type: radio.radio_type, radio_threshold: radio.radio_threshold, - // TODO: only return the speedtests used for calculation speedtests: radio.speedtests.speedtests, location_trust_scores: radio.location_trust_scores.trust_scores, covered_hexes: radio.covered_hexes.hexes, + eligible_for_boosted_hexes: radio.eligible_for_boosted_hexes, } } impl CoveragePoints { pub fn iter_boosted_hexes(&self) -> impl Iterator { - let verified = self.radio_threshold.threshold_met(); let eligible = self.eligible_for_boosted_hexes; self.covered_hexes .clone() .into_iter() - .filter(move |_| verified) .filter(move |_| eligible) .filter(|hex| hex.boosted_multiplier.is_some()) } @@ -143,97 +141,61 @@ impl RewardableRadio { radio_type: RadioType, speedtests: Vec, location_trust_scores: Vec, - verified_radio_threshold: RadioThreshold, + radio_threshold: RadioThreshold, covered_hexes: Vec, ) -> Result { let any_boosted_hexes = any_boosted(&covered_hexes); let location_trust_scores = if any_boosted_hexes { - LocationTrustScores::new_with_boosted_hexes(location_trust_scores) + LocationTrustScores::new_with_boosted_hexes(&radio_type, location_trust_scores) } else { - LocationTrustScores::new(location_trust_scores) + LocationTrustScores::new(&radio_type, location_trust_scores) }; - let location_multiplier = location_trust_scores.multiplier; - let mut eligible_for_boosted_hexes = true; - if radio_type.is_wifi() && location_multiplier < dec!(0.75) { - eligible_for_boosted_hexes = false; - } - if !verified_radio_threshold.threshold_met() { - eligible_for_boosted_hexes = false; - } + let eligible_for_boosted_hexes = eligible_for_boosted_hexes( + &radio_type, + location_trust_scores.multiplier, + &radio_threshold, + ); let speedtests = Speedtests::new(speedtests); + let covered_hexes = if eligible_for_boosted_hexes { + CoveredHexes::new(&radio_type, covered_hexes)? + } else { + CoveredHexes::new_removing_boosts(&radio_type, covered_hexes)? + }; + Ok(Self { radio_type, speedtests, location_trust_scores, - radio_threshold: verified_radio_threshold, - covered_hexes: CoveredHexes::new( - &radio_type, - eligible_for_boosted_hexes, - covered_hexes, - )?, + radio_threshold, + covered_hexes, + eligible_for_boosted_hexes, }) } pub fn radio_type(&self) -> RadioType { self.radio_type } +} - // These points need to be reported in the proto pre-(location, speedtest) multipliers - pub fn accumulate_hex_coverage_points(&self) -> Decimal { - self.covered_hexes - .hexes - .iter() - .map(|hex| hex.calculated_coverage_points) - .sum() - } - - pub fn iter_covered_hexes(&self) -> impl Iterator { - self.covered_hexes.hexes.clone().into_iter() - } - - fn eligible_for_boosted_hexes(&self) -> bool { - // hip93: if radio is wifi & location_trust score multiplier < 0.75, no boosting - if self.is_wifi() { - return self.location_trust_multiplier() > dec!(0.75); - } - - // hip84: if radio has not met minimum data and subscriber thresholds, no boosting - if !self.radio_threshold.threshold_met() { - return false; - } - - true - } - - pub fn location_trust_multiplier(&self) -> Decimal { - // CBRS radios are always trusted because they have internal GPS - if self.is_cbrs() { - return dec!(1); - } - - self.location_trust_scores.multiplier - } - - pub fn speedtest_multiplier(&self) -> MaxOneMultplier { - self.speedtests.multiplier +fn eligible_for_boosted_hexes( + radio_type: &RadioType, + location_trust_score: Decimal, + radio_threshold: &RadioThreshold, +) -> bool { + // hip93: if radio is wifi & location_trust score multiplier < 0.75, no boosting + if radio_type.is_wifi() && location_trust_score < dec!(0.75) { + return false; } - fn is_wifi(&self) -> bool { - matches!( - self.radio_type, - RadioType::IndoorWifi | RadioType::OutdoorWifi - ) + // hip84: if radio has not met minimum data and subscriber thresholds, no boosting + if !radio_threshold.threshold_met() { + return false; } - fn is_cbrs(&self) -> bool { - matches!( - self.radio_type, - RadioType::IndoorCbrs | RadioType::OutdoorCbrs - ) - } + true } #[derive(Debug, Clone)] @@ -262,11 +224,22 @@ fn any_boosted(ranked_coverage: &[RankedCoverage]) -> bool { } impl CoveredHexes { - fn new( + fn new_removing_boosts( radio_type: &RadioType, - eligible_for_boosted_hexes: bool, ranked_coverage: Vec, ) -> Result { + let ranked_coverage: Vec<_> = ranked_coverage + .into_iter() + .map(|ranked| RankedCoverage { + boosted: None, + ..ranked + }) + .collect(); + + Self::new(radio_type, ranked_coverage) + } + + fn new(radio_type: &RadioType, ranked_coverage: Vec) -> Result { let rank_multipliers = radio_type.rank_multipliers(); // verify all hexes can obtain a base coverage point @@ -280,11 +253,7 @@ impl CoveredHexes { .cloned() .unwrap_or(dec!(0)); - let boosted_multiplier = if eligible_for_boosted_hexes { - ranked.boosted.map(|boost| boost.get()).map(Decimal::from) - } else { - None - }; + let boosted_multiplier = ranked.boosted.map(|boost| boost.get()).map(Decimal::from); let calculated_coverage_points = coverage_points * assignment_multiplier @@ -308,6 +277,13 @@ impl CoveredHexes { hexes: covered_hexes, }) } + + fn accumulated_calculated_coverage_points(&self) -> Decimal { + self.hexes + .iter() + .map(|hex| hex.calculated_coverage_points) + .sum() + } } #[derive(Debug, Clone, Copy, PartialEq)] @@ -356,9 +332,13 @@ impl RadioType { } } - fn is_wifi(&self) -> bool { + pub fn is_wifi(&self) -> bool { matches!(self, Self::IndoorWifi | Self::OutdoorWifi) } + + pub fn is_cbrs(&self) -> bool { + matches!(self, Self::IndoorCbrs | Self::OutdoorCbrs) + } } #[derive(Debug, Clone, PartialEq)] @@ -425,17 +405,17 @@ mod tests { .base_coverage_points(&SignalLevel::High) .unwrap(); // Boosted Hex get's radio over the base_points - let trusted_location = LocationTrustScores::with_trust_scores(&[dec!(1), dec!(1)]); - let trusted_wifi = make_wifi(trusted_location.trust_scores); - assert!(trusted_wifi.location_trust_multiplier() > dec!(0.75)); + let trusted_location = LocationTrust::with_trust_scores(&[dec!(1), dec!(1)]); + let trusted_wifi = make_wifi(trusted_location); + assert!(trusted_wifi.location_trust_scores.multiplier > dec!(0.75)); assert!( calculate_coverage_points(trusted_wifi.clone()).total_coverage_points > base_points ); // degraded location score get's radio under base_points - let untrusted_location = LocationTrustScores::with_trust_scores(&[dec!(0.1), dec!(0.2)]); - let untrusted_wifi = make_wifi(untrusted_location.trust_scores); - assert!(untrusted_wifi.location_trust_multiplier() < dec!(0.75)); + let untrusted_location = LocationTrust::with_trust_scores(&[dec!(0.1), dec!(0.2)]); + let untrusted_wifi = make_wifi(untrusted_location); + assert!(untrusted_wifi.location_trust_scores.multiplier < dec!(0.75)); assert!(calculate_coverage_points(untrusted_wifi).total_coverage_points < base_points); } @@ -464,17 +444,17 @@ mod tests { .base_coverage_points(&SignalLevel::High) .unwrap(); // Boosted Hex get's radio over the base_points - let trusted_location = LocationTrustScores::with_trust_scores(&[dec!(1), dec!(1)]); - let trusted_wifi = make_wifi(trusted_location.trust_scores); - assert!(trusted_wifi.location_trust_multiplier() > dec!(0.75)); + let trusted_location = LocationTrust::with_trust_scores(&[dec!(1), dec!(1)]); + let trusted_wifi = make_wifi(trusted_location); + assert!(trusted_wifi.location_trust_scores.multiplier > dec!(0.75)); assert!( calculate_coverage_points(trusted_wifi.clone()).total_coverage_points > base_points ); // degraded location score get's radio under base_points - let untrusted_location = LocationTrustScores::with_trust_scores(&[dec!(0.1), dec!(0.2)]); - let untrusted_wifi = make_wifi(untrusted_location.trust_scores); - assert!(untrusted_wifi.location_trust_multiplier() < dec!(0.75)); + let untrusted_location = LocationTrust::with_trust_scores(&[dec!(0.1), dec!(0.2)]); + let untrusted_wifi = make_wifi(untrusted_location); + assert!(untrusted_wifi.location_trust_scores.multiplier < dec!(0.75)); assert!(calculate_coverage_points(untrusted_wifi).total_coverage_points < base_points); } @@ -484,7 +464,7 @@ mod tests { RewardableRadio::new( RadioType::IndoorCbrs, speedtests, - LocationTrustScores::maximum().trust_scores, + LocationTrust::maximum(), RadioThreshold::Verified, vec![RankedCoverage { hotspot_key: pubkey(), @@ -568,7 +548,7 @@ mod tests { let indoor_cbrs = RewardableRadio::new( RadioType::IndoorCbrs, Speedtest::maximum(), - LocationTrustScores::maximum().trust_scores, + LocationTrust::maximum(), RadioThreshold::Verified, vec![ // yellow - POI ≥ 1 Urbanized @@ -620,7 +600,7 @@ mod tests { let outdoor_wifi = RewardableRadio::new( RadioType::OutdoorWifi, Speedtest::maximum(), - LocationTrustScores::maximum().trust_scores, + LocationTrust::maximum(), RadioThreshold::Verified, vec![ RankedCoverage { @@ -678,7 +658,7 @@ mod tests { let indoor_wifi = RewardableRadio::new( RadioType::IndoorWifi, Speedtest::maximum(), - LocationTrustScores::maximum().trust_scores, + LocationTrust::maximum(), RadioThreshold::Verified, vec![ RankedCoverage { @@ -724,8 +704,7 @@ mod tests { let indoor_wifi = RewardableRadio::new( RadioType::IndoorWifi, Speedtest::maximum(), - LocationTrustScores::with_trust_scores(&[dec!(0.1), dec!(0.2), dec!(0.3), dec!(0.4)]) - .trust_scores, + LocationTrust::with_trust_scores(&[dec!(0.1), dec!(0.2), dec!(0.3), dec!(0.4)]), RadioThreshold::Verified, vec![RankedCoverage { hotspot_key: pubkey(), @@ -771,7 +750,7 @@ mod tests { let indoor_wifi = RewardableRadio::new( RadioType::IndoorWifi, Speedtest::maximum(), - LocationTrustScores::maximum().trust_scores, + LocationTrust::maximum(), RadioThreshold::Verified, covered_hexes.clone(), ) @@ -787,7 +766,7 @@ mod tests { let indoor_wifi = RewardableRadio::new( RadioType::IndoorWifi, Speedtest::maximum(), - LocationTrustScores::maximum().trust_scores, + LocationTrust::maximum(), RadioThreshold::UnVerified, covered_hexes, ) @@ -803,7 +782,7 @@ mod tests { let outdoor_cbrs = RewardableRadio::new( RadioType::OutdoorCbrs, Speedtest::maximum(), - LocationTrustScores::maximum().trust_scores, + LocationTrust::maximum(), RadioThreshold::Verified, vec![ RankedCoverage { @@ -849,7 +828,7 @@ mod tests { let indoor_cbrs = RewardableRadio::new( RadioType::IndoorCbrs, Speedtest::maximum(), - LocationTrustScores::maximum().trust_scores, + LocationTrust::maximum(), RadioThreshold::Verified, vec![ RankedCoverage { @@ -877,7 +856,7 @@ mod tests { let outdoor_wifi = RewardableRadio::new( RadioType::OutdoorWifi, Speedtest::maximum(), - LocationTrustScores::maximum().trust_scores, + LocationTrust::maximum(), RadioThreshold::Verified, vec![ RankedCoverage { @@ -923,7 +902,7 @@ mod tests { let indoor_wifi = RewardableRadio::new( RadioType::IndoorWifi, Speedtest::maximum(), - LocationTrustScores::maximum().trust_scores, + LocationTrust::maximum(), RadioThreshold::Verified, vec![ RankedCoverage { @@ -996,26 +975,24 @@ mod tests { } } - impl LocationTrustScores { - fn maximum() -> Self { - Self::new(vec![LocationTrust { + impl LocationTrust { + fn maximum() -> Vec { + vec![LocationTrust { distance_to_asserted: Meters::new(1), trust_score: dec!(1.0), - }]) + }] } - fn with_trust_scores(trust_scores: &[Decimal]) -> Self { - Self::new( - trust_scores - .to_owned() - .iter() - .copied() - .map(|trust_score| LocationTrust { - distance_to_asserted: Meters::new(1), - trust_score, - }) - .collect(), - ) + fn with_trust_scores(trust_scores: &[Decimal]) -> Vec { + trust_scores + .to_owned() + .iter() + .copied() + .map(|trust_score| LocationTrust { + distance_to_asserted: Meters::new(1), + trust_score, + }) + .collect() } } From d113dd499823f4f45ac0bd821bcfbc0672fe9a85 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 5 Jun 2024 14:13:41 -0700 Subject: [PATCH 057/115] reorder doc links by HIP# --- coverage_point_calculator/src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index e29421455..9c5346cba 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -21,7 +21,8 @@ /// /// - location_trust_score_multiplier /// - [HIP-98][qos-score] -/// - increase Boosted hex restriction, 30m -> 50m [HIP-93][boosted-hex-restriction] +/// - states 30m requirement for boosted hexes [HIP-107][prevent-gaming] +/// - increase Boosted hex restriction, 30m -> 50m [Pull Request][boosted-hex-restriction] /// /// - speedtest_multiplier /// - [HIP-74][modeled-coverage] @@ -30,19 +31,18 @@ /// /// ## References: /// [modeled-coverage]: https://github.com/helium/HIP/blob/main/0074-mobile-poc-modeled-coverage-rewards.md#outdoor-radios -/// [cbrs-experimental]: https://github.com/helium/HIP/blob/main/0113-reward-cbrs-as-experimental.md -/// [oracle-boosting]: https://github.com/helium/HIP/blob/main/0103-oracle-hex-boosting.md -/// [hex-limits]: https://github.com/helium/HIP/blob/main/0105-modification-of-mobile-subdao-hex-limits.md /// [provider-boosting]: https://github.com/helium/HIP/blob/main/0084-service-provider-hex-boosting.md -/// [qos-score]: https://github.com/helium/HIP/blob/main/0098-mobile-subdao-quality-of-service-requirements.md /// [wifi-aps]: https://github.com/helium/HIP/blob/main/0093-addition-of-wifi-aps-to-mobile-subdao.md +/// [qos-score]: https://github.com/helium/HIP/blob/main/0098-mobile-subdao-quality-of-service-requirements.md +/// [oracle-boosting]: https://github.com/helium/HIP/blob/main/0103-oracle-hex-boosting.md +/// [hex-limits]: https://github.com/helium/HIP/blob/main/0105-modification-of-mobile-subdao-hex-limits.md +/// [prevent-gaming]: https://github.com/helium/HIP/blob/main/0107-preventing-gaming-within-the-mobile-network.md +/// [cbrs-experimental]: https://github.com/helium/HIP/blob/main/0113-reward-cbrs-as-experimental.md /// [mobile-poc-blog]: https://docs.helium.com/mobile/proof-of-coverage /// [boosted-hex-restriction]: https://github.com/helium/oracles/pull/808 /// /// To Integrate in Docs: /// -/// Some verbiage about ranks. -/// https://github.com/helium/HIP/blob/main/0105-modification-of-mobile-subdao-hex-limits.md /// /// Has something to say about 30meters from asserted location wrt poc rewards /// for boosted hexes. From 47999df12506c55d3837fb7f08ab981f62a40021 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 5 Jun 2024 14:17:43 -0700 Subject: [PATCH 058/115] move hexes to their own module --- coverage_point_calculator/src/hexes.rs | 90 +++++++++++++++++++++ coverage_point_calculator/src/lib.rs | 105 +++---------------------- 2 files changed, 100 insertions(+), 95 deletions(-) create mode 100644 coverage_point_calculator/src/hexes.rs diff --git a/coverage_point_calculator/src/hexes.rs b/coverage_point_calculator/src/hexes.rs new file mode 100644 index 000000000..14c9b01b8 --- /dev/null +++ b/coverage_point_calculator/src/hexes.rs @@ -0,0 +1,90 @@ +use coverage_map::RankedCoverage; +use hex_assignments::assignment::HexAssignments; +use rust_decimal::Decimal; +use rust_decimal_macros::dec; + +use crate::{RadioType, Result}; + +#[derive(Debug, Clone)] +pub struct CoveredHexes { + pub hexes: Vec, +} + +#[derive(Debug, Clone)] +pub struct CoveredHex { + pub hex: hextree::Cell, + // -- + pub base_coverage_points: Decimal, + pub calculated_coverage_points: Decimal, + // oracle boosted + pub assignments: HexAssignments, + pub assignment_multiplier: Decimal, + // -- + pub rank: usize, + pub rank_multiplier: Decimal, + // provider boosted + pub boosted_multiplier: Option, +} + +impl CoveredHexes { + pub fn new_without_boosts( + radio_type: &RadioType, + ranked_coverage: Vec, + ) -> Result { + let ranked_coverage: Vec<_> = ranked_coverage + .into_iter() + .map(|ranked| RankedCoverage { + boosted: None, + ..ranked + }) + .collect(); + + Self::new(radio_type, ranked_coverage) + } + + pub fn new(radio_type: &RadioType, ranked_coverage: Vec) -> Result { + let rank_multipliers = radio_type.rank_multipliers(); + + // verify all hexes can obtain a base coverage point + let covered_hexes = ranked_coverage + .into_iter() + .map(|ranked| { + let coverage_points = radio_type.base_coverage_points(&ranked.signal_level)?; + let assignment_multiplier = ranked.assignments.boosting_multiplier(); + let rank_multiplier = rank_multipliers + .get(ranked.rank - 1) + .cloned() + .unwrap_or(dec!(0)); + + let boosted_multiplier = ranked.boosted.map(|boost| boost.get()).map(Decimal::from); + + let calculated_coverage_points = coverage_points + * assignment_multiplier + * rank_multiplier + * boosted_multiplier.unwrap_or(dec!(1)); + + Ok(CoveredHex { + hex: ranked.hex, + base_coverage_points: coverage_points, + calculated_coverage_points, + assignments: ranked.assignments, + assignment_multiplier, + rank: ranked.rank, + rank_multiplier, + boosted_multiplier, + }) + }) + .collect::>>()?; + + Ok(Self { + hexes: covered_hexes, + }) + } + + pub fn accumulate_calculated_coverage_points(&self) -> Decimal { + self.hexes + .iter() + .map(|hex| hex.calculated_coverage_points) + .sum() + } +} diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 9c5346cba..8f04993d6 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -49,15 +49,16 @@ /// https://github.com/helium/HIP/blob/8b1e814afa61a714b5ba63d3265e5897ab4c5116/0107-preventing-gaming-within-the-mobile-network.md /// use crate::{ + hexes::{CoveredHex, CoveredHexes}, location::{LocationTrust, LocationTrustScores}, speedtest::Speedtest, }; use coverage_map::{RankedCoverage, SignalLevel}; -use hex_assignments::assignment::HexAssignments; use rust_decimal::{Decimal, RoundingStrategy}; use rust_decimal_macros::dec; use speedtest::Speedtests; +pub mod hexes; pub mod location; pub mod speedtest; @@ -102,7 +103,7 @@ pub enum Error { } pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { - let hex_coverage_points = radio.covered_hexes.accumulated_calculated_coverage_points(); + let hex_coverage_points = radio.covered_hexes.accumulate_calculated_coverage_points(); let location_trust_multiplier = radio.location_trust_scores.multiplier; let speedtest_multiplier = radio.speedtests.multiplier; @@ -144,7 +145,11 @@ impl RewardableRadio { radio_threshold: RadioThreshold, covered_hexes: Vec, ) -> Result { - let any_boosted_hexes = any_boosted(&covered_hexes); + // QUESTION: we need to know about boosted hexes to determine location multiplier. + // The location multiplier is then used to determine if they are eligible for boosted hexes. + // In the case where they cannot use boosted hexes, should the location mulitiplier be restored? + + let any_boosted_hexes = covered_hexes.iter().any(|hex| hex.boosted.is_some()); let location_trust_scores = if any_boosted_hexes { LocationTrustScores::new_with_boosted_hexes(&radio_type, location_trust_scores) } else { @@ -157,17 +162,15 @@ impl RewardableRadio { &radio_threshold, ); - let speedtests = Speedtests::new(speedtests); - let covered_hexes = if eligible_for_boosted_hexes { CoveredHexes::new(&radio_type, covered_hexes)? } else { - CoveredHexes::new_removing_boosts(&radio_type, covered_hexes)? + CoveredHexes::new_without_boosts(&radio_type, covered_hexes)? }; Ok(Self { radio_type, - speedtests, + speedtests: Speedtests::new(speedtests), location_trust_scores, radio_threshold, covered_hexes, @@ -198,94 +201,6 @@ fn eligible_for_boosted_hexes( true } -#[derive(Debug, Clone)] -pub struct CoveredHexes { - hexes: Vec, -} - -#[derive(Debug, Clone)] -pub struct CoveredHex { - pub hex: hextree::Cell, - // -- - base_coverage_points: Decimal, - calculated_coverage_points: Decimal, - // oracle boosted - assignemnts: HexAssignments, - assignment_multiplier: Decimal, - // -- - rank: usize, - rank_multiplier: Decimal, - // provider boosted - pub boosted_multiplier: Option, -} - -fn any_boosted(ranked_coverage: &[RankedCoverage]) -> bool { - ranked_coverage.iter().any(|hex| hex.boosted.is_some()) -} - -impl CoveredHexes { - fn new_removing_boosts( - radio_type: &RadioType, - ranked_coverage: Vec, - ) -> Result { - let ranked_coverage: Vec<_> = ranked_coverage - .into_iter() - .map(|ranked| RankedCoverage { - boosted: None, - ..ranked - }) - .collect(); - - Self::new(radio_type, ranked_coverage) - } - - fn new(radio_type: &RadioType, ranked_coverage: Vec) -> Result { - let rank_multipliers = radio_type.rank_multipliers(); - - // verify all hexes can obtain a base coverage point - let covered_hexes = ranked_coverage - .into_iter() - .map(|ranked| { - let coverage_points = radio_type.base_coverage_points(&ranked.signal_level)?; - let assignment_multiplier = ranked.assignments.boosting_multiplier(); - let rank_multiplier = rank_multipliers - .get(ranked.rank - 1) - .cloned() - .unwrap_or(dec!(0)); - - let boosted_multiplier = ranked.boosted.map(|boost| boost.get()).map(Decimal::from); - - let calculated_coverage_points = coverage_points - * assignment_multiplier - * rank_multiplier - * boosted_multiplier.unwrap_or(dec!(1)); - - Ok(CoveredHex { - hex: ranked.hex, - base_coverage_points: coverage_points, - calculated_coverage_points, - assignemnts: ranked.assignments, - assignment_multiplier, - rank: ranked.rank, - rank_multiplier, - boosted_multiplier, - }) - }) - .collect::>>()?; - - Ok(Self { - hexes: covered_hexes, - }) - } - - fn accumulated_calculated_coverage_points(&self) -> Decimal { - self.hexes - .iter() - .map(|hex| hex.calculated_coverage_points) - .sum() - } -} - #[derive(Debug, Clone, Copy, PartialEq)] pub enum RadioType { IndoorWifi, From 28a0aa01448a4b72769eb4ab478537c1e2836205 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 5 Jun 2024 14:18:56 -0700 Subject: [PATCH 059/115] construct new location trust for boosted version --- coverage_point_calculator/src/location.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/coverage_point_calculator/src/location.rs b/coverage_point_calculator/src/location.rs index d73b08e96..3f2b0db31 100644 --- a/coverage_point_calculator/src/location.rs +++ b/coverage_point_calculator/src/location.rs @@ -47,23 +47,25 @@ impl LocationTrustScores { ) -> Self { let trust_scores: Vec<_> = trust_scores .into_iter() - .map(|l| LocationTrust { - trust_score: l.boosted_trust_score(), - distance_to_asserted: l.distance_to_asserted, - }) + .map(LocationTrust::into_boosted) .collect(); Self::new(radio_type, trust_scores) } } impl LocationTrust { - fn boosted_trust_score(&self) -> Decimal { + fn into_boosted(self) -> Self { // Cap multipliers to 0.25x when a radio covers _any_ boosted hex // and it's distance to asserted is above the threshold. - if self.distance_to_asserted > RESTRICTIVE_MAX_DISTANCE { + let trust_score = if self.distance_to_asserted > RESTRICTIVE_MAX_DISTANCE { dec!(0.25).min(self.trust_score) } else { self.trust_score + }; + + LocationTrust { + trust_score, + distance_to_asserted: self.distance_to_asserted, } } } From 51150b2c656d333f0ad6e985c87fcbe937e509d7 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 5 Jun 2024 14:52:57 -0700 Subject: [PATCH 060/115] add notable conditions to docs --- coverage_point_calculator/src/lib.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 8f04993d6..16ad6433d 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -29,6 +29,18 @@ /// - added "Good" speedtest tier [HIP-98][qos-score] /// - latency is explicitly under limit in HIP https://github.com/helium/oracles/pull/737 /// +/// ## Notable Conditions: +/// - Location +/// - If a Radio covers any boosted hexes, [LocationTrust] scores must meet distance requirements, or be degraded. +/// - CBRS Radio's location is always trusted because of GPS. +/// +/// - Speedtests +/// - The latest 6 speedtests will be used. +/// - There must be more than 2 speedtests. +/// +/// - Covered Hexes +/// - If a Radio is not [eligible_for_boosted_hexes], boost values are removed before calculations. [CoveredHexes::new_without_boosts] +/// /// ## References: /// [modeled-coverage]: https://github.com/helium/HIP/blob/main/0074-mobile-poc-modeled-coverage-rewards.md#outdoor-radios /// [provider-boosting]: https://github.com/helium/HIP/blob/main/0084-service-provider-hex-boosting.md @@ -41,13 +53,6 @@ /// [mobile-poc-blog]: https://docs.helium.com/mobile/proof-of-coverage /// [boosted-hex-restriction]: https://github.com/helium/oracles/pull/808 /// -/// To Integrate in Docs: -/// -/// -/// Has something to say about 30meters from asserted location wrt poc rewards -/// for boosted hexes. -/// https://github.com/helium/HIP/blob/8b1e814afa61a714b5ba63d3265e5897ab4c5116/0107-preventing-gaming-within-the-mobile-network.md -/// use crate::{ hexes::{CoveredHex, CoveredHexes}, location::{LocationTrust, LocationTrustScores}, From fb1da40665ea5bd1e7756934bcf9ae4ac50559b6 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 5 Jun 2024 14:53:21 -0700 Subject: [PATCH 061/115] represent boosted hex eligiblity with an enum There is more than 1 case that can preclude a radio from eligibility, let's communicate that in a better way that a boolean. --- coverage_point_calculator/src/lib.rs | 54 +++++++++++++++++----------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 16ad6433d..d872103c8 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -78,7 +78,7 @@ pub struct RewardableRadio { location_trust_scores: LocationTrustScores, radio_threshold: RadioThreshold, covered_hexes: CoveredHexes, - eligible_for_boosted_hexes: bool, + boosted_hex_eligibility: BoostedHexStatus, } #[derive(Debug)] @@ -86,7 +86,6 @@ pub struct CoveragePoints { /// Value used when calculating poc_reward pub total_coverage_points: Decimal, /// Coverage Points collected from each Covered Hex - /// vvv turn into function call pub hex_coverage_points: Decimal, /// Location Trust Multiplier, maximum of 1 pub location_trust_multiplier: Decimal, @@ -98,7 +97,7 @@ pub struct CoveragePoints { pub speedtests: Vec, pub location_trust_scores: Vec, pub covered_hexes: Vec, - pub eligible_for_boosted_hexes: bool, + pub boosted_hex_eligibility: BoostedHexStatus, } #[derive(thiserror::Error, Debug)] @@ -126,13 +125,13 @@ pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { speedtests: radio.speedtests.speedtests, location_trust_scores: radio.location_trust_scores.trust_scores, covered_hexes: radio.covered_hexes.hexes, - eligible_for_boosted_hexes: radio.eligible_for_boosted_hexes, + boosted_hex_eligibility: radio.boosted_hex_eligibility, } } impl CoveragePoints { pub fn iter_boosted_hexes(&self) -> impl Iterator { - let eligible = self.eligible_for_boosted_hexes; + let eligible = self.boosted_hex_eligibility.is_eligible(); self.covered_hexes .clone() @@ -161,13 +160,13 @@ impl RewardableRadio { LocationTrustScores::new(&radio_type, location_trust_scores) }; - let eligible_for_boosted_hexes = eligible_for_boosted_hexes( + let boosted_hex_status = BoostedHexStatus::new( &radio_type, location_trust_scores.multiplier, &radio_threshold, ); - let covered_hexes = if eligible_for_boosted_hexes { + let covered_hexes = if boosted_hex_status.is_eligible() { CoveredHexes::new(&radio_type, covered_hexes)? } else { CoveredHexes::new_without_boosts(&radio_type, covered_hexes)? @@ -179,7 +178,7 @@ impl RewardableRadio { location_trust_scores, radio_threshold, covered_hexes, - eligible_for_boosted_hexes, + boosted_hex_eligibility: boosted_hex_status, }) } @@ -188,22 +187,35 @@ impl RewardableRadio { } } -fn eligible_for_boosted_hexes( - radio_type: &RadioType, - location_trust_score: Decimal, - radio_threshold: &RadioThreshold, -) -> bool { - // hip93: if radio is wifi & location_trust score multiplier < 0.75, no boosting - if radio_type.is_wifi() && location_trust_score < dec!(0.75) { - return false; - } +#[derive(Debug, Clone)] +pub enum BoostedHexStatus { + Eligible, + WifiLocationScoreBelowThreshold(Decimal), + RadioThresholdNotMet, +} + +impl BoostedHexStatus { + fn new( + radio_type: &RadioType, + location_trust_score: Decimal, + radio_threshold: &RadioThreshold, + ) -> Self { + // hip93: if radio is wifi & location_trust score multiplier < 0.75, no boosting + if radio_type.is_wifi() && location_trust_score < dec!(0.75) { + return Self::WifiLocationScoreBelowThreshold(location_trust_score); + } + + // hip84: if radio has not met minimum data and subscriber thresholds, no boosting + if !radio_threshold.threshold_met() { + return Self::RadioThresholdNotMet; + } - // hip84: if radio has not met minimum data and subscriber thresholds, no boosting - if !radio_threshold.threshold_met() { - return false; + Self::Eligible } - true + fn is_eligible(&self) -> bool { + matches!(self, Self::Eligible) + } } #[derive(Debug, Clone, Copy, PartialEq)] From ac3a77f35465c738680e773a09af529b5e4fac46 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 5 Jun 2024 15:13:37 -0700 Subject: [PATCH 062/115] intersperse speedtests to make success clearer --- coverage_point_calculator/src/speedtest.rs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/coverage_point_calculator/src/speedtest.rs b/coverage_point_calculator/src/speedtest.rs index ae8fa05cc..9069efb52 100644 --- a/coverage_point_calculator/src/speedtest.rs +++ b/coverage_point_calculator/src/speedtest.rs @@ -209,22 +209,30 @@ mod tests { timestamp, }; + // Intersperse new and old speedtests. + // new speedtests have 1.0 multipliers + // old speedtests have 0.0 multipliers let speedtests = Speedtests::new(vec![ make_speedtest(date(2024, 4, 6), Millis::new(15)), - make_speedtest(date(2024, 4, 5), Millis::new(15)), - make_speedtest(date(2024, 4, 4), Millis::new(15)), - make_speedtest(date(2024, 4, 3), Millis::new(15)), - make_speedtest(date(2024, 4, 2), Millis::new(15)), - make_speedtest(date(2024, 4, 1), Millis::new(15)), - // make_speedtest(date(2022, 4, 6), Millis::new(999)), + // -- + make_speedtest(date(2024, 4, 5), Millis::new(15)), make_speedtest(date(2022, 4, 5), Millis::new(999)), + // -- + make_speedtest(date(2024, 4, 4), Millis::new(15)), make_speedtest(date(2022, 4, 4), Millis::new(999)), + // -- make_speedtest(date(2022, 4, 3), Millis::new(999)), + make_speedtest(date(2024, 4, 3), Millis::new(15)), + // -- + make_speedtest(date(2024, 4, 2), Millis::new(15)), make_speedtest(date(2022, 4, 2), Millis::new(999)), + // -- + make_speedtest(date(2024, 4, 1), Millis::new(15)), make_speedtest(date(2022, 4, 1), Millis::new(999)), ]); - println!("{speedtests:?}"); + + // Old speedtests should be unused assert_eq!(dec!(1), speedtests.multiplier); } From 19956c043cf0edf613f771e2fc9a56d8c620c148 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 5 Jun 2024 15:14:18 -0700 Subject: [PATCH 063/115] I think the shorter name is just as good --- coverage_point_calculator/src/hexes.rs | 2 +- coverage_point_calculator/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/coverage_point_calculator/src/hexes.rs b/coverage_point_calculator/src/hexes.rs index 14c9b01b8..a4705a231 100644 --- a/coverage_point_calculator/src/hexes.rs +++ b/coverage_point_calculator/src/hexes.rs @@ -81,7 +81,7 @@ impl CoveredHexes { }) } - pub fn accumulate_calculated_coverage_points(&self) -> Decimal { + pub fn calculated_coverage_points(&self) -> Decimal { self.hexes .iter() .map(|hex| hex.calculated_coverage_points) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index d872103c8..d5c7ce888 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -107,7 +107,7 @@ pub enum Error { } pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { - let hex_coverage_points = radio.covered_hexes.accumulate_calculated_coverage_points(); + let hex_coverage_points = radio.covered_hexes.calculated_coverage_points(); let location_trust_multiplier = radio.location_trust_scores.multiplier; let speedtest_multiplier = radio.speedtests.multiplier; From 847a1fa438a081b82fc22c7b4a815107f00f7068 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 5 Jun 2024 15:14:43 -0700 Subject: [PATCH 064/115] coverage points impl is not important enough to be that high in the file --- coverage_point_calculator/src/lib.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index d5c7ce888..7a4caed5a 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -129,18 +129,6 @@ pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { } } -impl CoveragePoints { - pub fn iter_boosted_hexes(&self) -> impl Iterator { - let eligible = self.boosted_hex_eligibility.is_eligible(); - - self.covered_hexes - .clone() - .into_iter() - .filter(move |_| eligible) - .filter(|hex| hex.boosted_multiplier.is_some()) - } -} - impl RewardableRadio { pub fn new( radio_type: RadioType, @@ -187,6 +175,18 @@ impl RewardableRadio { } } +impl CoveragePoints { + pub fn iter_boosted_hexes(&self) -> impl Iterator { + let eligible = self.boosted_hex_eligibility.is_eligible(); + + self.covered_hexes + .clone() + .into_iter() + .filter(move |_| eligible) + .filter(|hex| hex.boosted_multiplier.is_some()) + } +} + #[derive(Debug, Clone)] pub enum BoostedHexStatus { Eligible, From 4541393c3fc3ad2a1358c8184ce0a4c3916106be Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 5 Jun 2024 15:15:02 -0700 Subject: [PATCH 065/115] flesh out docs for RewardableRadio and CoveragePoints --- coverage_point_calculator/src/lib.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 7a4caed5a..2d7d52179 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -70,7 +70,7 @@ pub mod speedtest; pub type Result = std::result::Result; pub type MaxOneMultplier = Decimal; -/// Input Radio to calculation +/// Necessary checks for calculating coverage points is done during [RewardableRadio::new]. #[derive(Debug, Clone)] pub struct RewardableRadio { radio_type: RadioType, @@ -81,6 +81,19 @@ pub struct RewardableRadio { boosted_hex_eligibility: BoostedHexStatus, } +/// Output of calculating coverage points for a [RewardableRadio]. +/// +/// The only data included was used for calculating coverage points. +/// +/// - If more than the allowed speedtests were provided, only the speedtests +/// considered are included here. +/// +/// - When a radio covers boosted hexes, [location_trust_scores] will contain a +/// trust score _after_ the boosted hex restriction has been applied. +/// +/// - When a radio is not eligible for boosted hex rewards, [covered_hexes] will +/// have no boosted_multiplier values. +/// #[derive(Debug)] pub struct CoveragePoints { /// Value used when calculating poc_reward From 8a8b3cae43542ec673baaccefa48c54cb284a025 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 5 Jun 2024 15:28:42 -0700 Subject: [PATCH 066/115] assert against expected points in integration allows us to remove radio_type getter from RewardableRadio --- coverage_point_calculator/src/lib.rs | 4 - .../tests/coverage_point_calculator.rs | 103 ++++++------------ 2 files changed, 32 insertions(+), 75 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 2d7d52179..87711cbc2 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -182,10 +182,6 @@ impl RewardableRadio { boosted_hex_eligibility: boosted_hex_status, }) } - - pub fn radio_type(&self) -> RadioType { - self.radio_type - } } impl CoveragePoints { diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index c6f36a4b7..9ca8e98e5 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, num::NonZeroU32, str::FromStr}; +use std::{num::NonZeroU32, str::FromStr}; use chrono::Utc; use coverage_map::{RankedCoverage, SignalLevel}; @@ -51,12 +51,11 @@ fn base_radio_coverage_points() { boosted: NonZeroU32::new(0), }]; - let mut radios = vec![]; - for radio_type in [ - RadioType::IndoorWifi, - RadioType::IndoorCbrs, - RadioType::OutdoorWifi, - RadioType::OutdoorCbrs, + for (radio_type, expcted_base_coverage_point) in [ + (RadioType::IndoorWifi, dec!(400)), + (RadioType::IndoorCbrs, dec!(100)), + (RadioType::OutdoorWifi, dec!(16)), + (RadioType::OutdoorCbrs, dec!(4)), ] { let radio = RewardableRadio::new( radio_type, @@ -66,33 +65,25 @@ fn base_radio_coverage_points() { hexes.clone(), ) .unwrap(); - radios.push(radio.clone()); - println!( - "{radio_type:?} \t--> {}", - calculate_coverage_points(radio).total_coverage_points + + let coverage_points = calculate_coverage_points(radio); + assert_eq!( + expcted_base_coverage_point, + coverage_points.total_coverage_points ); } - - let output = radios - .into_iter() - .map(|r| { - ( - r.radio_type(), - calculate_coverage_points(r).total_coverage_points, - ) - }) - .collect::>(); - println!("{output:#?}"); } #[test] -fn radio_unique_coverage() { +fn radios_with_coverage() { + // Enough hexes will be provided to each type of radio, that they are + // awarded 400 coverage points. + let pubkey = helium_crypto::PublicKeyBinary::from_str( "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6", ) .unwrap(); - // all radios will receive 400 coverage points let base_hex = RankedCoverage { hotspot_key: pubkey, cbsd_id: None, @@ -106,26 +97,7 @@ fn radio_unique_coverage() { }, boosted: NonZeroU32::new(0), }; - let hex = std::iter::repeat(base_hex); - - let mut map = HashMap::new(); - map.insert("indoor_wifi", hex.clone().take(1).collect()); - map.insert("indoor_cbrs", hex.clone().take(4).collect()); - map.insert("outdoor_wifi", hex.clone().take(25).collect()); - map.insert("outdoor_cbrs", hex.clone().take(100).collect()); - - fn hexes( - coverage_map: &HashMap<&str, Vec>, - key: &RadioType, - ) -> Vec { - let key = match key { - RadioType::IndoorWifi => "indoor_wifi", - RadioType::OutdoorWifi => "outdoor_wifi", - RadioType::IndoorCbrs => "indoor_cbrs", - RadioType::OutdoorCbrs => "outdoor_cbrs", - }; - coverage_map.get(key).unwrap().clone() - } + let base_hex_iter = std::iter::repeat(base_hex); let default_speedtests = vec![ Speedtest { @@ -146,33 +118,22 @@ fn radio_unique_coverage() { trust_score: dec!(1.0), }]; - let mut radios = vec![]; - for radio_type in [ - RadioType::IndoorWifi, - RadioType::IndoorCbrs, - RadioType::OutdoorWifi, - RadioType::OutdoorCbrs, + for (radio_type, num_hexes) in [ + (RadioType::IndoorWifi, 1), + (RadioType::IndoorCbrs, 4), + (RadioType::OutdoorWifi, 25), + (RadioType::OutdoorCbrs, 100), ] { - radios.push( - RewardableRadio::new( - radio_type, - default_speedtests.clone(), - default_location_trust_scores.clone(), - RadioThreshold::Verified, - hexes(&map, &radio_type), - ) - .unwrap(), - ); - } + let radio = RewardableRadio::new( + radio_type, + default_speedtests.clone(), + default_location_trust_scores.clone(), + RadioThreshold::Verified, + base_hex_iter.clone().take(num_hexes).collect(), + ) + .unwrap(); - let coverage_points = radios - .into_iter() - .map(|r| { - ( - r.radio_type(), - calculate_coverage_points(r).total_coverage_points, - ) - }) - .collect::>(); - println!("{coverage_points:#?}") + let coverage_points = calculate_coverage_points(radio); + assert_eq!(dec!(400), coverage_points.total_coverage_points); + } } From 36d93b5d6e3b5f3e9233670343c9f9b61f3b137b Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 5 Jun 2024 15:50:13 -0700 Subject: [PATCH 067/115] remove type alias --- coverage_point_calculator/src/lib.rs | 1 - coverage_point_calculator/src/speedtest.rs | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 87711cbc2..cc2c96931 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -68,7 +68,6 @@ pub mod location; pub mod speedtest; pub type Result = std::result::Result; -pub type MaxOneMultplier = Decimal; /// Necessary checks for calculating coverage points is done during [RewardableRadio::new]. #[derive(Debug, Clone)] diff --git a/coverage_point_calculator/src/speedtest.rs b/coverage_point_calculator/src/speedtest.rs index 9069efb52..55372228a 100644 --- a/coverage_point_calculator/src/speedtest.rs +++ b/coverage_point_calculator/src/speedtest.rs @@ -2,8 +2,6 @@ use chrono::{DateTime, Utc}; use rust_decimal::Decimal; use rust_decimal_macros::dec; -use crate::MaxOneMultplier; - const MIN_REQUIRED_SPEEDTEST_SAMPLES: usize = 2; const MAX_ALLOWED_SPEEDTEST_SAMPLES: usize = 6; @@ -116,7 +114,7 @@ pub enum SpeedtestTier { } impl SpeedtestTier { - pub fn multiplier(&self) -> MaxOneMultplier { + pub fn multiplier(&self) -> Decimal { match self { SpeedtestTier::Good => dec!(1.00), SpeedtestTier::Acceptable => dec!(0.75), From 1aadb874afa4ad97ae0b2ed255fdaf65d3467ffb Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 5 Jun 2024 16:24:20 -0700 Subject: [PATCH 068/115] move helpers behind tests add comments to assertions --- coverage_point_calculator/src/lib.rs | 274 +++++++++++++-------------- 1 file changed, 131 insertions(+), 143 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index cc2c96931..c98749e8c 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -308,26 +308,14 @@ mod tests { use hex_assignments::{assignment::HexAssignments, Assignment}; use rust_decimal_macros::dec; - fn hex_location() -> hextree::Cell { - hextree::Cell::from_raw(0x8c2681a3064edff).unwrap() - } - - fn assignments_maximum() -> HexAssignments { - HexAssignments { - footfall: Assignment::A, - landtype: Assignment::A, - urbanized: Assignment::A, - } - } - #[test] fn hip_84_radio_meets_minimum_subscriber_threshold_for_boosted_hexes() { - let make_wifi = |location_trust_scores: Vec| { + let make_wifi = |radio_verified: RadioThreshold| { RewardableRadio::new( RadioType::IndoorWifi, - Speedtest::maximum(), - location_trust_scores, - RadioThreshold::Verified, + speedtest_maximum(), + location_trust_maximum(), + radio_verified, vec![RankedCoverage { hotspot_key: pubkey(), cbsd_id: None, @@ -344,19 +332,22 @@ mod tests { let base_points = RadioType::IndoorWifi .base_coverage_points(&SignalLevel::High) .unwrap(); - // Boosted Hex get's radio over the base_points - let trusted_location = LocationTrust::with_trust_scores(&[dec!(1), dec!(1)]); - let trusted_wifi = make_wifi(trusted_location); - assert!(trusted_wifi.location_trust_scores.multiplier > dec!(0.75)); - assert!( - calculate_coverage_points(trusted_wifi.clone()).total_coverage_points > base_points + + // Radio meeting the threshold is eligible for boosted hexes. + // Boosted hex provides radio with more than base_points. + let verified_wifi = make_wifi(RadioThreshold::Verified); + assert_eq!( + base_points * dec!(5), + calculate_coverage_points(verified_wifi.clone()).total_coverage_points ); - // degraded location score get's radio under base_points - let untrusted_location = LocationTrust::with_trust_scores(&[dec!(0.1), dec!(0.2)]); - let untrusted_wifi = make_wifi(untrusted_location); - assert!(untrusted_wifi.location_trust_scores.multiplier < dec!(0.75)); - assert!(calculate_coverage_points(untrusted_wifi).total_coverage_points < base_points); + // Radio not meeting the threshold is not eligible for boosted hexes. + // Boost from hex is not applied, radio receives base points. + let unverified_wifi = make_wifi(RadioThreshold::UnVerified); + assert_eq!( + base_points, + calculate_coverage_points(unverified_wifi).total_coverage_points + ); } #[test] @@ -364,7 +355,7 @@ mod tests { let make_wifi = |location_trust_scores: Vec| { RewardableRadio::new( RadioType::IndoorWifi, - Speedtest::maximum(), + speedtest_maximum(), location_trust_scores, RadioThreshold::Verified, vec![RankedCoverage { @@ -383,17 +374,18 @@ mod tests { let base_points = RadioType::IndoorWifi .base_coverage_points(&SignalLevel::High) .unwrap(); - // Boosted Hex get's radio over the base_points - let trusted_location = LocationTrust::with_trust_scores(&[dec!(1), dec!(1)]); - let trusted_wifi = make_wifi(trusted_location); + + // Radio with good trust score is eligible for boosted hexes. + // Boosted hex provides radio with more than base_points. + let trusted_wifi = make_wifi(location_trust_with_scores(&[dec!(1), dec!(1)])); assert!(trusted_wifi.location_trust_scores.multiplier > dec!(0.75)); assert!( calculate_coverage_points(trusted_wifi.clone()).total_coverage_points > base_points ); - // degraded location score get's radio under base_points - let untrusted_location = LocationTrust::with_trust_scores(&[dec!(0.1), dec!(0.2)]); - let untrusted_wifi = make_wifi(untrusted_location); + // Radio with poor trust score is not eligible for boosted hexes. + // Boost from hex is not applied, and points are further lowered by poor trust score. + let untrusted_wifi = make_wifi(location_trust_with_scores(&[dec!(0.1), dec!(0.2)])); assert!(untrusted_wifi.location_trust_scores.multiplier < dec!(0.75)); assert!(calculate_coverage_points(untrusted_wifi).total_coverage_points < base_points); } @@ -404,7 +396,7 @@ mod tests { RewardableRadio::new( RadioType::IndoorCbrs, speedtests, - LocationTrust::maximum(), + location_trust_maximum(), RadioThreshold::Verified, vec![RankedCoverage { hotspot_key: pubkey(), @@ -419,15 +411,15 @@ mod tests { .expect("indoor cbrs with speedtests") }; - let indoor_cbrs = make_indoor_cbrs(Speedtest::maximum()); + let indoor_cbrs = make_indoor_cbrs(speedtest_maximum()); assert_eq!( dec!(100), calculate_coverage_points(indoor_cbrs).total_coverage_points ); let indoor_cbrs = make_indoor_cbrs(vec![ - Speedtest::download(BytesPs::mbps(88)), - Speedtest::download(BytesPs::mbps(88)), + speedtest_with_download(BytesPs::mbps(88)), + speedtest_with_download(BytesPs::mbps(88)), ]); assert_eq!( dec!(75), @@ -435,8 +427,8 @@ mod tests { ); let indoor_cbrs = make_indoor_cbrs(vec![ - Speedtest::download(BytesPs::mbps(62)), - Speedtest::download(BytesPs::mbps(62)), + speedtest_with_download(BytesPs::mbps(62)), + speedtest_with_download(BytesPs::mbps(62)), ]); assert_eq!( dec!(50), @@ -444,8 +436,8 @@ mod tests { ); let indoor_cbrs = make_indoor_cbrs(vec![ - Speedtest::download(BytesPs::mbps(42)), - Speedtest::download(BytesPs::mbps(42)), + speedtest_with_download(BytesPs::mbps(42)), + speedtest_with_download(BytesPs::mbps(42)), ]); assert_eq!( dec!(25), @@ -453,8 +445,8 @@ mod tests { ); let indoor_cbrs = make_indoor_cbrs(vec![ - Speedtest::download(BytesPs::mbps(25)), - Speedtest::download(BytesPs::mbps(25)), + speedtest_with_download(BytesPs::mbps(25)), + speedtest_with_download(BytesPs::mbps(25)), ]); assert_eq!( dec!(0), @@ -464,7 +456,7 @@ mod tests { #[test] fn oracle_boosting_assignments_apply_per_hex() { - fn local_hex( + fn ranked_coverage( footfall: Assignment, landtype: Assignment, urbanized: Assignment, @@ -487,44 +479,44 @@ mod tests { use Assignment::*; let indoor_cbrs = RewardableRadio::new( RadioType::IndoorCbrs, - Speedtest::maximum(), - LocationTrust::maximum(), + speedtest_maximum(), + location_trust_maximum(), RadioThreshold::Verified, vec![ // yellow - POI ≥ 1 Urbanized - local_hex(A, A, A), // 100 - local_hex(A, B, A), // 100 - local_hex(A, C, A), // 100 + ranked_coverage(A, A, A), // 100 + ranked_coverage(A, B, A), // 100 + ranked_coverage(A, C, A), // 100 // orange - POI ≥ 1 Not Urbanized - local_hex(A, A, B), // 100 - local_hex(A, B, B), // 100 - local_hex(A, C, B), // 100 + ranked_coverage(A, A, B), // 100 + ranked_coverage(A, B, B), // 100 + ranked_coverage(A, C, B), // 100 // light green - Point of Interest Urbanized - local_hex(B, A, A), // 70 - local_hex(B, B, A), // 70 - local_hex(B, C, A), // 70 + ranked_coverage(B, A, A), // 70 + ranked_coverage(B, B, A), // 70 + ranked_coverage(B, C, A), // 70 // dark green - Point of Interest Not Urbanized - local_hex(B, A, B), // 50 - local_hex(B, B, B), // 50 - local_hex(B, C, B), // 50 + ranked_coverage(B, A, B), // 50 + ranked_coverage(B, B, B), // 50 + ranked_coverage(B, C, B), // 50 // light blue - No POI Urbanized - local_hex(C, A, A), // 40 - local_hex(C, B, A), // 30 - local_hex(C, C, A), // 5 + ranked_coverage(C, A, A), // 40 + ranked_coverage(C, B, A), // 30 + ranked_coverage(C, C, A), // 5 // dark blue - No POI Not Urbanized - local_hex(C, A, B), // 20 - local_hex(C, B, B), // 15 - local_hex(C, C, B), // 3 + ranked_coverage(C, A, B), // 20 + ranked_coverage(C, B, B), // 15 + ranked_coverage(C, C, B), // 3 // gray - Outside of USA - local_hex(A, A, C), // 0 - local_hex(A, B, C), // 0 - local_hex(A, C, C), // 0 - local_hex(B, A, C), // 0 - local_hex(B, B, C), // 0 - local_hex(B, C, C), // 0 - local_hex(C, A, C), // 0 - local_hex(C, B, C), // 0 - local_hex(C, C, C), // 0 + ranked_coverage(A, A, C), // 0 + ranked_coverage(A, B, C), // 0 + ranked_coverage(A, C, C), // 0 + ranked_coverage(B, A, C), // 0 + ranked_coverage(B, B, C), // 0 + ranked_coverage(B, C, C), // 0 + ranked_coverage(C, A, C), // 0 + ranked_coverage(C, B, C), // 0 + ranked_coverage(C, C, C), // 0 ], ) .expect("indoor cbrs"); @@ -539,8 +531,8 @@ mod tests { fn outdoor_radios_consider_top_3_ranked_hexes() { let outdoor_wifi = RewardableRadio::new( RadioType::OutdoorWifi, - Speedtest::maximum(), - LocationTrust::maximum(), + speedtest_maximum(), + location_trust_maximum(), RadioThreshold::Verified, vec![ RankedCoverage { @@ -597,8 +589,8 @@ mod tests { fn indoor_radios_only_consider_first_ranked_hexes() { let indoor_wifi = RewardableRadio::new( RadioType::IndoorWifi, - Speedtest::maximum(), - LocationTrust::maximum(), + speedtest_maximum(), + location_trust_maximum(), RadioThreshold::Verified, vec![ RankedCoverage { @@ -643,8 +635,8 @@ mod tests { // Location scores are averaged together let indoor_wifi = RewardableRadio::new( RadioType::IndoorWifi, - Speedtest::maximum(), - LocationTrust::with_trust_scores(&[dec!(0.1), dec!(0.2), dec!(0.3), dec!(0.4)]), + speedtest_maximum(), + location_trust_with_scores(&[dec!(0.1), dec!(0.2), dec!(0.3), dec!(0.4)]), RadioThreshold::Verified, vec![RankedCoverage { hotspot_key: pubkey(), @@ -659,6 +651,7 @@ mod tests { .expect("indoor wifi"); // Location trust scores is 1/4 + // (0.1 + 0.2 + 0.3 + 0.4) / 4 assert_eq!( dec!(100), calculate_coverage_points(indoor_wifi).total_coverage_points @@ -689,40 +682,27 @@ mod tests { ]; let indoor_wifi = RewardableRadio::new( RadioType::IndoorWifi, - Speedtest::maximum(), - LocationTrust::maximum(), + speedtest_maximum(), + location_trust_maximum(), RadioThreshold::Verified, covered_hexes.clone(), ) - .expect("verified indoor wifi"); + .expect("indoor wifi"); + // The hex with a low signal_level is boosted to the same level as a // signal_level of High. assert_eq!( dec!(800), calculate_coverage_points(indoor_wifi.clone()).total_coverage_points ); - - // When the radio is not verified for boosted rewards, the boost has no effect. - let indoor_wifi = RewardableRadio::new( - RadioType::IndoorWifi, - Speedtest::maximum(), - LocationTrust::maximum(), - RadioThreshold::UnVerified, - covered_hexes, - ) - .expect("unverified indoor wifi"); - assert_eq!( - dec!(500), - calculate_coverage_points(indoor_wifi).total_coverage_points - ); } #[test] fn base_radio_coverage_points() { let outdoor_cbrs = RewardableRadio::new( RadioType::OutdoorCbrs, - Speedtest::maximum(), - LocationTrust::maximum(), + speedtest_maximum(), + location_trust_maximum(), RadioThreshold::Verified, vec![ RankedCoverage { @@ -767,8 +747,8 @@ mod tests { let indoor_cbrs = RewardableRadio::new( RadioType::IndoorCbrs, - Speedtest::maximum(), - LocationTrust::maximum(), + speedtest_maximum(), + location_trust_maximum(), RadioThreshold::Verified, vec![ RankedCoverage { @@ -795,8 +775,8 @@ mod tests { let outdoor_wifi = RewardableRadio::new( RadioType::OutdoorWifi, - Speedtest::maximum(), - LocationTrust::maximum(), + speedtest_maximum(), + location_trust_maximum(), RadioThreshold::Verified, vec![ RankedCoverage { @@ -841,8 +821,8 @@ mod tests { let indoor_wifi = RewardableRadio::new( RadioType::IndoorWifi, - Speedtest::maximum(), - LocationTrust::maximum(), + speedtest_maximum(), + location_trust_maximum(), RadioThreshold::Verified, vec![ RankedCoverage { @@ -887,53 +867,61 @@ mod tests { ); } - impl Speedtest { - fn maximum() -> Vec { - vec![ - Self { - upload_speed: BytesPs::mbps(15), - download_speed: BytesPs::mbps(150), - latency: Millis::new(15), - timestamp: Utc::now(), - }, - Self { - upload_speed: BytesPs::mbps(15), - download_speed: BytesPs::mbps(150), - latency: Millis::new(15), - timestamp: Utc::now(), - }, - ] + fn hex_location() -> hextree::Cell { + hextree::Cell::from_raw(0x8c2681a3064edff).unwrap() + } + + fn assignments_maximum() -> HexAssignments { + HexAssignments { + footfall: Assignment::A, + landtype: Assignment::A, + urbanized: Assignment::A, } + } - fn download(download: BytesPs) -> Self { - Self { + fn speedtest_maximum() -> Vec { + vec![ + Speedtest { upload_speed: BytesPs::mbps(15), - download_speed: download, + download_speed: BytesPs::mbps(150), latency: Millis::new(15), timestamp: Utc::now(), - } - } + }, + Speedtest { + upload_speed: BytesPs::mbps(15), + download_speed: BytesPs::mbps(150), + latency: Millis::new(15), + timestamp: Utc::now(), + }, + ] } - impl LocationTrust { - fn maximum() -> Vec { - vec![LocationTrust { - distance_to_asserted: Meters::new(1), - trust_score: dec!(1.0), - }] + fn speedtest_with_download(download: BytesPs) -> Speedtest { + Speedtest { + upload_speed: BytesPs::mbps(15), + download_speed: download, + latency: Millis::new(15), + timestamp: Utc::now(), } + } - fn with_trust_scores(trust_scores: &[Decimal]) -> Vec { - trust_scores - .to_owned() - .iter() - .copied() - .map(|trust_score| LocationTrust { - distance_to_asserted: Meters::new(1), - trust_score, - }) - .collect() - } + fn location_trust_maximum() -> Vec { + vec![LocationTrust { + distance_to_asserted: Meters::new(1), + trust_score: dec!(1.0), + }] + } + + fn location_trust_with_scores(trust_scores: &[Decimal]) -> Vec { + trust_scores + .to_owned() + .iter() + .copied() + .map(|trust_score| LocationTrust { + distance_to_asserted: Meters::new(1), + trust_score, + }) + .collect() } fn pubkey() -> helium_crypto::PublicKeyBinary { From ff38db3eded7682047c52f52205147688efdd7f4 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 15:21:28 -0700 Subject: [PATCH 069/115] Use inner line comment to describe crate inner line comments "//!" describe what is above. outer line comments "///" describe what is below. Thank you Matty. --- coverage_point_calculator/src/lib.rs | 110 +++++++++++++-------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index c98749e8c..29ea3f324 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -1,58 +1,58 @@ -/// -/// Many changes to the rewards algorithm are contained in and across many HIPs. -/// The blog post [MOBILE Proof of Coverage][mobile-poc-blog] contains a more -/// thorough explanation of many of them. It is not exhaustive, but a great -/// place to start. -/// -/// ## Fields: -/// - modeled_coverage_points -/// - [HIP-74][modeled-coverage] -/// - reduced cbrs radio coverage points [HIP-113][cbrs-experimental] -/// -/// - assignment_multiplier -/// - [HIP-103][oracle-boosting] -/// -/// - rank -/// - [HIP-105][hex-limits] -/// -/// - hex_boost_multiplier -/// - must meet minimum subscriber thresholds [HIP-84][provider-boosting] -/// - Wifi Location trust score >0.75 for boosted hex eligibility [HIP-93][wifi-aps] -/// -/// - location_trust_score_multiplier -/// - [HIP-98][qos-score] -/// - states 30m requirement for boosted hexes [HIP-107][prevent-gaming] -/// - increase Boosted hex restriction, 30m -> 50m [Pull Request][boosted-hex-restriction] -/// -/// - speedtest_multiplier -/// - [HIP-74][modeled-coverage] -/// - added "Good" speedtest tier [HIP-98][qos-score] -/// - latency is explicitly under limit in HIP https://github.com/helium/oracles/pull/737 -/// -/// ## Notable Conditions: -/// - Location -/// - If a Radio covers any boosted hexes, [LocationTrust] scores must meet distance requirements, or be degraded. -/// - CBRS Radio's location is always trusted because of GPS. -/// -/// - Speedtests -/// - The latest 6 speedtests will be used. -/// - There must be more than 2 speedtests. -/// -/// - Covered Hexes -/// - If a Radio is not [eligible_for_boosted_hexes], boost values are removed before calculations. [CoveredHexes::new_without_boosts] -/// -/// ## References: -/// [modeled-coverage]: https://github.com/helium/HIP/blob/main/0074-mobile-poc-modeled-coverage-rewards.md#outdoor-radios -/// [provider-boosting]: https://github.com/helium/HIP/blob/main/0084-service-provider-hex-boosting.md -/// [wifi-aps]: https://github.com/helium/HIP/blob/main/0093-addition-of-wifi-aps-to-mobile-subdao.md -/// [qos-score]: https://github.com/helium/HIP/blob/main/0098-mobile-subdao-quality-of-service-requirements.md -/// [oracle-boosting]: https://github.com/helium/HIP/blob/main/0103-oracle-hex-boosting.md -/// [hex-limits]: https://github.com/helium/HIP/blob/main/0105-modification-of-mobile-subdao-hex-limits.md -/// [prevent-gaming]: https://github.com/helium/HIP/blob/main/0107-preventing-gaming-within-the-mobile-network.md -/// [cbrs-experimental]: https://github.com/helium/HIP/blob/main/0113-reward-cbrs-as-experimental.md -/// [mobile-poc-blog]: https://docs.helium.com/mobile/proof-of-coverage -/// [boosted-hex-restriction]: https://github.com/helium/oracles/pull/808 -/// +//! +//! Many changes to the rewards algorithm are contained in and across many HIPs. +//! The blog post [MOBILE Proof of Coverage][mobile-poc-blog] contains a more +//! thorough explanation of many of them. It is not exhaustive, but a great +//! place to start. +//! +//! ## Fields: +//! - modeled_coverage_points +//! - [HIP-74][modeled-coverage] +//! - reduced cbrs radio coverage points [HIP-113][cbrs-experimental] +//! +//! - assignment_multiplier +//! - [HIP-103][oracle-boosting] +//! +//! - rank +//! - [HIP-105][hex-limits] +//! +//! - hex_boost_multiplier +//! - must meet minimum subscriber thresholds [HIP-84][provider-boosting] +//! - Wifi Location trust score >0.75 for boosted hex eligibility [HIP-93][wifi-aps] +//! +//! - location_trust_score_multiplier +//! - [HIP-98][qos-score] +//! - states 30m requirement for boosted hexes [HIP-107][prevent-gaming] +//! - increase Boosted hex restriction, 30m -> 50m [Pull Request][boosted-hex-restriction] +//! +//! - speedtest_multiplier +//! - [HIP-74][modeled-coverage] +//! - added "Good" speedtest tier [HIP-98][qos-score] +//! - latency is explicitly under limit in HIP +//! +//! ## Notable Conditions: +//! - Location +//! - If a Radio covers any boosted hexes, [LocationTrust] scores must meet distance requirements, or be degraded. +//! - CBRS Radio's location is always trusted because of GPS. +//! +//! - Speedtests +//! - The latest 6 speedtests will be used. +//! - There must be more than 2 speedtests. +//! +//! - Covered Hexes +//! - If a Radio is not [CoveragePoints::boosted_hex_eligibility], boost values are removed before calculations. [CoveredHexes::new_without_boosts] +//! +//! ## References: +//! [modeled-coverage]: https://github.com/helium/HIP/blob/main/0074-mobile-poc-modeled-coverage-rewards.md#outdoor-radios +//! [provider-boosting]: https://github.com/helium/HIP/blob/main/0084-service-provider-hex-boosting.md +//! [wifi-aps]: https://github.com/helium/HIP/blob/main/0093-addition-of-wifi-aps-to-mobile-subdao.md +//! [qos-score]: https://github.com/helium/HIP/blob/main/0098-mobile-subdao-quality-of-service-requirements.md +//! [oracle-boosting]: https://github.com/helium/HIP/blob/main/0103-oracle-hex-boosting.md +//! [hex-limits]: https://github.com/helium/HIP/blob/main/0105-modification-of-mobile-subdao-hex-limits.md +//! [prevent-gaming]: https://github.com/helium/HIP/blob/main/0107-preventing-gaming-within-the-mobile-network.md +//! [cbrs-experimental]: https://github.com/helium/HIP/blob/main/0113-reward-cbrs-as-experimental.md +//! [mobile-poc-blog]: https://docs.helium.com/mobile/proof-of-coverage +//! [boosted-hex-restriction]: https://github.com/helium/oracles/pull/808 +//! use crate::{ hexes::{CoveredHex, CoveredHexes}, location::{LocationTrust, LocationTrustScores}, From cb6b3ef947ece190085ac809c0db2f6bad0cbcef Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 15:27:51 -0700 Subject: [PATCH 070/115] Clean up constructors We can not need to worry about constructor naming conventions by moving more decision making into the constructors themselves. `RewardableRadio::new` now only brings to the surface the fact that location trust scores need to know about coverage, and covered hexes need to know about location trust multiplier. But nothing more internally. --- coverage_point_calculator/src/hexes.rs | 31 ++++++++++----------- coverage_point_calculator/src/lib.rs | 18 ++++-------- coverage_point_calculator/src/location.rs | 34 +++++++++++++---------- 3 files changed, 39 insertions(+), 44 deletions(-) diff --git a/coverage_point_calculator/src/hexes.rs b/coverage_point_calculator/src/hexes.rs index a4705a231..8a23bf81a 100644 --- a/coverage_point_calculator/src/hexes.rs +++ b/coverage_point_calculator/src/hexes.rs @@ -3,7 +3,7 @@ use hex_assignments::assignment::HexAssignments; use rust_decimal::Decimal; use rust_decimal_macros::dec; -use crate::{RadioType, Result}; +use crate::{BoostedHexStatus, RadioType, Result}; #[derive(Debug, Clone)] pub struct CoveredHexes { @@ -27,23 +27,22 @@ pub struct CoveredHex { } impl CoveredHexes { - pub fn new_without_boosts( - radio_type: &RadioType, + pub fn new( + radio_type: RadioType, ranked_coverage: Vec, + boosted_hex_status: BoostedHexStatus, ) -> Result { - let ranked_coverage: Vec<_> = ranked_coverage - .into_iter() - .map(|ranked| RankedCoverage { - boosted: None, - ..ranked - }) - .collect(); - - Self::new(radio_type, ranked_coverage) - } - - pub fn new(radio_type: &RadioType, ranked_coverage: Vec) -> Result { - let rank_multipliers = radio_type.rank_multipliers(); + let ranked_coverage = if !boosted_hex_status.is_eligible() { + ranked_coverage + .into_iter() + .map(|ranked| RankedCoverage { + boosted: None, + ..ranked + }) + .collect() + } else { + ranked_coverage + }; // verify all hexes can obtain a base coverage point let covered_hexes = ranked_coverage diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 29ea3f324..7f97e1973 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -54,7 +54,7 @@ //! [boosted-hex-restriction]: https://github.com/helium/oracles/pull/808 //! use crate::{ - hexes::{CoveredHex, CoveredHexes}, + hexes::CoveredHexes, location::{LocationTrust, LocationTrustScores}, speedtest::Speedtest, }; @@ -147,18 +147,14 @@ impl RewardableRadio { speedtests: Vec, location_trust_scores: Vec, radio_threshold: RadioThreshold, - covered_hexes: Vec, + ranked_coverage: Vec, ) -> Result { // QUESTION: we need to know about boosted hexes to determine location multiplier. // The location multiplier is then used to determine if they are eligible for boosted hexes. // In the case where they cannot use boosted hexes, should the location mulitiplier be restored? - let any_boosted_hexes = covered_hexes.iter().any(|hex| hex.boosted.is_some()); - let location_trust_scores = if any_boosted_hexes { - LocationTrustScores::new_with_boosted_hexes(&radio_type, location_trust_scores) - } else { - LocationTrustScores::new(&radio_type, location_trust_scores) - }; + let location_trust_scores = + LocationTrustScores::new(radio_type, location_trust_scores, &ranked_coverage); let boosted_hex_status = BoostedHexStatus::new( &radio_type, @@ -166,11 +162,7 @@ impl RewardableRadio { &radio_threshold, ); - let covered_hexes = if boosted_hex_status.is_eligible() { - CoveredHexes::new(&radio_type, covered_hexes)? - } else { - CoveredHexes::new_without_boosts(&radio_type, covered_hexes)? - }; + let covered_hexes = CoveredHexes::new(radio_type, ranked_coverage, boosted_hex_status)?; Ok(Self { radio_type, diff --git a/coverage_point_calculator/src/location.rs b/coverage_point_calculator/src/location.rs index 3f2b0db31..4e9fb6780 100644 --- a/coverage_point_calculator/src/location.rs +++ b/coverage_point_calculator/src/location.rs @@ -1,3 +1,4 @@ +use coverage_map::RankedCoverage; use rust_decimal::Decimal; use rust_decimal_macros::dec; @@ -27,31 +28,34 @@ pub struct LocationTrust { } impl LocationTrustScores { - pub fn new(radio_type: &RadioType, trust_scores: Vec) -> Self { + pub fn new( + radio_type: RadioType, + trust_scores: Vec, + ranked_coverage: &[RankedCoverage], + ) -> Self { + let any_boosted_hexes = ranked_coverage.iter().any(|hex| hex.boosted.is_some()); + + let cleaned_scores = if any_boosted_hexes { + trust_scores + .into_iter() + .map(LocationTrust::into_boosted) + .collect() + } else { + trust_scores + }; + // CBRS radios are always trusted because they have internal GPS let multiplier = if radio_type.is_cbrs() { dec!(1) } else { - multiplier(&trust_scores) + multiplier(&cleaned_scores) }; Self { multiplier, - trust_scores, + trust_scores: cleaned_scores, } } - - pub fn new_with_boosted_hexes( - radio_type: &RadioType, - trust_scores: Vec, - ) -> Self { - let trust_scores: Vec<_> = trust_scores - .into_iter() - .map(LocationTrust::into_boosted) - .collect(); - - Self::new(radio_type, trust_scores) - } } impl LocationTrust { fn into_boosted(self) -> Self { From a9e4846586f303abfa27919e79f1039d17d9ec97 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 15:29:52 -0700 Subject: [PATCH 071/115] remove unused iter function until it is needed --- coverage_point_calculator/src/lib.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 7f97e1973..188e922ff 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -175,18 +175,6 @@ impl RewardableRadio { } } -impl CoveragePoints { - pub fn iter_boosted_hexes(&self) -> impl Iterator { - let eligible = self.boosted_hex_eligibility.is_eligible(); - - self.covered_hexes - .clone() - .into_iter() - .filter(move |_| eligible) - .filter(|hex| hex.boosted_multiplier.is_some()) - } -} - #[derive(Debug, Clone)] pub enum BoostedHexStatus { Eligible, From 5d7bd20e67204e6321e558632704b403ae9f2b2a Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 15:30:55 -0700 Subject: [PATCH 072/115] If a type can trivially be copy, let's make it copy --- coverage_point_calculator/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 188e922ff..6e481b748 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -175,7 +175,7 @@ impl RewardableRadio { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub enum BoostedHexStatus { Eligible, WifiLocationScoreBelowThreshold(Decimal), @@ -261,7 +261,7 @@ impl RadioType { } } -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum RadioThreshold { Verified, UnVerified, From 03dfe72230c57ba49f21edcda05f5019ea782538 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 15:31:28 -0700 Subject: [PATCH 073/115] move error closer to Result it takes part in --- coverage_point_calculator/src/lib.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 6e481b748..49dfaf0ba 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -69,6 +69,11 @@ pub mod speedtest; pub type Result = std::result::Result; +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("signal level {0:?} not allowed for {1:?}")] + InvalidSignalLevel(SignalLevel, RadioType), +} /// Necessary checks for calculating coverage points is done during [RewardableRadio::new]. #[derive(Debug, Clone)] pub struct RewardableRadio { @@ -112,12 +117,6 @@ pub struct CoveragePoints { pub boosted_hex_eligibility: BoostedHexStatus, } -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("signal level {0:?} not allowed for {1:?}")] - InvalidSignalLevel(SignalLevel, RadioType), -} - pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { let hex_coverage_points = radio.covered_hexes.calculated_coverage_points(); let location_trust_multiplier = radio.location_trust_scores.multiplier; From 1730802c018aec0f3339fe3ee6129dc681e1b5d3 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 15:37:12 -0700 Subject: [PATCH 074/115] remove need for managing rank as a index into a list --- coverage_point_calculator/src/hexes.rs | 6 +----- coverage_point_calculator/src/lib.rs | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/coverage_point_calculator/src/hexes.rs b/coverage_point_calculator/src/hexes.rs index 8a23bf81a..9b515595b 100644 --- a/coverage_point_calculator/src/hexes.rs +++ b/coverage_point_calculator/src/hexes.rs @@ -49,12 +49,8 @@ impl CoveredHexes { .into_iter() .map(|ranked| { let coverage_points = radio_type.base_coverage_points(&ranked.signal_level)?; + let rank_multiplier = radio_type.rank_multiplier(ranked.rank); let assignment_multiplier = ranked.assignments.boosting_multiplier(); - let rank_multiplier = rank_multipliers - .get(ranked.rank - 1) - .cloned() - .unwrap_or(dec!(0)); - let boosted_multiplier = ranked.boosted.map(|boost| boost.get()).map(Decimal::from); let calculated_coverage_points = coverage_points diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 49dfaf0ba..01a258d0d 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -242,12 +242,21 @@ impl RadioType { Ok(mult) } - fn rank_multipliers(&self) -> Vec { - match self { - RadioType::IndoorWifi => vec![dec!(1)], - RadioType::IndoorCbrs => vec![dec!(1)], - RadioType::OutdoorWifi => vec![dec!(1), dec!(0.5), dec!(0.25)], - RadioType::OutdoorCbrs => vec![dec!(1), dec!(0.5), dec!(0.25)], + fn rank_multiplier(&self, rank: usize) -> Decimal { + match (self, rank) { + // Indoors Radios + (RadioType::IndoorWifi, 1) => dec!(1), + (RadioType::IndoorCbrs, 1) => dec!(1), + // Outdoor Wifi + (RadioType::OutdoorWifi, 1) => dec!(1), + (RadioType::OutdoorWifi, 2) => dec!(0.5), + (RadioType::OutdoorWifi, 3) => dec!(0.25), + // Outdoor Cbrs + (RadioType::OutdoorCbrs, 1) => dec!(1), + (RadioType::OutdoorCbrs, 2) => dec!(0.5), + (RadioType::OutdoorCbrs, 3) => dec!(0.25), + // Radios outside acceptable rank in a hex do not get points for that hex. + _ => dec!(0), } } From d96ddab50eb3dde8ba4632fa4193b879112a8f63 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 15:38:41 -0700 Subject: [PATCH 075/115] add actual comments for fields in CoveredHex --- coverage_point_calculator/src/hexes.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/coverage_point_calculator/src/hexes.rs b/coverage_point_calculator/src/hexes.rs index 9b515595b..eecd2d93d 100644 --- a/coverage_point_calculator/src/hexes.rs +++ b/coverage_point_calculator/src/hexes.rs @@ -13,16 +13,18 @@ pub struct CoveredHexes { #[derive(Debug, Clone)] pub struct CoveredHex { pub hex: hextree::Cell, - // -- + /// Default points received from (RadioType, SignalLevel) pair. pub base_coverage_points: Decimal, + /// Coverage points including assignment, rank, and boosted hex multipliers. pub calculated_coverage_points: Decimal, - // oracle boosted + /// Oracle boosted Assignments pub assignments: HexAssignments, pub assignment_multiplier: Decimal, - // -- + /// [RankedCoverage::rank] 1-based pub rank: usize, pub rank_multiplier: Decimal, - // provider boosted + /// Provider boosted multiplier. Will be None if the Radio does not qualify + /// for boosted rewards. pub boosted_multiplier: Option, } From f94def7da2cceede0fc32df5125fe7a7597bdbe5 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 15:39:52 -0700 Subject: [PATCH 076/115] Unverified is a single word --- coverage_point_calculator/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 01a258d0d..8f3b873ce 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -272,7 +272,7 @@ impl RadioType { #[derive(Debug, Clone, Copy, PartialEq)] pub enum RadioThreshold { Verified, - UnVerified, + Unverified, } impl RadioThreshold { @@ -331,7 +331,7 @@ mod tests { // Radio not meeting the threshold is not eligible for boosted hexes. // Boost from hex is not applied, radio receives base points. - let unverified_wifi = make_wifi(RadioThreshold::UnVerified); + let unverified_wifi = make_wifi(RadioThreshold::Unverified); assert_eq!( base_points, calculate_coverage_points(unverified_wifi).total_coverage_points From 4601b7446eb8a91020c011ce1903b7c1aa126046 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 15:40:50 -0700 Subject: [PATCH 077/115] question was answered, negative "No, if the radio covers a boosted hex, then the location trust score is calculated using the more restrictive distance_to_asserted rule." - bbalser --- coverage_point_calculator/src/lib.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 8f3b873ce..a6cf005b2 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -148,10 +148,6 @@ impl RewardableRadio { radio_threshold: RadioThreshold, ranked_coverage: Vec, ) -> Result { - // QUESTION: we need to know about boosted hexes to determine location multiplier. - // The location multiplier is then used to determine if they are eligible for boosted hexes. - // In the case where they cannot use boosted hexes, should the location mulitiplier be restored? - let location_trust_scores = LocationTrustScores::new(radio_type, location_trust_scores, &ranked_coverage); From 33e75731a0951772fb68527b8a01eb3ceeb7f530 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 15:47:59 -0700 Subject: [PATCH 078/115] fix doc references to code --- coverage_point_calculator/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index a6cf005b2..041e208bf 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -92,10 +92,10 @@ pub struct RewardableRadio { /// - If more than the allowed speedtests were provided, only the speedtests /// considered are included here. /// -/// - When a radio covers boosted hexes, [location_trust_scores] will contain a +/// - When a radio covers boosted hexes, [CoveragePoints::location_trust_scores] will contain a /// trust score _after_ the boosted hex restriction has been applied. /// -/// - When a radio is not eligible for boosted hex rewards, [covered_hexes] will +/// - When a radio is not eligible for boosted hex rewards, [CoveragePoints::covered_hexes] will /// have no boosted_multiplier values. /// #[derive(Debug)] From 6a43d7b012b50bcb122a6301c93d79eca01f9a6e Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 15:48:13 -0700 Subject: [PATCH 079/115] include rstest --- Cargo.lock | 40 ++++++++++++++++++++++++++++ coverage_point_calculator/Cargo.toml | 1 + 2 files changed, 41 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index c6eb61e79..6c27516d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2139,6 +2139,7 @@ dependencies = [ "helium-crypto", "hex-assignments", "hextree", + "rstest", "rust_decimal", "rust_decimal_macros", "thiserror", @@ -3254,6 +3255,12 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "goblin" version = "0.5.4" @@ -5954,6 +5961,12 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -6189,6 +6202,33 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rstest" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afd55a67069d6e434a95161415f5beeada95a01c7b815508a82dcb0e1593682" +dependencies = [ + "rstest_macros", + "rustc_version 0.4.0", +] + +[[package]] +name = "rstest_macros" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4165dfae59a39dd41d8dec720d3cbfbc71f69744efb480a3920f5d4e0cc6798d" +dependencies = [ + "cfg-if", + "glob", + "proc-macro2 1.0.69", + "quote 1.0.33", + "regex", + "relative-path", + "rustc_version 0.4.0", + "syn 2.0.38", + "unicode-ident", +] + [[package]] name = "rtoolbox" version = "0.0.2" diff --git a/coverage_point_calculator/Cargo.toml b/coverage_point_calculator/Cargo.toml index 38d76d8f7..fc59596ee 100644 --- a/coverage_point_calculator/Cargo.toml +++ b/coverage_point_calculator/Cargo.toml @@ -17,3 +17,4 @@ coverage-map = { path = "../coverage_map" } [dev-dependencies] helium-crypto = { workspace = true } +rstest = { version = "0.21.0", default-features = false } From 7004083efbd860abe1d5d5a494c4ee589bb155dd Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 15:49:07 -0700 Subject: [PATCH 080/115] more natural function name to read --- coverage_point_calculator/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 041e208bf..0b7883f9a 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -189,7 +189,7 @@ impl BoostedHexStatus { } // hip84: if radio has not met minimum data and subscriber thresholds, no boosting - if !radio_threshold.threshold_met() { + if !radio_threshold.is_met() { return Self::RadioThresholdNotMet; } @@ -272,7 +272,7 @@ pub enum RadioThreshold { } impl RadioThreshold { - fn threshold_met(&self) -> bool { + fn is_met(&self) -> bool { matches!(self, Self::Verified) } } From 963e9c37af1c0ddb17ed8fddc08075fe7317a223 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 15:49:38 -0700 Subject: [PATCH 081/115] make unread fields public You could make the argument to remove these fields, they were used in the construction of a radio, and play a critical role in determining points. I chose to keep them around because if anything happens where you have a radio and want information about values within, these fields will be crucial in helping you figure that out. --- coverage_point_calculator/src/lib.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 0b7883f9a..5cd8adb8b 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -74,15 +74,16 @@ pub enum Error { #[error("signal level {0:?} not allowed for {1:?}")] InvalidSignalLevel(SignalLevel, RadioType), } + /// Necessary checks for calculating coverage points is done during [RewardableRadio::new]. #[derive(Debug, Clone)] pub struct RewardableRadio { - radio_type: RadioType, + pub radio_type: RadioType, + pub radio_threshold: RadioThreshold, + pub boosted_hex_eligibility: BoostedHexStatus, speedtests: Speedtests, location_trust_scores: LocationTrustScores, - radio_threshold: RadioThreshold, covered_hexes: CoveredHexes, - boosted_hex_eligibility: BoostedHexStatus, } /// Output of calculating coverage points for a [RewardableRadio]. From 4780bf36ee957c62a7fd67313f82ea4d151ccd1f Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 15:52:50 -0700 Subject: [PATCH 082/115] remove radio information from returned coverage points rather than consuming a radio you were forced to construct, and returning much of the same information in a different shape. You get to keep the radio you've made, and if you want to know about both, you can hold on to both. --- coverage_point_calculator/src/lib.rs | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 5cd8adb8b..0bbb2678c 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -109,16 +109,9 @@ pub struct CoveragePoints { pub location_trust_multiplier: Decimal, /// Speedtest Mulitplier, maximum of 1 pub speedtest_multiplier: Decimal, - // --- - pub radio_type: RadioType, - pub radio_threshold: RadioThreshold, - pub speedtests: Vec, - pub location_trust_scores: Vec, - pub covered_hexes: Vec, - pub boosted_hex_eligibility: BoostedHexStatus, } -pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { +pub fn calculate_coverage_points(radio: &RewardableRadio) -> CoveragePoints { let hex_coverage_points = radio.covered_hexes.calculated_coverage_points(); let location_trust_multiplier = radio.location_trust_scores.multiplier; let speedtest_multiplier = radio.speedtests.multiplier; @@ -131,13 +124,6 @@ pub fn calculate_coverage_points(radio: RewardableRadio) -> CoveragePoints { hex_coverage_points, location_trust_multiplier, speedtest_multiplier, - // Radio information - radio_type: radio.radio_type, - radio_threshold: radio.radio_threshold, - speedtests: radio.speedtests.speedtests, - location_trust_scores: radio.location_trust_scores.trust_scores, - covered_hexes: radio.covered_hexes.hexes, - boosted_hex_eligibility: radio.boosted_hex_eligibility, } } From bac1804491e2f35709e9137beda7c4c260d076c6 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 15:54:21 -0700 Subject: [PATCH 083/115] try not to compare directly against values This tests cares about the relative value of the speedtest multiplier being applied. As long as that relationship holds, this test is fine. It doesn't want to worry about arbitrary points changing. --- coverage_point_calculator/src/lib.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 0bbb2678c..c65367e96 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -382,9 +382,13 @@ mod tests { .expect("indoor cbrs with speedtests") }; + let base_coverage_points = RadioType::IndoorCbrs + .base_coverage_points(&SignalLevel::High) + .unwrap(); + let indoor_cbrs = make_indoor_cbrs(speedtest_maximum()); assert_eq!( - dec!(100), + base_coverage_points * SpeedtestTier::Good.multiplier(), calculate_coverage_points(indoor_cbrs).total_coverage_points ); @@ -393,7 +397,7 @@ mod tests { speedtest_with_download(BytesPs::mbps(88)), ]); assert_eq!( - dec!(75), + base_coverage_points * SpeedtestTier::Acceptable.multiplier(), calculate_coverage_points(indoor_cbrs.clone()).total_coverage_points ); @@ -402,7 +406,7 @@ mod tests { speedtest_with_download(BytesPs::mbps(62)), ]); assert_eq!( - dec!(50), + base_coverage_points * SpeedtestTier::Degraded.multiplier(), calculate_coverage_points(indoor_cbrs).total_coverage_points ); @@ -411,7 +415,7 @@ mod tests { speedtest_with_download(BytesPs::mbps(42)), ]); assert_eq!( - dec!(25), + base_coverage_points * SpeedtestTier::Poor.multiplier(), calculate_coverage_points(indoor_cbrs).total_coverage_points ); @@ -420,7 +424,7 @@ mod tests { speedtest_with_download(BytesPs::mbps(25)), ]); assert_eq!( - dec!(0), + base_coverage_points * SpeedtestTier::Fail.multiplier(), calculate_coverage_points(indoor_cbrs).total_coverage_points ); } From d846d7dfb32c3782b876df5c8a4bee01a16b58e6 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 15:55:38 -0700 Subject: [PATCH 084/115] provide a parameterized test for each type of radio This allows us to be more specific about how much a hex contributes to each type of radio without needing to know how individual hex values stack. --- coverage_point_calculator/src/lib.rs | 234 +++++++++++---------------- 1 file changed, 94 insertions(+), 140 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index c65367e96..6530aa32f 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -267,12 +267,12 @@ impl RadioThreshold { #[cfg(test)] mod tests { - use std::{num::NonZeroU32, str::FromStr}; + use rstest::rstest; + use speedtest::SpeedtestTier; - use crate::{ - location::Meters, - speedtest::{BytesPs, Millis}, - }; + use std::num::NonZeroU32; + + use crate::{location::Meters, speedtest::BytesPs}; use super::*; use chrono::Utc; @@ -672,172 +672,126 @@ mod tests { ); } - #[test] - fn base_radio_coverage_points() { + #[rstest] + #[case(SignalLevel::High, dec!(4))] + #[case(SignalLevel::Medium, dec!(2))] + #[case(SignalLevel::Low, dec!(1))] + #[case(SignalLevel::None, dec!(0))] + fn outdoor_cbrs_base_coverage_points( + #[case] signal_level: SignalLevel, + #[case] expected: Decimal, + ) { let outdoor_cbrs = RewardableRadio::new( RadioType::OutdoorCbrs, speedtest_maximum(), location_trust_maximum(), RadioThreshold::Verified, - vec![ - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: Some("serial".to_string()), - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: Some("serial".to_string()), - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::Medium, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: Some("serial".to_string()), - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::Low, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: Some("serial".to_string()), - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::None, - assignments: assignments_maximum(), - boosted: None, - }, - ], + vec![RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, + signal_level, + assignments: assignments_maximum(), + boosted: None, + }], ) .expect("outdoor cbrs"); + assert_eq!( + expected, + calculate_coverage_points(outdoor_cbrs).total_coverage_points + ); + } + + #[rstest] + #[case(SignalLevel::High, dec!(100))] + #[case(SignalLevel::Low, dec!(25))] + fn indoor_cbrs_base_coverage_points( + #[case] signal_level: SignalLevel, + #[case] expected: Decimal, + ) { let indoor_cbrs = RewardableRadio::new( RadioType::IndoorCbrs, speedtest_maximum(), location_trust_maximum(), RadioThreshold::Verified, - vec![ - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: Some("serial".to_string()), - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: Some("serial".to_string()), - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::Low, - assignments: assignments_maximum(), - boosted: None, - }, - ], + vec![RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, + signal_level, + assignments: assignments_maximum(), + boosted: None, + }], ) .expect("indoor cbrs"); + assert_eq!( + expected, + calculate_coverage_points(indoor_cbrs).total_coverage_points + ); + } + + #[rstest] + #[case(SignalLevel::High, dec!(16))] + #[case(SignalLevel::Medium, dec!(8))] + #[case(SignalLevel::Low, dec!(4))] + #[case(SignalLevel::None, dec!(0))] + fn outdoor_wifi_base_coverage_points( + #[case] signal_level: SignalLevel, + #[case] expected: Decimal, + ) { let outdoor_wifi = RewardableRadio::new( - RadioType::OutdoorWifi, + RadioType::IndoorCbrs, speedtest_maximum(), location_trust_maximum(), RadioThreshold::Verified, - vec![ - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::Medium, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::Low, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::None, - assignments: assignments_maximum(), - boosted: None, - }, - ], + vec![RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: Some("serial".to_string()), + hex: hex_location(), + rank: 1, + signal_level, + assignments: assignments_maximum(), + boosted: None, + }], ) - .expect("outdoor wifi"); + .expect("indoor cbrs"); + assert_eq!( + expected, + calculate_coverage_points(outdoor_wifi).total_coverage_points + ); + } + + #[rstest] + #[case(SignalLevel::High, dec!(400))] + #[case(SignalLevel::Low, dec!(100))] + fn indoor_wifi_base_coverage_points( + #[case] signal_level: SignalLevel, + #[case] expected: Decimal, + ) { let indoor_wifi = RewardableRadio::new( RadioType::IndoorWifi, speedtest_maximum(), location_trust_maximum(), RadioThreshold::Verified, - vec![ - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::Low, - assignments: assignments_maximum(), - boosted: None, - }, - ], + vec![RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level, + assignments: assignments_maximum(), + boosted: None, + }], ) .expect("indoor wifi"); - // When each radio contains a hex of every applicable signal_level, and - // multipliers are break even. These are the accumulated coverage points. - assert_eq!( - dec!(7), - calculate_coverage_points(outdoor_cbrs).total_coverage_points - ); - assert_eq!( - dec!(125), - calculate_coverage_points(indoor_cbrs).total_coverage_points - ); - assert_eq!( - dec!(28), - calculate_coverage_points(outdoor_wifi).total_coverage_points - ); assert_eq!( - dec!(500), + expected, calculate_coverage_points(indoor_wifi).total_coverage_points ); } From 33360701ed2785f5b5a04b7df2a93a838a596652 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 15:57:53 -0700 Subject: [PATCH 085/115] coverage-map was updated to not use helium-crypto --- coverage_point_calculator/src/lib.rs | 7 ++----- .../tests/coverage_point_calculator.rs | 16 +++------------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 6530aa32f..99ebe31bc 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -853,10 +853,7 @@ mod tests { .collect() } - fn pubkey() -> helium_crypto::PublicKeyBinary { - helium_crypto::PublicKeyBinary::from_str( - "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6", - ) - .expect("failed owner parse") + fn pubkey() -> Vec { + vec![1] } } diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index 9ca8e98e5..c801024c9 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -1,4 +1,4 @@ -use std::{num::NonZeroU32, str::FromStr}; +use std::num::NonZeroU32; use chrono::Utc; use coverage_map::{RankedCoverage, SignalLevel}; @@ -32,13 +32,8 @@ fn base_radio_coverage_points() { trust_score: dec!(1.0), }]; - let pubkey = helium_crypto::PublicKeyBinary::from_str( - "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6", - ) - .unwrap(); - let hexes = vec![RankedCoverage { - hotspot_key: pubkey, + hotspot_key: vec![1], cbsd_id: None, hex: hextree::Cell::from_raw(0x8c2681a3064edff).unwrap(), rank: 1, @@ -79,13 +74,8 @@ fn radios_with_coverage() { // Enough hexes will be provided to each type of radio, that they are // awarded 400 coverage points. - let pubkey = helium_crypto::PublicKeyBinary::from_str( - "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6", - ) - .unwrap(); - let base_hex = RankedCoverage { - hotspot_key: pubkey, + hotspot_key: vec![1], cbsd_id: None, hex: hextree::Cell::from_raw(0x8c2681a3064edff).unwrap(), rank: 1, From c4e43eb10ae3f9757776fa8bdf5e047d05adff72 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 15:59:21 -0700 Subject: [PATCH 086/115] remove Millis as a newtype There's nothing that can be further enforced by having a newtype. Making the field as `_millis` communicates the same ammount of information as the newtype. --- coverage_point_calculator/src/lib.rs | 6 +- coverage_point_calculator/src/speedtest.rs | 59 ++++++++----------- .../tests/coverage_point_calculator.rs | 10 ++-- 3 files changed, 34 insertions(+), 41 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 99ebe31bc..6a9df78d7 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -813,13 +813,13 @@ mod tests { Speedtest { upload_speed: BytesPs::mbps(15), download_speed: BytesPs::mbps(150), - latency: Millis::new(15), + latency_millis: 15, timestamp: Utc::now(), }, Speedtest { upload_speed: BytesPs::mbps(15), download_speed: BytesPs::mbps(150), - latency: Millis::new(15), + latency_millis: 15, timestamp: Utc::now(), }, ] @@ -829,7 +829,7 @@ mod tests { Speedtest { upload_speed: BytesPs::mbps(15), download_speed: download, - latency: Millis::new(15), + latency_millis: 15, timestamp: Utc::now(), } } diff --git a/coverage_point_calculator/src/speedtest.rs b/coverage_point_calculator/src/speedtest.rs index 55372228a..5e0a3de46 100644 --- a/coverage_point_calculator/src/speedtest.rs +++ b/coverage_point_calculator/src/speedtest.rs @@ -5,6 +5,8 @@ use rust_decimal_macros::dec; const MIN_REQUIRED_SPEEDTEST_SAMPLES: usize = 2; const MAX_ALLOWED_SPEEDTEST_SAMPLES: usize = 6; +type Millis = u32; + #[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)] pub struct BytesPs(u64); @@ -22,15 +24,6 @@ impl BytesPs { } } -#[derive(Debug, Default, Clone, Copy, PartialEq)] -pub struct Millis(u32); - -impl Millis { - pub fn new(milliseconds: u32) -> Self { - Self(milliseconds) - } -} - #[derive(Debug, Clone)] pub struct Speedtests { pub multiplier: Decimal, @@ -69,7 +62,7 @@ impl Speedtests { pub struct Speedtest { pub upload_speed: BytesPs, pub download_speed: BytesPs, - pub latency: Millis, + pub latency_millis: u32, pub timestamp: DateTime, } @@ -77,7 +70,7 @@ impl Speedtest { pub fn multiplier(&self) -> Decimal { let upload = SpeedtestTier::from_upload(&self.upload_speed); let download = SpeedtestTier::from_download(&self.download_speed); - let latency = SpeedtestTier::from_latency(&self.latency); + let latency = SpeedtestTier::from_latency(self.latency_millis); let tier = upload.min(download).min(latency); tier.multiplier() @@ -91,14 +84,14 @@ impl Speedtest { for test in speedtests { upload += test.upload_speed.0; download += test.download_speed.0; - latency += test.latency.0; + latency += test.latency_millis; } let count = speedtests.len(); Self { upload_speed: BytesPs::new(upload / count as u64), download_speed: BytesPs::new(download / count as u64), - latency: Millis::new(latency / count as u32), + latency_millis: latency / count as u32, timestamp: Utc::now(), } } @@ -144,7 +137,7 @@ impl SpeedtestTier { } } - fn from_latency(Millis(millis): &Millis) -> Self { + fn from_latency(millis: Millis) -> Self { match millis { ..=49 => Self::Good, ..=59 => Self::Acceptable, @@ -177,11 +170,11 @@ mod tests { assert_eq!(Fail, SpeedtestTier::from_upload(&BytesPs::mbps(1))); // latency - assert_eq!(Good, SpeedtestTier::from_latency(&Millis::new(49))); - assert_eq!(Acceptable, SpeedtestTier::from_latency(&Millis::new(59))); - assert_eq!(Degraded, SpeedtestTier::from_latency(&Millis::new(74))); - assert_eq!(Poor, SpeedtestTier::from_latency(&Millis::new(99))); - assert_eq!(Fail, SpeedtestTier::from_latency(&Millis::new(101))); + assert_eq!(Good, SpeedtestTier::from_latency(49)); + assert_eq!(Acceptable, SpeedtestTier::from_latency(59)); + assert_eq!(Degraded, SpeedtestTier::from_latency(74)); + assert_eq!(Poor, SpeedtestTier::from_latency(99)); + assert_eq!(Fail, SpeedtestTier::from_latency(101)); } #[test] @@ -189,7 +182,7 @@ mod tests { let base = Speedtest { upload_speed: BytesPs::mbps(15), download_speed: BytesPs::mbps(150), - latency: Millis::new(15), + latency_millis: 15, timestamp: Utc::now(), }; let speedtests = std::iter::repeat(base).take(10).collect(); @@ -203,7 +196,7 @@ mod tests { let make_speedtest = |timestamp: DateTime, latency: Millis| Speedtest { upload_speed: BytesPs::mbps(15), download_speed: BytesPs::mbps(150), - latency, + latency_millis: latency, timestamp, }; @@ -211,23 +204,23 @@ mod tests { // new speedtests have 1.0 multipliers // old speedtests have 0.0 multipliers let speedtests = Speedtests::new(vec![ - make_speedtest(date(2024, 4, 6), Millis::new(15)), - make_speedtest(date(2022, 4, 6), Millis::new(999)), + make_speedtest(date(2024, 4, 6), 15), + make_speedtest(date(2022, 4, 6), 999), // -- - make_speedtest(date(2024, 4, 5), Millis::new(15)), - make_speedtest(date(2022, 4, 5), Millis::new(999)), + make_speedtest(date(2024, 4, 5), 15), + make_speedtest(date(2022, 4, 5), 999), // -- - make_speedtest(date(2024, 4, 4), Millis::new(15)), - make_speedtest(date(2022, 4, 4), Millis::new(999)), + make_speedtest(date(2024, 4, 4), 15), + make_speedtest(date(2022, 4, 4), 999), // -- - make_speedtest(date(2022, 4, 3), Millis::new(999)), - make_speedtest(date(2024, 4, 3), Millis::new(15)), + make_speedtest(date(2022, 4, 3), 999), + make_speedtest(date(2024, 4, 3), 15), // -- - make_speedtest(date(2024, 4, 2), Millis::new(15)), - make_speedtest(date(2022, 4, 2), Millis::new(999)), + make_speedtest(date(2024, 4, 2), 15), + make_speedtest(date(2022, 4, 2), 999), // -- - make_speedtest(date(2024, 4, 1), Millis::new(15)), - make_speedtest(date(2022, 4, 1), Millis::new(999)), + make_speedtest(date(2024, 4, 1), 15), + make_speedtest(date(2022, 4, 1), 999), ]); // Old speedtests should be unused diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index c801024c9..ead088de8 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -5,7 +5,7 @@ use coverage_map::{RankedCoverage, SignalLevel}; use coverage_point_calculator::{ calculate_coverage_points, location::{LocationTrust, Meters}, - speedtest::{BytesPs, Millis, Speedtest}, + speedtest::{BytesPs, Speedtest}, RadioThreshold, RadioType, RewardableRadio, }; use hex_assignments::{assignment::HexAssignments, Assignment}; @@ -17,13 +17,13 @@ fn base_radio_coverage_points() { Speedtest { upload_speed: BytesPs::mbps(15), download_speed: BytesPs::mbps(150), - latency: Millis::new(15), + latency_millis: 15, timestamp: Utc::now(), }, Speedtest { upload_speed: BytesPs::mbps(15), download_speed: BytesPs::mbps(150), - latency: Millis::new(15), + latency_millis: 15, timestamp: Utc::now(), }, ]; @@ -93,13 +93,13 @@ fn radios_with_coverage() { Speedtest { upload_speed: BytesPs::mbps(15), download_speed: BytesPs::mbps(150), - latency: Millis::new(15), + latency_millis: 15, timestamp: Utc::now(), }, Speedtest { upload_speed: BytesPs::mbps(15), download_speed: BytesPs::mbps(150), - latency: Millis::new(15), + latency_millis: 15, timestamp: Utc::now(), }, ]; From 7be97e91c58cdbb6a490c4d9d5812617d67cb9f8 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 16:01:28 -0700 Subject: [PATCH 087/115] SpeedtestTier is Copy, as well as BytePs We don't need to enforce passing references for Copy types, let people use them directly and the compiler can figure it out. --- coverage_point_calculator/src/speedtest.rs | 30 +++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/coverage_point_calculator/src/speedtest.rs b/coverage_point_calculator/src/speedtest.rs index 5e0a3de46..ad94655a8 100644 --- a/coverage_point_calculator/src/speedtest.rs +++ b/coverage_point_calculator/src/speedtest.rs @@ -68,8 +68,8 @@ pub struct Speedtest { impl Speedtest { pub fn multiplier(&self) -> Decimal { - let upload = SpeedtestTier::from_upload(&self.upload_speed); - let download = SpeedtestTier::from_download(&self.download_speed); + let upload = SpeedtestTier::from_upload(self.upload_speed); + let download = SpeedtestTier::from_download(self.download_speed); let latency = SpeedtestTier::from_latency(self.latency_millis); let tier = upload.min(download).min(latency); @@ -107,7 +107,7 @@ pub enum SpeedtestTier { } impl SpeedtestTier { - pub fn multiplier(&self) -> Decimal { + pub fn multiplier(self) -> Decimal { match self { SpeedtestTier::Good => dec!(1.00), SpeedtestTier::Acceptable => dec!(0.75), @@ -117,7 +117,7 @@ impl SpeedtestTier { } } - fn from_download(bytes: &BytesPs) -> Self { + fn from_download(bytes: BytesPs) -> Self { match bytes.as_mbps() { 100.. => Self::Good, 75.. => Self::Acceptable, @@ -127,7 +127,7 @@ impl SpeedtestTier { } } - fn from_upload(bytes: &BytesPs) -> Self { + fn from_upload(bytes: BytesPs) -> Self { match bytes.as_mbps() { 10.. => Self::Good, 8.. => Self::Acceptable, @@ -156,18 +156,18 @@ mod tests { fn speedtest_teirs() { use SpeedtestTier::*; // download - assert_eq!(Good, SpeedtestTier::from_download(&BytesPs::mbps(100))); - assert_eq!(Acceptable, SpeedtestTier::from_download(&BytesPs::mbps(80))); - assert_eq!(Degraded, SpeedtestTier::from_download(&BytesPs::mbps(62))); - assert_eq!(Poor, SpeedtestTier::from_download(&BytesPs::mbps(42))); - assert_eq!(Fail, SpeedtestTier::from_download(&BytesPs::mbps(20))); + assert_eq!(Good, SpeedtestTier::from_download(BytesPs::mbps(100))); + assert_eq!(Acceptable, SpeedtestTier::from_download(BytesPs::mbps(80))); + assert_eq!(Degraded, SpeedtestTier::from_download(BytesPs::mbps(62))); + assert_eq!(Poor, SpeedtestTier::from_download(BytesPs::mbps(42))); + assert_eq!(Fail, SpeedtestTier::from_download(BytesPs::mbps(20))); // upload - assert_eq!(Good, SpeedtestTier::from_upload(&BytesPs::mbps(10))); - assert_eq!(Acceptable, SpeedtestTier::from_upload(&BytesPs::mbps(8))); - assert_eq!(Degraded, SpeedtestTier::from_upload(&BytesPs::mbps(6))); - assert_eq!(Poor, SpeedtestTier::from_upload(&BytesPs::mbps(4))); - assert_eq!(Fail, SpeedtestTier::from_upload(&BytesPs::mbps(1))); + assert_eq!(Good, SpeedtestTier::from_upload(BytesPs::mbps(10))); + assert_eq!(Acceptable, SpeedtestTier::from_upload(BytesPs::mbps(8))); + assert_eq!(Degraded, SpeedtestTier::from_upload(BytesPs::mbps(6))); + assert_eq!(Poor, SpeedtestTier::from_upload(BytesPs::mbps(4))); + assert_eq!(Fail, SpeedtestTier::from_upload(BytesPs::mbps(1))); // latency assert_eq!(Good, SpeedtestTier::from_latency(49)); From 55c530ccc80a49dda95a78725f294bd0831a5614 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 16:02:08 -0700 Subject: [PATCH 088/115] add test to make sure minimum required speedtests are enforced --- coverage_point_calculator/src/speedtest.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/coverage_point_calculator/src/speedtest.rs b/coverage_point_calculator/src/speedtest.rs index ad94655a8..35312619b 100644 --- a/coverage_point_calculator/src/speedtest.rs +++ b/coverage_point_calculator/src/speedtest.rs @@ -177,6 +177,26 @@ mod tests { assert_eq!(Fail, SpeedtestTier::from_latency(101)); } + #[test] + fn minimum_required_speedtests_provided_for_multiplier_above_zero() { + let speedtest = Speedtest { + upload_speed: BytesPs::mbps(15), + download_speed: BytesPs::mbps(150), + latency_millis: 15, + timestamp: Utc::now(), + }; + let speedtests = |num: usize| std::iter::repeat(speedtest).take(num).collect(); + + assert_eq!( + dec!(0), + Speedtests::new(speedtests(MIN_REQUIRED_SPEEDTEST_SAMPLES - 1)).multiplier + ); + assert_eq!( + dec!(1), + Speedtests::new(speedtests(MIN_REQUIRED_SPEEDTEST_SAMPLES)).multiplier + ); + } + #[test] fn restrict_to_maximum_speedtests_allowed() { let base = Speedtest { From 4684bb0a8d81f69a17fee1997cf679f0456af2fc Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 16:02:37 -0700 Subject: [PATCH 089/115] switchup location testing angle Rather than testing statements like "the multiplier should be an average of the scores". This module benefits more from describing the different scenarios, and showing boosted and unboosted versions in contrast with each other, I think. --- coverage_point_calculator/src/location.rs | 176 ++++++++++++---------- 1 file changed, 98 insertions(+), 78 deletions(-) diff --git a/coverage_point_calculator/src/location.rs b/coverage_point_calculator/src/location.rs index 4e9fb6780..66fb94ad7 100644 --- a/coverage_point_calculator/src/location.rs +++ b/coverage_point_calculator/src/location.rs @@ -83,101 +83,121 @@ fn multiplier(trust_scores: &[LocationTrust]) -> Decimal { #[cfg(test)] mod tests { + use std::num::NonZeroU32; + + use coverage_map::SignalLevel; + use hex_assignments::{assignment::HexAssignments, Assignment}; + use super::*; #[test] - fn boosted_hexes_within_distance_retain_trust_score() { - let lts = LocationTrustScores::new_with_boosted_hexes( - &RadioType::IndoorWifi, - vec![LocationTrust { + fn all_locations_within_max_boosted_distance() { + let trust_scores = vec![ + LocationTrust { distance_to_asserted: Meters(49), - trust_score: dec!(1), - }], - ); - - assert_eq!( - LocationTrustScores { - multiplier: dec!(1), - trust_scores: vec![LocationTrust { - distance_to_asserted: Meters(49), - trust_score: dec!(1) - }] + trust_score: dec!(0.5), + }, + LocationTrust { + distance_to_asserted: Meters(50), + trust_score: dec!(0.5), }, - lts + ]; + let boosted = LocationTrustScores::new( + RadioType::IndoorWifi, + trust_scores.clone(), + &boosted_ranked_coverage(), ); + let unboosted = LocationTrustScores::new(RadioType::IndoorWifi, trust_scores, &[]); + + assert_eq!(dec!(0.5), boosted.multiplier); + assert_eq!(dec!(0.5), unboosted.multiplier); } #[test] - fn boosted_hexes_past_distance_reduce_trust_score() { - let lts = LocationTrustScores::new_with_boosted_hexes( - &RadioType::IndoorWifi, - vec![LocationTrust { + fn all_locations_past_max_boosted_distance() { + let trust_scores = vec![ + LocationTrust { distance_to_asserted: Meters(51), - trust_score: dec!(1), - }], + trust_score: dec!(0.5), + }, + LocationTrust { + distance_to_asserted: Meters(100), + trust_score: dec!(0.5), + }, + ]; + + let boosted = LocationTrustScores::new( + RadioType::IndoorWifi, + trust_scores.clone(), + &boosted_ranked_coverage(), ); + let unboosted = LocationTrustScores::new(RadioType::IndoorWifi, trust_scores, &[]); + + assert_eq!(dec!(0.25), boosted.multiplier); + assert_eq!(dec!(0.5), unboosted.multiplier); + } - assert_eq!( - LocationTrustScores { - multiplier: dec!(0.25), - trust_scores: vec![LocationTrust { - distance_to_asserted: Meters(51), - trust_score: dec!(0.25) - }] + #[test] + fn locations_around_max_boosted_distance() { + let trust_scores = vec![ + LocationTrust { + distance_to_asserted: Meters(50), + trust_score: dec!(0.5), + }, + LocationTrust { + distance_to_asserted: Meters(51), + trust_score: dec!(0.5), }, - lts + ]; + + let boosted = LocationTrustScores::new( + RadioType::IndoorWifi, + trust_scores.clone(), + &boosted_ranked_coverage(), ); + let unboosted = LocationTrustScores::new(RadioType::IndoorWifi, trust_scores, &[]); + + // location past distance limit trust score is degraded + let degraded_mult = (dec!(0.5) + dec!(0.25)) / dec!(2); + assert_eq!(degraded_mult, boosted.multiplier); + // location past distance limit trust score is untouched + assert_eq!(dec!(0.5), unboosted.multiplier); } #[test] - fn multiplier_is_average_of_scores() { - // All locations within max distance - let boosted_trust_scores = LocationTrustScores::new_with_boosted_hexes( - &RadioType::IndoorWifi, - vec![ - LocationTrust { - distance_to_asserted: Meters(49), - trust_score: dec!(0.5), - }, - LocationTrust { - distance_to_asserted: Meters(49), - trust_score: dec!(0.5), - }, - ], - ); - assert_eq!(dec!(0.5), boosted_trust_scores.multiplier); - - // 1 location within max distance, 1 location outside - let boosted_over_limit_trust_scores = LocationTrustScores::new_with_boosted_hexes( - &RadioType::IndoorWifi, - vec![ - LocationTrust { - distance_to_asserted: Meters(49), - trust_score: dec!(0.5), - }, - LocationTrust { - distance_to_asserted: Meters(51), - trust_score: dec!(0.5), - }, - ], + fn cbrs_trust_score_bypassed_for_gps_trust() { + // CBRS radios have GPS units in them, they are always trusted, + // regardless of their score or distance provided. + + let trust_scores = vec![LocationTrust { + distance_to_asserted: Meters(99999), + trust_score: dec!(0), + }]; + + let boosted = LocationTrustScores::new( + RadioType::IndoorCbrs, + trust_scores.clone(), + &boosted_ranked_coverage(), ); - let mult = (dec!(0.5) + dec!(0.25)) / dec!(2); - assert_eq!(mult, boosted_over_limit_trust_scores.multiplier); - - // All locations outside boosted distance restriction, but no boosted hexes - let unboosted_trust_scores = LocationTrustScores::new( - &RadioType::IndoorWifi, - vec![ - LocationTrust { - distance_to_asserted: Meters(100), - trust_score: dec!(0.5), - }, - LocationTrust { - distance_to_asserted: Meters(100), - trust_score: dec!(0.5), - }, - ], - ); - assert_eq!(dec!(0.5), unboosted_trust_scores.multiplier); + let unboosted = LocationTrustScores::new(RadioType::IndoorCbrs, trust_scores, &[]); + + assert_eq!(dec!(1), boosted.multiplier); + assert_eq!(dec!(1), unboosted.multiplier); + } + + fn boosted_ranked_coverage() -> Vec { + vec![RankedCoverage { + hex: hextree::Cell::from_raw(0x8c2681a3064edff).unwrap(), + rank: 1, + hotspot_key: vec![], + cbsd_id: None, + signal_level: SignalLevel::High, + assignments: HexAssignments { + footfall: Assignment::A, + landtype: Assignment::A, + urbanized: Assignment::A, + }, + boosted: NonZeroU32::new(5), + }] } } From fca8c2f8d80619de5dca6f21bd3f7be6f60c3ae5 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 16:06:06 -0700 Subject: [PATCH 090/115] pass radios as references in test --- coverage_point_calculator/src/lib.rs | 38 +++++++++---------- .../tests/coverage_point_calculator.rs | 4 +- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 6a9df78d7..91a1ac0e6 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -309,7 +309,7 @@ mod tests { let verified_wifi = make_wifi(RadioThreshold::Verified); assert_eq!( base_points * dec!(5), - calculate_coverage_points(verified_wifi.clone()).total_coverage_points + calculate_coverage_points(&verified_wifi).total_coverage_points ); // Radio not meeting the threshold is not eligible for boosted hexes. @@ -317,7 +317,7 @@ mod tests { let unverified_wifi = make_wifi(RadioThreshold::Unverified); assert_eq!( base_points, - calculate_coverage_points(unverified_wifi).total_coverage_points + calculate_coverage_points(&unverified_wifi).total_coverage_points ); } @@ -350,15 +350,13 @@ mod tests { // Boosted hex provides radio with more than base_points. let trusted_wifi = make_wifi(location_trust_with_scores(&[dec!(1), dec!(1)])); assert!(trusted_wifi.location_trust_scores.multiplier > dec!(0.75)); - assert!( - calculate_coverage_points(trusted_wifi.clone()).total_coverage_points > base_points - ); + assert!(calculate_coverage_points(&trusted_wifi).total_coverage_points > base_points); // Radio with poor trust score is not eligible for boosted hexes. // Boost from hex is not applied, and points are further lowered by poor trust score. let untrusted_wifi = make_wifi(location_trust_with_scores(&[dec!(0.1), dec!(0.2)])); assert!(untrusted_wifi.location_trust_scores.multiplier < dec!(0.75)); - assert!(calculate_coverage_points(untrusted_wifi).total_coverage_points < base_points); + assert!(calculate_coverage_points(&untrusted_wifi).total_coverage_points < base_points); } #[test] @@ -389,7 +387,7 @@ mod tests { let indoor_cbrs = make_indoor_cbrs(speedtest_maximum()); assert_eq!( base_coverage_points * SpeedtestTier::Good.multiplier(), - calculate_coverage_points(indoor_cbrs).total_coverage_points + calculate_coverage_points(&indoor_cbrs).total_coverage_points ); let indoor_cbrs = make_indoor_cbrs(vec![ @@ -398,7 +396,7 @@ mod tests { ]); assert_eq!( base_coverage_points * SpeedtestTier::Acceptable.multiplier(), - calculate_coverage_points(indoor_cbrs.clone()).total_coverage_points + calculate_coverage_points(&indoor_cbrs).total_coverage_points ); let indoor_cbrs = make_indoor_cbrs(vec![ @@ -407,7 +405,7 @@ mod tests { ]); assert_eq!( base_coverage_points * SpeedtestTier::Degraded.multiplier(), - calculate_coverage_points(indoor_cbrs).total_coverage_points + calculate_coverage_points(&indoor_cbrs).total_coverage_points ); let indoor_cbrs = make_indoor_cbrs(vec![ @@ -416,7 +414,7 @@ mod tests { ]); assert_eq!( base_coverage_points * SpeedtestTier::Poor.multiplier(), - calculate_coverage_points(indoor_cbrs).total_coverage_points + calculate_coverage_points(&indoor_cbrs).total_coverage_points ); let indoor_cbrs = make_indoor_cbrs(vec![ @@ -425,7 +423,7 @@ mod tests { ]); assert_eq!( base_coverage_points * SpeedtestTier::Fail.multiplier(), - calculate_coverage_points(indoor_cbrs).total_coverage_points + calculate_coverage_points(&indoor_cbrs).total_coverage_points ); } @@ -498,7 +496,7 @@ mod tests { assert_eq!( dec!(1073), - calculate_coverage_points(indoor_cbrs).total_coverage_points + calculate_coverage_points(&indoor_cbrs).total_coverage_points ); } @@ -556,7 +554,7 @@ mod tests { // rank 42 :: 0.00 * 16 == 0 assert_eq!( dec!(28), - calculate_coverage_points(outdoor_wifi).total_coverage_points + calculate_coverage_points(&outdoor_wifi).total_coverage_points ); } @@ -601,7 +599,7 @@ mod tests { assert_eq!( dec!(400), - calculate_coverage_points(indoor_wifi).total_coverage_points + calculate_coverage_points(&indoor_wifi).total_coverage_points ); } @@ -629,7 +627,7 @@ mod tests { // (0.1 + 0.2 + 0.3 + 0.4) / 4 assert_eq!( dec!(100), - calculate_coverage_points(indoor_wifi).total_coverage_points + calculate_coverage_points(&indoor_wifi).total_coverage_points ); } @@ -668,7 +666,7 @@ mod tests { // signal_level of High. assert_eq!( dec!(800), - calculate_coverage_points(indoor_wifi.clone()).total_coverage_points + calculate_coverage_points(&indoor_wifi).total_coverage_points ); } @@ -700,7 +698,7 @@ mod tests { assert_eq!( expected, - calculate_coverage_points(outdoor_cbrs).total_coverage_points + calculate_coverage_points(&outdoor_cbrs).total_coverage_points ); } @@ -730,7 +728,7 @@ mod tests { assert_eq!( expected, - calculate_coverage_points(indoor_cbrs).total_coverage_points + calculate_coverage_points(&indoor_cbrs).total_coverage_points ); } @@ -762,7 +760,7 @@ mod tests { assert_eq!( expected, - calculate_coverage_points(outdoor_wifi).total_coverage_points + calculate_coverage_points(&outdoor_wifi).total_coverage_points ); } @@ -792,7 +790,7 @@ mod tests { assert_eq!( expected, - calculate_coverage_points(indoor_wifi).total_coverage_points + calculate_coverage_points(&indoor_wifi).total_coverage_points ); } diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index ead088de8..a06b13d3a 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -61,7 +61,7 @@ fn base_radio_coverage_points() { ) .unwrap(); - let coverage_points = calculate_coverage_points(radio); + let coverage_points = calculate_coverage_points(&radio); assert_eq!( expcted_base_coverage_point, coverage_points.total_coverage_points @@ -123,7 +123,7 @@ fn radios_with_coverage() { ) .unwrap(); - let coverage_points = calculate_coverage_points(radio); + let coverage_points = calculate_coverage_points(&radio); assert_eq!(dec!(400), coverage_points.total_coverage_points); } } From aada11f90f3c3a5950fceb0ed6be6a7d4d655fe4 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 16:14:59 -0700 Subject: [PATCH 091/115] remove Meters as a newtype There's nothing that can be further enforced by having a newtype. Making the field as `meters_from_` communicates the same ammount of information as the newtype. --- coverage_point_calculator/src/lib.rs | 6 ++-- coverage_point_calculator/src/location.rs | 34 ++++++++----------- .../tests/coverage_point_calculator.rs | 6 ++-- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 91a1ac0e6..99f63273b 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -272,7 +272,7 @@ mod tests { use std::num::NonZeroU32; - use crate::{location::Meters, speedtest::BytesPs}; + use crate::speedtest::BytesPs; use super::*; use chrono::Utc; @@ -834,7 +834,7 @@ mod tests { fn location_trust_maximum() -> Vec { vec![LocationTrust { - distance_to_asserted: Meters::new(1), + meters_to_asserted: 1, trust_score: dec!(1.0), }] } @@ -845,7 +845,7 @@ mod tests { .iter() .copied() .map(|trust_score| LocationTrust { - distance_to_asserted: Meters::new(1), + meters_to_asserted: 1, trust_score, }) .collect() diff --git a/coverage_point_calculator/src/location.rs b/coverage_point_calculator/src/location.rs index 66fb94ad7..5d0182a93 100644 --- a/coverage_point_calculator/src/location.rs +++ b/coverage_point_calculator/src/location.rs @@ -4,16 +4,12 @@ use rust_decimal_macros::dec; use crate::RadioType; -const RESTRICTIVE_MAX_DISTANCE: Meters = Meters(50); +/// When a Radio is covering any boosted hexes, it's trust score location must +/// be within this distance to it's asserted location. Otherwise the trust_score +/// will be capped at 0.25x. +const RESTRICTIVE_MAX_DISTANCE: Meters = 50; -#[derive(Debug, Clone, PartialEq, PartialOrd)] -pub struct Meters(u32); - -impl Meters { - pub fn new(meters: u32) -> Self { - Self(meters) - } -} +type Meters = u32; #[derive(Debug, Clone, PartialEq)] pub struct LocationTrustScores { @@ -23,7 +19,7 @@ pub struct LocationTrustScores { #[derive(Debug, Clone, PartialEq)] pub struct LocationTrust { - pub distance_to_asserted: Meters, + pub meters_to_asserted: Meters, pub trust_score: Decimal, } @@ -61,7 +57,7 @@ impl LocationTrust { fn into_boosted(self) -> Self { // Cap multipliers to 0.25x when a radio covers _any_ boosted hex // and it's distance to asserted is above the threshold. - let trust_score = if self.distance_to_asserted > RESTRICTIVE_MAX_DISTANCE { + let trust_score = if self.meters_to_asserted > RESTRICTIVE_MAX_DISTANCE { dec!(0.25).min(self.trust_score) } else { self.trust_score @@ -69,7 +65,7 @@ impl LocationTrust { LocationTrust { trust_score, - distance_to_asserted: self.distance_to_asserted, + meters_to_asserted: self.meters_to_asserted, } } } @@ -94,11 +90,11 @@ mod tests { fn all_locations_within_max_boosted_distance() { let trust_scores = vec![ LocationTrust { - distance_to_asserted: Meters(49), + meters_to_asserted: 49, trust_score: dec!(0.5), }, LocationTrust { - distance_to_asserted: Meters(50), + meters_to_asserted: 50, trust_score: dec!(0.5), }, ]; @@ -117,11 +113,11 @@ mod tests { fn all_locations_past_max_boosted_distance() { let trust_scores = vec![ LocationTrust { - distance_to_asserted: Meters(51), + meters_to_asserted: 51, trust_score: dec!(0.5), }, LocationTrust { - distance_to_asserted: Meters(100), + meters_to_asserted: 100, trust_score: dec!(0.5), }, ]; @@ -141,11 +137,11 @@ mod tests { fn locations_around_max_boosted_distance() { let trust_scores = vec![ LocationTrust { - distance_to_asserted: Meters(50), + meters_to_asserted: 50, trust_score: dec!(0.5), }, LocationTrust { - distance_to_asserted: Meters(51), + meters_to_asserted: 51, trust_score: dec!(0.5), }, ]; @@ -170,7 +166,7 @@ mod tests { // regardless of their score or distance provided. let trust_scores = vec![LocationTrust { - distance_to_asserted: Meters(99999), + meters_to_asserted: 99999, trust_score: dec!(0), }]; diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index a06b13d3a..72faf928b 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -4,7 +4,7 @@ use chrono::Utc; use coverage_map::{RankedCoverage, SignalLevel}; use coverage_point_calculator::{ calculate_coverage_points, - location::{LocationTrust, Meters}, + location::LocationTrust, speedtest::{BytesPs, Speedtest}, RadioThreshold, RadioType, RewardableRadio, }; @@ -28,7 +28,7 @@ fn base_radio_coverage_points() { }, ]; let location_trust_scores = vec![LocationTrust { - distance_to_asserted: Meters::new(1), + meters_to_asserted: 1, trust_score: dec!(1.0), }]; @@ -104,7 +104,7 @@ fn radios_with_coverage() { }, ]; let default_location_trust_scores = vec![LocationTrust { - distance_to_asserted: Meters::new(1), + meters_to_asserted: 1, trust_score: dec!(1.0), }]; From 479e9328d1659a5fd4bc24c99b331fcbf1708dc7 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 16:20:21 -0700 Subject: [PATCH 092/115] name base_coverage_points consistently having bare coverage_point names laying around can be quickly confusing. --- coverage_point_calculator/src/hexes.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/coverage_point_calculator/src/hexes.rs b/coverage_point_calculator/src/hexes.rs index eecd2d93d..2170edd4d 100644 --- a/coverage_point_calculator/src/hexes.rs +++ b/coverage_point_calculator/src/hexes.rs @@ -50,19 +50,19 @@ impl CoveredHexes { let covered_hexes = ranked_coverage .into_iter() .map(|ranked| { - let coverage_points = radio_type.base_coverage_points(&ranked.signal_level)?; + let base_coverage_points = radio_type.base_coverage_points(&ranked.signal_level)?; let rank_multiplier = radio_type.rank_multiplier(ranked.rank); let assignment_multiplier = ranked.assignments.boosting_multiplier(); let boosted_multiplier = ranked.boosted.map(|boost| boost.get()).map(Decimal::from); - let calculated_coverage_points = coverage_points + let calculated_coverage_points = base_coverage_points * assignment_multiplier * rank_multiplier * boosted_multiplier.unwrap_or(dec!(1)); Ok(CoveredHex { hex: ranked.hex, - base_coverage_points: coverage_points, + base_coverage_points, calculated_coverage_points, assignments: ranked.assignments, assignment_multiplier, From 6fa795d94cc7b109e840877ad7cb65da111b4d51 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 16:39:12 -0700 Subject: [PATCH 093/115] parameterize ranked tests Speaking of a single rank at a time simplifies the test by not needing to know how values stack on top of each other. I kept indoor/outdoor as separate tests as to not overwhelm the reader with a parameterized test that has too many variables --- coverage_point_calculator/src/lib.rs | 84 +++++++++++----------------- 1 file changed, 33 insertions(+), 51 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 99f63273b..6a2520074 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -500,68 +500,50 @@ mod tests { ); } - #[test] - fn outdoor_radios_consider_top_3_ranked_hexes() { + #[rstest] + #[case(RadioType::OutdoorWifi, 1, dec!(16))] + #[case(RadioType::OutdoorWifi, 2, dec!(8))] + #[case(RadioType::OutdoorWifi, 3, dec!(4))] + #[case(RadioType::OutdoorWifi, 42, dec!(0))] + fn outdoor_radios_consider_top_3_ranked_hexes( + #[case] radio_type: RadioType, + #[case] rank: usize, + #[case] expected_points: Decimal, + ) { let outdoor_wifi = RewardableRadio::new( - RadioType::OutdoorWifi, + radio_type, speedtest_maximum(), location_trust_maximum(), RadioThreshold::Verified, - vec![ - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 1, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 2, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 3, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - RankedCoverage { - hotspot_key: pubkey(), - cbsd_id: None, - hex: hex_location(), - rank: 42, - signal_level: SignalLevel::High, - assignments: assignments_maximum(), - boosted: None, - }, - ], + vec![RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank, + signal_level: SignalLevel::High, + assignments: assignments_maximum(), + boosted: None, + }], ) .expect("outdoor wifi"); - // rank 1 :: 1.00 * 16 == 16 - // rank 2 :: 0.50 * 16 == 8 - // rank 3 :: 0.25 * 16 == 4 - // rank 42 :: 0.00 * 16 == 0 assert_eq!( - dec!(28), + expected_points, calculate_coverage_points(&outdoor_wifi).total_coverage_points ); } - #[test] - fn indoor_radios_only_consider_first_ranked_hexes() { + #[rstest] + #[case(RadioType::IndoorWifi, 1, dec!(400))] + #[case(RadioType::IndoorWifi, 2, dec!(0))] + #[case(RadioType::IndoorWifi, 42, dec!(0))] + fn indoor_radios_only_consider_first_ranked_hexes( + #[case] radio_type: RadioType, + #[case] rank: usize, + #[case] expected_points: Decimal, + ) { let indoor_wifi = RewardableRadio::new( - RadioType::IndoorWifi, + radio_type, speedtest_maximum(), location_trust_maximum(), RadioThreshold::Verified, @@ -570,7 +552,7 @@ mod tests { hotspot_key: pubkey(), cbsd_id: None, hex: hex_location(), - rank: 1, + rank, signal_level: SignalLevel::High, assignments: assignments_maximum(), boosted: None, @@ -598,7 +580,7 @@ mod tests { .expect("indoor wifi"); assert_eq!( - dec!(400), + expected_points, calculate_coverage_points(&indoor_wifi).total_coverage_points ); } From 3d3cd0851d7314de918091d9735a8c506f0564a0 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 16:51:26 -0700 Subject: [PATCH 094/115] use correct radio type for test copy/paste error from rebasing on top of coverage-map updates, yay! But now we know the tests break. --- coverage_point_calculator/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 6a2520074..eb3e01750 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -724,7 +724,7 @@ mod tests { #[case] expected: Decimal, ) { let outdoor_wifi = RewardableRadio::new( - RadioType::IndoorCbrs, + RadioType::OutdoorWifi, speedtest_maximum(), location_trust_maximum(), RadioThreshold::Verified, From 389134b00f847ae2968b64d9d9618f0a61967164 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 16:54:38 -0700 Subject: [PATCH 095/115] clippy does not like overlapping ranges I agree with the concept, so I've added the lower bounds. Personally, I still think these match statements are easier to read than the equivalent `else if` expression. So they're staying as range comparisons. --- coverage_point_calculator/src/speedtest.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/coverage_point_calculator/src/speedtest.rs b/coverage_point_calculator/src/speedtest.rs index 35312619b..eb607e864 100644 --- a/coverage_point_calculator/src/speedtest.rs +++ b/coverage_point_calculator/src/speedtest.rs @@ -139,10 +139,10 @@ impl SpeedtestTier { fn from_latency(millis: Millis) -> Self { match millis { - ..=49 => Self::Good, - ..=59 => Self::Acceptable, - ..=74 => Self::Degraded, - ..=99 => Self::Poor, + 00..=49 => Self::Good, + 50..=59 => Self::Acceptable, + 60..=74 => Self::Degraded, + 75..=99 => Self::Poor, _ => Self::Fail, } } From df93e3d841d60df3f5ceab62f1c2914f367208b1 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 17:36:50 -0700 Subject: [PATCH 096/115] exchange struct constructor for function I like the idea of the API to this crate being 2 functions. In my head, that makes it clearer that any functions hanging off of exposed structs can be used for information gathering, but are ancillary to the main use of this crate. --- coverage_point_calculator/src/lib.rs | 88 ++++++++++--------- .../tests/coverage_point_calculator.rs | 7 +- 2 files changed, 50 insertions(+), 45 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index eb3e01750..3258d11da 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -111,6 +111,40 @@ pub struct CoveragePoints { pub speedtest_multiplier: Decimal, } +/// Entry point into the Coverage Point Calculator. +/// +/// All of the necessary checks for rewardability are done during construction. +/// If you can successfully construct a [RewardableRadio], it can be passed to +/// [calculate_coverage_points]. +pub fn make_rewardable_radio( + radio_type: RadioType, + speedtests: Vec, + location_trust_scores: Vec, + radio_threshold: RadioThreshold, + ranked_coverage: Vec, +) -> Result { + let location_trust_scores = + LocationTrustScores::new(radio_type, location_trust_scores, &ranked_coverage); + + let boosted_hex_status = BoostedHexStatus::new( + &radio_type, + location_trust_scores.multiplier, + &radio_threshold, + ); + + let covered_hexes = CoveredHexes::new(radio_type, ranked_coverage, boosted_hex_status)?; + + Ok(RewardableRadio { + radio_type, + speedtests: Speedtests::new(speedtests), + location_trust_scores, + radio_threshold, + covered_hexes, + boosted_hex_eligibility: boosted_hex_status, + }) +} + +/// This function contains the simplest form of the Coverage Points eqation. pub fn calculate_coverage_points(radio: &RewardableRadio) -> CoveragePoints { let hex_coverage_points = radio.covered_hexes.calculated_coverage_points(); let location_trust_multiplier = radio.location_trust_scores.multiplier; @@ -127,36 +161,6 @@ pub fn calculate_coverage_points(radio: &RewardableRadio) -> CoveragePoints { } } -impl RewardableRadio { - pub fn new( - radio_type: RadioType, - speedtests: Vec, - location_trust_scores: Vec, - radio_threshold: RadioThreshold, - ranked_coverage: Vec, - ) -> Result { - let location_trust_scores = - LocationTrustScores::new(radio_type, location_trust_scores, &ranked_coverage); - - let boosted_hex_status = BoostedHexStatus::new( - &radio_type, - location_trust_scores.multiplier, - &radio_threshold, - ); - - let covered_hexes = CoveredHexes::new(radio_type, ranked_coverage, boosted_hex_status)?; - - Ok(Self { - radio_type, - speedtests: Speedtests::new(speedtests), - location_trust_scores, - radio_threshold, - covered_hexes, - boosted_hex_eligibility: boosted_hex_status, - }) - } -} - #[derive(Debug, Clone, Copy)] pub enum BoostedHexStatus { Eligible, @@ -282,7 +286,7 @@ mod tests { #[test] fn hip_84_radio_meets_minimum_subscriber_threshold_for_boosted_hexes() { let make_wifi = |radio_verified: RadioThreshold| { - RewardableRadio::new( + make_rewardable_radio( RadioType::IndoorWifi, speedtest_maximum(), location_trust_maximum(), @@ -324,7 +328,7 @@ mod tests { #[test] fn hip_93_wifi_with_low_location_score_receives_no_boosted_hexes() { let make_wifi = |location_trust_scores: Vec| { - RewardableRadio::new( + make_rewardable_radio( RadioType::IndoorWifi, speedtest_maximum(), location_trust_scores, @@ -362,7 +366,7 @@ mod tests { #[test] fn speedtest() { let make_indoor_cbrs = |speedtests: Vec| { - RewardableRadio::new( + make_rewardable_radio( RadioType::IndoorCbrs, speedtests, location_trust_maximum(), @@ -450,7 +454,7 @@ mod tests { } use Assignment::*; - let indoor_cbrs = RewardableRadio::new( + let indoor_cbrs = make_rewardable_radio( RadioType::IndoorCbrs, speedtest_maximum(), location_trust_maximum(), @@ -510,7 +514,7 @@ mod tests { #[case] rank: usize, #[case] expected_points: Decimal, ) { - let outdoor_wifi = RewardableRadio::new( + let outdoor_wifi = make_rewardable_radio( radio_type, speedtest_maximum(), location_trust_maximum(), @@ -542,7 +546,7 @@ mod tests { #[case] rank: usize, #[case] expected_points: Decimal, ) { - let indoor_wifi = RewardableRadio::new( + let indoor_wifi = make_rewardable_radio( radio_type, speedtest_maximum(), location_trust_maximum(), @@ -588,7 +592,7 @@ mod tests { #[test] fn location_trust_score_multiplier() { // Location scores are averaged together - let indoor_wifi = RewardableRadio::new( + let indoor_wifi = make_rewardable_radio( RadioType::IndoorWifi, speedtest_maximum(), location_trust_with_scores(&[dec!(0.1), dec!(0.2), dec!(0.3), dec!(0.4)]), @@ -635,7 +639,7 @@ mod tests { boosted: NonZeroU32::new(4), }, ]; - let indoor_wifi = RewardableRadio::new( + let indoor_wifi = make_rewardable_radio( RadioType::IndoorWifi, speedtest_maximum(), location_trust_maximum(), @@ -661,7 +665,7 @@ mod tests { #[case] signal_level: SignalLevel, #[case] expected: Decimal, ) { - let outdoor_cbrs = RewardableRadio::new( + let outdoor_cbrs = make_rewardable_radio( RadioType::OutdoorCbrs, speedtest_maximum(), location_trust_maximum(), @@ -691,7 +695,7 @@ mod tests { #[case] signal_level: SignalLevel, #[case] expected: Decimal, ) { - let indoor_cbrs = RewardableRadio::new( + let indoor_cbrs = make_rewardable_radio( RadioType::IndoorCbrs, speedtest_maximum(), location_trust_maximum(), @@ -723,7 +727,7 @@ mod tests { #[case] signal_level: SignalLevel, #[case] expected: Decimal, ) { - let outdoor_wifi = RewardableRadio::new( + let outdoor_wifi = make_rewardable_radio( RadioType::OutdoorWifi, speedtest_maximum(), location_trust_maximum(), @@ -753,7 +757,7 @@ mod tests { #[case] signal_level: SignalLevel, #[case] expected: Decimal, ) { - let indoor_wifi = RewardableRadio::new( + let indoor_wifi = make_rewardable_radio( RadioType::IndoorWifi, speedtest_maximum(), location_trust_maximum(), diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index 72faf928b..e21b2e919 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -5,8 +5,9 @@ use coverage_map::{RankedCoverage, SignalLevel}; use coverage_point_calculator::{ calculate_coverage_points, location::LocationTrust, + make_rewardable_radio, speedtest::{BytesPs, Speedtest}, - RadioThreshold, RadioType, RewardableRadio, + RadioThreshold, RadioType, }; use hex_assignments::{assignment::HexAssignments, Assignment}; use rust_decimal_macros::dec; @@ -52,7 +53,7 @@ fn base_radio_coverage_points() { (RadioType::OutdoorWifi, dec!(16)), (RadioType::OutdoorCbrs, dec!(4)), ] { - let radio = RewardableRadio::new( + let radio = make_rewardable_radio( radio_type, speedtests.clone(), location_trust_scores.clone(), @@ -114,7 +115,7 @@ fn radios_with_coverage() { (RadioType::OutdoorWifi, 25), (RadioType::OutdoorCbrs, 100), ] { - let radio = RewardableRadio::new( + let radio = make_rewardable_radio( radio_type, default_speedtests.clone(), default_location_trust_scores.clone(), From 81e7e25b64387a75ecaa2140370648b0056855d9 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Thu, 6 Jun 2024 17:51:25 -0700 Subject: [PATCH 097/115] Don't let the comments fall behind --- coverage_point_calculator/src/lib.rs | 29 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 3258d11da..027f7b81d 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -39,7 +39,7 @@ //! - There must be more than 2 speedtests. //! //! - Covered Hexes -//! - If a Radio is not [CoveragePoints::boosted_hex_eligibility], boost values are removed before calculations. [CoveredHexes::new_without_boosts] +//! - If a Radio is not [BoostedHexStatus::Eligible], boost values are removed before calculations. [CoveredHexes::new] //! //! ## References: //! [modeled-coverage]: https://github.com/helium/HIP/blob/main/0074-mobile-poc-modeled-coverage-rewards.md#outdoor-radios @@ -75,7 +75,20 @@ pub enum Error { InvalidSignalLevel(SignalLevel, RadioType), } -/// Necessary checks for calculating coverage points is done during [RewardableRadio::new]. +/// Necessary checks for calculating coverage points is done during +/// [RewardableRadio::new]. +/// +/// The data in this struct may be different from the input data, but +/// it contains the values used for calculating coverage points. +/// +/// - If more than the allowed speedtests were provided, only the speedtests +/// considered are included here. +/// +/// - When a radio covers boosted hexes, [CoveragePoints::location_trust_scores] will contain a +/// trust score _after_ the boosted hex restriction has been applied. +/// +/// - When a radio is not eligible for boosted hex rewards, [CoveragePoints::covered_hexes] will +/// have no boosted_multiplier values. #[derive(Debug, Clone)] pub struct RewardableRadio { pub radio_type: RadioType, @@ -87,18 +100,6 @@ pub struct RewardableRadio { } /// Output of calculating coverage points for a [RewardableRadio]. -/// -/// The only data included was used for calculating coverage points. -/// -/// - If more than the allowed speedtests were provided, only the speedtests -/// considered are included here. -/// -/// - When a radio covers boosted hexes, [CoveragePoints::location_trust_scores] will contain a -/// trust score _after_ the boosted hex restriction has been applied. -/// -/// - When a radio is not eligible for boosted hex rewards, [CoveragePoints::covered_hexes] will -/// have no boosted_multiplier values. -/// #[derive(Debug)] pub struct CoveragePoints { /// Value used when calculating poc_reward From ff65ac81f0ee80daab629ca4b1d0ca5ec49cfaf7 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 7 Jun 2024 14:01:01 -0700 Subject: [PATCH 098/115] move radio_threshold earlier in argument list This puts the information about the radio closer to each other, the grouping feels nicer, I think --- coverage_point_calculator/src/lib.rs | 26 +++++++++---------- .../tests/coverage_point_calculator.rs | 4 +-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 027f7b81d..67d0b6d55 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -119,9 +119,9 @@ pub struct CoveragePoints { /// [calculate_coverage_points]. pub fn make_rewardable_radio( radio_type: RadioType, + radio_threshold: RadioThreshold, speedtests: Vec, location_trust_scores: Vec, - radio_threshold: RadioThreshold, ranked_coverage: Vec, ) -> Result { let location_trust_scores = @@ -289,9 +289,9 @@ mod tests { let make_wifi = |radio_verified: RadioThreshold| { make_rewardable_radio( RadioType::IndoorWifi, + radio_verified, speedtest_maximum(), location_trust_maximum(), - radio_verified, vec![RankedCoverage { hotspot_key: pubkey(), cbsd_id: None, @@ -331,9 +331,9 @@ mod tests { let make_wifi = |location_trust_scores: Vec| { make_rewardable_radio( RadioType::IndoorWifi, + RadioThreshold::Verified, speedtest_maximum(), location_trust_scores, - RadioThreshold::Verified, vec![RankedCoverage { hotspot_key: pubkey(), cbsd_id: None, @@ -369,9 +369,9 @@ mod tests { let make_indoor_cbrs = |speedtests: Vec| { make_rewardable_radio( RadioType::IndoorCbrs, + RadioThreshold::Verified, speedtests, location_trust_maximum(), - RadioThreshold::Verified, vec![RankedCoverage { hotspot_key: pubkey(), cbsd_id: Some("serial".to_string()), @@ -457,9 +457,9 @@ mod tests { use Assignment::*; let indoor_cbrs = make_rewardable_radio( RadioType::IndoorCbrs, + RadioThreshold::Verified, speedtest_maximum(), location_trust_maximum(), - RadioThreshold::Verified, vec![ // yellow - POI ≥ 1 Urbanized ranked_coverage(A, A, A), // 100 @@ -517,9 +517,9 @@ mod tests { ) { let outdoor_wifi = make_rewardable_radio( radio_type, + RadioThreshold::Verified, speedtest_maximum(), location_trust_maximum(), - RadioThreshold::Verified, vec![RankedCoverage { hotspot_key: pubkey(), cbsd_id: None, @@ -549,9 +549,9 @@ mod tests { ) { let indoor_wifi = make_rewardable_radio( radio_type, + RadioThreshold::Verified, speedtest_maximum(), location_trust_maximum(), - RadioThreshold::Verified, vec![ RankedCoverage { hotspot_key: pubkey(), @@ -595,9 +595,9 @@ mod tests { // Location scores are averaged together let indoor_wifi = make_rewardable_radio( RadioType::IndoorWifi, + RadioThreshold::Verified, speedtest_maximum(), location_trust_with_scores(&[dec!(0.1), dec!(0.2), dec!(0.3), dec!(0.4)]), - RadioThreshold::Verified, vec![RankedCoverage { hotspot_key: pubkey(), cbsd_id: None, @@ -642,9 +642,9 @@ mod tests { ]; let indoor_wifi = make_rewardable_radio( RadioType::IndoorWifi, + RadioThreshold::Verified, speedtest_maximum(), location_trust_maximum(), - RadioThreshold::Verified, covered_hexes.clone(), ) .expect("indoor wifi"); @@ -668,9 +668,9 @@ mod tests { ) { let outdoor_cbrs = make_rewardable_radio( RadioType::OutdoorCbrs, + RadioThreshold::Verified, speedtest_maximum(), location_trust_maximum(), - RadioThreshold::Verified, vec![RankedCoverage { hotspot_key: pubkey(), cbsd_id: Some("serial".to_string()), @@ -698,9 +698,9 @@ mod tests { ) { let indoor_cbrs = make_rewardable_radio( RadioType::IndoorCbrs, + RadioThreshold::Verified, speedtest_maximum(), location_trust_maximum(), - RadioThreshold::Verified, vec![RankedCoverage { hotspot_key: pubkey(), cbsd_id: Some("serial".to_string()), @@ -730,9 +730,9 @@ mod tests { ) { let outdoor_wifi = make_rewardable_radio( RadioType::OutdoorWifi, + RadioThreshold::Verified, speedtest_maximum(), location_trust_maximum(), - RadioThreshold::Verified, vec![RankedCoverage { hotspot_key: pubkey(), cbsd_id: Some("serial".to_string()), @@ -760,9 +760,9 @@ mod tests { ) { let indoor_wifi = make_rewardable_radio( RadioType::IndoorWifi, + RadioThreshold::Verified, speedtest_maximum(), location_trust_maximum(), - RadioThreshold::Verified, vec![RankedCoverage { hotspot_key: pubkey(), cbsd_id: None, diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index e21b2e919..55dbb81c8 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -55,9 +55,9 @@ fn base_radio_coverage_points() { ] { let radio = make_rewardable_radio( radio_type, + RadioThreshold::Verified, speedtests.clone(), location_trust_scores.clone(), - RadioThreshold::Verified, hexes.clone(), ) .unwrap(); @@ -117,9 +117,9 @@ fn radios_with_coverage() { ] { let radio = make_rewardable_radio( radio_type, + RadioThreshold::Verified, default_speedtests.clone(), default_location_trust_scores.clone(), - RadioThreshold::Verified, base_hex_iter.clone().take(num_hexes).collect(), ) .unwrap(); From 83b0a212180b92e40d9b95ece3b6221e31330524 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 7 Jun 2024 14:04:10 -0700 Subject: [PATCH 099/115] include all fields in coverage points this is stepping towards having a single struct --- coverage_point_calculator/src/lib.rs | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 67d0b6d55..46a14b8a0 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -59,6 +59,7 @@ use crate::{ speedtest::Speedtest, }; use coverage_map::{RankedCoverage, SignalLevel}; +use hexes::CoveredHex; use rust_decimal::{Decimal, RoundingStrategy}; use rust_decimal_macros::dec; use speedtest::Speedtests; @@ -110,6 +111,18 @@ pub struct CoveragePoints { pub location_trust_multiplier: Decimal, /// Speedtest Mulitplier, maximum of 1 pub speedtest_multiplier: Decimal, + /// Input Radio Type + pub radio_type: RadioType, + /// Input RadioThreshold + pub radio_threshold: RadioThreshold, + /// Derived Eligibility for Boosted Hex Rewards + pub boosted_hex_eligibility: BoostedHexStatus, + /// Speedtests used in calculcation + pub speedtests: Vec, + /// Locaiton Trust Scores used in calculations + pub location_trust_scores: Vec, + /// Covered Hexes used in calculations + pub covered_hexes: Vec, } /// Entry point into the Coverage Point Calculator. @@ -159,6 +172,27 @@ pub fn calculate_coverage_points(radio: &RewardableRadio) -> CoveragePoints { hex_coverage_points, location_trust_multiplier, speedtest_multiplier, + radio_type: radio.radio_type, + radio_threshold: radio.radio_threshold, + boosted_hex_eligibility: radio.boosted_hex_eligibility, + speedtests: radio + .speedtests + .speedtests + .iter() + .map(|s| s.clone()) + .collect(), + location_trust_scores: radio + .location_trust_scores + .trust_scores + .iter() + .map(|s| s.clone()) + .collect(), + covered_hexes: radio + .covered_hexes + .hexes + .iter() + .map(|h| h.clone()) + .collect(), } } From b35f2ae626ce9cc129f7118ce40e3f83bdb923aa Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 7 Jun 2024 14:08:12 -0700 Subject: [PATCH 100/115] provide single function as API to calculator --- coverage_point_calculator/src/lib.rs | 136 +++++++----------- .../tests/coverage_point_calculator.rs | 7 +- 2 files changed, 52 insertions(+), 91 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 46a14b8a0..fb9005e89 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -130,13 +130,13 @@ pub struct CoveragePoints { /// All of the necessary checks for rewardability are done during construction. /// If you can successfully construct a [RewardableRadio], it can be passed to /// [calculate_coverage_points]. -pub fn make_rewardable_radio( +pub fn calculate_coverage_points( radio_type: RadioType, radio_threshold: RadioThreshold, speedtests: Vec, location_trust_scores: Vec, ranked_coverage: Vec, -) -> Result { +) -> Result { let location_trust_scores = LocationTrustScores::new(radio_type, location_trust_scores, &ranked_coverage); @@ -148,18 +148,15 @@ pub fn make_rewardable_radio( let covered_hexes = CoveredHexes::new(radio_type, ranked_coverage, boosted_hex_status)?; - Ok(RewardableRadio { + let radio = RewardableRadio { radio_type, speedtests: Speedtests::new(speedtests), location_trust_scores, radio_threshold, covered_hexes, boosted_hex_eligibility: boosted_hex_status, - }) -} + }; -/// This function contains the simplest form of the Coverage Points eqation. -pub fn calculate_coverage_points(radio: &RewardableRadio) -> CoveragePoints { let hex_coverage_points = radio.covered_hexes.calculated_coverage_points(); let location_trust_multiplier = radio.location_trust_scores.multiplier; let speedtest_multiplier = radio.speedtests.multiplier; @@ -167,7 +164,7 @@ pub fn calculate_coverage_points(radio: &RewardableRadio) -> CoveragePoints { let coverage_points = hex_coverage_points * location_trust_multiplier * speedtest_multiplier; let total_coverage_points = coverage_points.round_dp_with_strategy(2, RoundingStrategy::ToZero); - CoveragePoints { + Ok(CoveragePoints { total_coverage_points, hex_coverage_points, location_trust_multiplier, @@ -193,7 +190,7 @@ pub fn calculate_coverage_points(radio: &RewardableRadio) -> CoveragePoints { .iter() .map(|h| h.clone()) .collect(), - } + }) } #[derive(Debug, Clone, Copy)] @@ -320,8 +317,8 @@ mod tests { #[test] fn hip_84_radio_meets_minimum_subscriber_threshold_for_boosted_hexes() { - let make_wifi = |radio_verified: RadioThreshold| { - make_rewardable_radio( + let calculate_wifi = |radio_verified: RadioThreshold| { + calculate_coverage_points( RadioType::IndoorWifi, radio_verified, speedtest_maximum(), @@ -345,25 +342,19 @@ mod tests { // Radio meeting the threshold is eligible for boosted hexes. // Boosted hex provides radio with more than base_points. - let verified_wifi = make_wifi(RadioThreshold::Verified); - assert_eq!( - base_points * dec!(5), - calculate_coverage_points(&verified_wifi).total_coverage_points - ); + let verified_wifi = calculate_wifi(RadioThreshold::Verified); + assert_eq!(base_points * dec!(5), verified_wifi.total_coverage_points); // Radio not meeting the threshold is not eligible for boosted hexes. // Boost from hex is not applied, radio receives base points. - let unverified_wifi = make_wifi(RadioThreshold::Unverified); - assert_eq!( - base_points, - calculate_coverage_points(&unverified_wifi).total_coverage_points - ); + let unverified_wifi = calculate_wifi(RadioThreshold::Unverified); + assert_eq!(base_points, unverified_wifi.total_coverage_points); } #[test] fn hip_93_wifi_with_low_location_score_receives_no_boosted_hexes() { - let make_wifi = |location_trust_scores: Vec| { - make_rewardable_radio( + let calculate_wifi = |location_trust_scores: Vec| { + calculate_coverage_points( RadioType::IndoorWifi, RadioThreshold::Verified, speedtest_maximum(), @@ -387,21 +378,21 @@ mod tests { // Radio with good trust score is eligible for boosted hexes. // Boosted hex provides radio with more than base_points. - let trusted_wifi = make_wifi(location_trust_with_scores(&[dec!(1), dec!(1)])); - assert!(trusted_wifi.location_trust_scores.multiplier > dec!(0.75)); - assert!(calculate_coverage_points(&trusted_wifi).total_coverage_points > base_points); + let trusted_wifi = calculate_wifi(location_trust_with_scores(&[dec!(1), dec!(1)])); + assert!(trusted_wifi.location_trust_multiplier > dec!(0.75)); + assert!(trusted_wifi.total_coverage_points > base_points); // Radio with poor trust score is not eligible for boosted hexes. // Boost from hex is not applied, and points are further lowered by poor trust score. - let untrusted_wifi = make_wifi(location_trust_with_scores(&[dec!(0.1), dec!(0.2)])); - assert!(untrusted_wifi.location_trust_scores.multiplier < dec!(0.75)); - assert!(calculate_coverage_points(&untrusted_wifi).total_coverage_points < base_points); + let untrusted_wifi = calculate_wifi(location_trust_with_scores(&[dec!(0.1), dec!(0.2)])); + assert!(untrusted_wifi.location_trust_multiplier < dec!(0.75)); + assert!(untrusted_wifi.total_coverage_points < base_points); } #[test] fn speedtest() { - let make_indoor_cbrs = |speedtests: Vec| { - make_rewardable_radio( + let calculate_indoor_cbrs = |speedtests: Vec| { + calculate_coverage_points( RadioType::IndoorCbrs, RadioThreshold::Verified, speedtests, @@ -423,46 +414,46 @@ mod tests { .base_coverage_points(&SignalLevel::High) .unwrap(); - let indoor_cbrs = make_indoor_cbrs(speedtest_maximum()); + let indoor_cbrs = calculate_indoor_cbrs(speedtest_maximum()); assert_eq!( base_coverage_points * SpeedtestTier::Good.multiplier(), - calculate_coverage_points(&indoor_cbrs).total_coverage_points + indoor_cbrs.total_coverage_points ); - let indoor_cbrs = make_indoor_cbrs(vec![ + let indoor_cbrs = calculate_indoor_cbrs(vec![ speedtest_with_download(BytesPs::mbps(88)), speedtest_with_download(BytesPs::mbps(88)), ]); assert_eq!( base_coverage_points * SpeedtestTier::Acceptable.multiplier(), - calculate_coverage_points(&indoor_cbrs).total_coverage_points + indoor_cbrs.total_coverage_points ); - let indoor_cbrs = make_indoor_cbrs(vec![ + let indoor_cbrs = calculate_indoor_cbrs(vec![ speedtest_with_download(BytesPs::mbps(62)), speedtest_with_download(BytesPs::mbps(62)), ]); assert_eq!( base_coverage_points * SpeedtestTier::Degraded.multiplier(), - calculate_coverage_points(&indoor_cbrs).total_coverage_points + indoor_cbrs.total_coverage_points ); - let indoor_cbrs = make_indoor_cbrs(vec![ + let indoor_cbrs = calculate_indoor_cbrs(vec![ speedtest_with_download(BytesPs::mbps(42)), speedtest_with_download(BytesPs::mbps(42)), ]); assert_eq!( base_coverage_points * SpeedtestTier::Poor.multiplier(), - calculate_coverage_points(&indoor_cbrs).total_coverage_points + indoor_cbrs.total_coverage_points ); - let indoor_cbrs = make_indoor_cbrs(vec![ + let indoor_cbrs = calculate_indoor_cbrs(vec![ speedtest_with_download(BytesPs::mbps(25)), speedtest_with_download(BytesPs::mbps(25)), ]); assert_eq!( base_coverage_points * SpeedtestTier::Fail.multiplier(), - calculate_coverage_points(&indoor_cbrs).total_coverage_points + indoor_cbrs.total_coverage_points ); } @@ -489,7 +480,7 @@ mod tests { } use Assignment::*; - let indoor_cbrs = make_rewardable_radio( + let indoor_cbrs = calculate_coverage_points( RadioType::IndoorCbrs, RadioThreshold::Verified, speedtest_maximum(), @@ -533,10 +524,7 @@ mod tests { ) .expect("indoor cbrs"); - assert_eq!( - dec!(1073), - calculate_coverage_points(&indoor_cbrs).total_coverage_points - ); + assert_eq!(dec!(1073), indoor_cbrs.total_coverage_points); } #[rstest] @@ -549,7 +537,7 @@ mod tests { #[case] rank: usize, #[case] expected_points: Decimal, ) { - let outdoor_wifi = make_rewardable_radio( + let outdoor_wifi = calculate_coverage_points( radio_type, RadioThreshold::Verified, speedtest_maximum(), @@ -566,10 +554,7 @@ mod tests { ) .expect("outdoor wifi"); - assert_eq!( - expected_points, - calculate_coverage_points(&outdoor_wifi).total_coverage_points - ); + assert_eq!(expected_points, outdoor_wifi.total_coverage_points); } #[rstest] @@ -581,7 +566,7 @@ mod tests { #[case] rank: usize, #[case] expected_points: Decimal, ) { - let indoor_wifi = make_rewardable_radio( + let indoor_wifi = calculate_coverage_points( radio_type, RadioThreshold::Verified, speedtest_maximum(), @@ -618,16 +603,13 @@ mod tests { ) .expect("indoor wifi"); - assert_eq!( - expected_points, - calculate_coverage_points(&indoor_wifi).total_coverage_points - ); + assert_eq!(expected_points, indoor_wifi.total_coverage_points); } #[test] fn location_trust_score_multiplier() { // Location scores are averaged together - let indoor_wifi = make_rewardable_radio( + let indoor_wifi = calculate_coverage_points( RadioType::IndoorWifi, RadioThreshold::Verified, speedtest_maximum(), @@ -646,10 +628,7 @@ mod tests { // Location trust scores is 1/4 // (0.1 + 0.2 + 0.3 + 0.4) / 4 - assert_eq!( - dec!(100), - calculate_coverage_points(&indoor_wifi).total_coverage_points - ); + assert_eq!(dec!(100), indoor_wifi.total_coverage_points); } #[test] @@ -674,7 +653,7 @@ mod tests { boosted: NonZeroU32::new(4), }, ]; - let indoor_wifi = make_rewardable_radio( + let indoor_wifi = calculate_coverage_points( RadioType::IndoorWifi, RadioThreshold::Verified, speedtest_maximum(), @@ -685,10 +664,7 @@ mod tests { // The hex with a low signal_level is boosted to the same level as a // signal_level of High. - assert_eq!( - dec!(800), - calculate_coverage_points(&indoor_wifi).total_coverage_points - ); + assert_eq!(dec!(800), indoor_wifi.total_coverage_points); } #[rstest] @@ -700,7 +676,7 @@ mod tests { #[case] signal_level: SignalLevel, #[case] expected: Decimal, ) { - let outdoor_cbrs = make_rewardable_radio( + let outdoor_cbrs = calculate_coverage_points( RadioType::OutdoorCbrs, RadioThreshold::Verified, speedtest_maximum(), @@ -717,10 +693,7 @@ mod tests { ) .expect("outdoor cbrs"); - assert_eq!( - expected, - calculate_coverage_points(&outdoor_cbrs).total_coverage_points - ); + assert_eq!(expected, outdoor_cbrs.total_coverage_points); } #[rstest] @@ -730,7 +703,7 @@ mod tests { #[case] signal_level: SignalLevel, #[case] expected: Decimal, ) { - let indoor_cbrs = make_rewardable_radio( + let indoor_cbrs = calculate_coverage_points( RadioType::IndoorCbrs, RadioThreshold::Verified, speedtest_maximum(), @@ -747,10 +720,7 @@ mod tests { ) .expect("indoor cbrs"); - assert_eq!( - expected, - calculate_coverage_points(&indoor_cbrs).total_coverage_points - ); + assert_eq!(expected, indoor_cbrs.total_coverage_points); } #[rstest] @@ -762,7 +732,7 @@ mod tests { #[case] signal_level: SignalLevel, #[case] expected: Decimal, ) { - let outdoor_wifi = make_rewardable_radio( + let outdoor_wifi = calculate_coverage_points( RadioType::OutdoorWifi, RadioThreshold::Verified, speedtest_maximum(), @@ -779,10 +749,7 @@ mod tests { ) .expect("indoor cbrs"); - assert_eq!( - expected, - calculate_coverage_points(&outdoor_wifi).total_coverage_points - ); + assert_eq!(expected, outdoor_wifi.total_coverage_points); } #[rstest] @@ -792,7 +759,7 @@ mod tests { #[case] signal_level: SignalLevel, #[case] expected: Decimal, ) { - let indoor_wifi = make_rewardable_radio( + let indoor_wifi = calculate_coverage_points( RadioType::IndoorWifi, RadioThreshold::Verified, speedtest_maximum(), @@ -809,10 +776,7 @@ mod tests { ) .expect("indoor wifi"); - assert_eq!( - expected, - calculate_coverage_points(&indoor_wifi).total_coverage_points - ); + assert_eq!(expected, indoor_wifi.total_coverage_points); } fn hex_location() -> hextree::Cell { diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index 55dbb81c8..a150716e2 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -5,7 +5,6 @@ use coverage_map::{RankedCoverage, SignalLevel}; use coverage_point_calculator::{ calculate_coverage_points, location::LocationTrust, - make_rewardable_radio, speedtest::{BytesPs, Speedtest}, RadioThreshold, RadioType, }; @@ -53,7 +52,7 @@ fn base_radio_coverage_points() { (RadioType::OutdoorWifi, dec!(16)), (RadioType::OutdoorCbrs, dec!(4)), ] { - let radio = make_rewardable_radio( + let coverage_points = calculate_coverage_points( radio_type, RadioThreshold::Verified, speedtests.clone(), @@ -62,7 +61,6 @@ fn base_radio_coverage_points() { ) .unwrap(); - let coverage_points = calculate_coverage_points(&radio); assert_eq!( expcted_base_coverage_point, coverage_points.total_coverage_points @@ -115,7 +113,7 @@ fn radios_with_coverage() { (RadioType::OutdoorWifi, 25), (RadioType::OutdoorCbrs, 100), ] { - let radio = make_rewardable_radio( + let coverage_points = calculate_coverage_points( radio_type, RadioThreshold::Verified, default_speedtests.clone(), @@ -124,7 +122,6 @@ fn radios_with_coverage() { ) .unwrap(); - let coverage_points = calculate_coverage_points(&radio); assert_eq!(dec!(400), coverage_points.total_coverage_points); } } From 6e8fd68336cd3def99edebaa11a6e5749b2b48e9 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 7 Jun 2024 14:11:11 -0700 Subject: [PATCH 101/115] remove RewardableRadio struct --- coverage_point_calculator/src/lib.rs | 53 ++++++---------------------- 1 file changed, 11 insertions(+), 42 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index fb9005e89..7e134d464 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -76,8 +76,7 @@ pub enum Error { InvalidSignalLevel(SignalLevel, RadioType), } -/// Necessary checks for calculating coverage points is done during -/// [RewardableRadio::new]. +/// Output of calculating coverage points for a Radio. /// /// The data in this struct may be different from the input data, but /// it contains the values used for calculating coverage points. @@ -91,17 +90,6 @@ pub enum Error { /// - When a radio is not eligible for boosted hex rewards, [CoveragePoints::covered_hexes] will /// have no boosted_multiplier values. #[derive(Debug, Clone)] -pub struct RewardableRadio { - pub radio_type: RadioType, - pub radio_threshold: RadioThreshold, - pub boosted_hex_eligibility: BoostedHexStatus, - speedtests: Speedtests, - location_trust_scores: LocationTrustScores, - covered_hexes: CoveredHexes, -} - -/// Output of calculating coverage points for a [RewardableRadio]. -#[derive(Debug)] pub struct CoveragePoints { /// Value used when calculating poc_reward pub total_coverage_points: Decimal, @@ -147,19 +135,11 @@ pub fn calculate_coverage_points( ); let covered_hexes = CoveredHexes::new(radio_type, ranked_coverage, boosted_hex_status)?; + let speedtests = Speedtests::new(speedtests); - let radio = RewardableRadio { - radio_type, - speedtests: Speedtests::new(speedtests), - location_trust_scores, - radio_threshold, - covered_hexes, - boosted_hex_eligibility: boosted_hex_status, - }; - - let hex_coverage_points = radio.covered_hexes.calculated_coverage_points(); - let location_trust_multiplier = radio.location_trust_scores.multiplier; - let speedtest_multiplier = radio.speedtests.multiplier; + let hex_coverage_points = covered_hexes.calculated_coverage_points(); + let location_trust_multiplier = location_trust_scores.multiplier; + let speedtest_multiplier = speedtests.multiplier; let coverage_points = hex_coverage_points * location_trust_multiplier * speedtest_multiplier; let total_coverage_points = coverage_points.round_dp_with_strategy(2, RoundingStrategy::ToZero); @@ -169,27 +149,16 @@ pub fn calculate_coverage_points( hex_coverage_points, location_trust_multiplier, speedtest_multiplier, - radio_type: radio.radio_type, - radio_threshold: radio.radio_threshold, - boosted_hex_eligibility: radio.boosted_hex_eligibility, - speedtests: radio - .speedtests - .speedtests - .iter() - .map(|s| s.clone()) - .collect(), - location_trust_scores: radio - .location_trust_scores + radio_type, + radio_threshold, + boosted_hex_eligibility: boosted_hex_status, + speedtests: speedtests.speedtests.iter().map(|s| s.clone()).collect(), + location_trust_scores: location_trust_scores .trust_scores .iter() .map(|s| s.clone()) .collect(), - covered_hexes: radio - .covered_hexes - .hexes - .iter() - .map(|h| h.clone()) - .collect(), + covered_hexes: covered_hexes.hexes.iter().map(|h| h.clone()).collect(), }) } From 3c696c3613ca2ca11c2ce4bd3995e07ae104377a Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 7 Jun 2024 14:18:54 -0700 Subject: [PATCH 102/115] remove wrapping struct for location trust scores This means we don't need to unwrap anything, and the API is more strictly dealing with data alone. The location funtions are only public to the crate to prevent someone from attempting to put together they're own coverage points calculator. They must go through the calculate_coverage_points function. --- coverage_point_calculator/src/lib.rs | 28 ++---- coverage_point_calculator/src/location.rs | 115 ++++++++-------------- 2 files changed, 48 insertions(+), 95 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 7e134d464..b020c3505 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -53,11 +53,7 @@ //! [mobile-poc-blog]: https://docs.helium.com/mobile/proof-of-coverage //! [boosted-hex-restriction]: https://github.com/helium/oracles/pull/808 //! -use crate::{ - hexes::CoveredHexes, - location::{LocationTrust, LocationTrustScores}, - speedtest::Speedtest, -}; +use crate::{hexes::CoveredHexes, location::LocationTrust, speedtest::Speedtest}; use coverage_map::{RankedCoverage, SignalLevel}; use hexes::CoveredHex; use rust_decimal::{Decimal, RoundingStrategy}; @@ -113,11 +109,6 @@ pub struct CoveragePoints { pub covered_hexes: Vec, } -/// Entry point into the Coverage Point Calculator. -/// -/// All of the necessary checks for rewardability are done during construction. -/// If you can successfully construct a [RewardableRadio], it can be passed to -/// [calculate_coverage_points]. pub fn calculate_coverage_points( radio_type: RadioType, radio_threshold: RadioThreshold, @@ -126,19 +117,16 @@ pub fn calculate_coverage_points( ranked_coverage: Vec, ) -> Result { let location_trust_scores = - LocationTrustScores::new(radio_type, location_trust_scores, &ranked_coverage); + location::clean_trust_scores(location_trust_scores, &ranked_coverage); + let location_trust_multiplier = location::multiplier(radio_type, &location_trust_scores); - let boosted_hex_status = BoostedHexStatus::new( - &radio_type, - location_trust_scores.multiplier, - &radio_threshold, - ); + let boosted_hex_status = + BoostedHexStatus::new(&radio_type, location_trust_multiplier, &radio_threshold); let covered_hexes = CoveredHexes::new(radio_type, ranked_coverage, boosted_hex_status)?; let speedtests = Speedtests::new(speedtests); let hex_coverage_points = covered_hexes.calculated_coverage_points(); - let location_trust_multiplier = location_trust_scores.multiplier; let speedtest_multiplier = speedtests.multiplier; let coverage_points = hex_coverage_points * location_trust_multiplier * speedtest_multiplier; @@ -153,11 +141,7 @@ pub fn calculate_coverage_points( radio_threshold, boosted_hex_eligibility: boosted_hex_status, speedtests: speedtests.speedtests.iter().map(|s| s.clone()).collect(), - location_trust_scores: location_trust_scores - .trust_scores - .iter() - .map(|s| s.clone()) - .collect(), + location_trust_scores, covered_hexes: covered_hexes.hexes.iter().map(|h| h.clone()).collect(), }) } diff --git a/coverage_point_calculator/src/location.rs b/coverage_point_calculator/src/location.rs index 5d0182a93..a75ef88eb 100644 --- a/coverage_point_calculator/src/location.rs +++ b/coverage_point_calculator/src/location.rs @@ -11,48 +11,40 @@ const RESTRICTIVE_MAX_DISTANCE: Meters = 50; type Meters = u32; -#[derive(Debug, Clone, PartialEq)] -pub struct LocationTrustScores { - pub multiplier: Decimal, - pub trust_scores: Vec, -} - #[derive(Debug, Clone, PartialEq)] pub struct LocationTrust { pub meters_to_asserted: Meters, pub trust_score: Decimal, } -impl LocationTrustScores { - pub fn new( - radio_type: RadioType, - trust_scores: Vec, - ranked_coverage: &[RankedCoverage], - ) -> Self { - let any_boosted_hexes = ranked_coverage.iter().any(|hex| hex.boosted.is_some()); - - let cleaned_scores = if any_boosted_hexes { - trust_scores - .into_iter() - .map(LocationTrust::into_boosted) - .collect() - } else { - trust_scores - }; - - // CBRS radios are always trusted because they have internal GPS - let multiplier = if radio_type.is_cbrs() { - dec!(1) - } else { - multiplier(&cleaned_scores) - }; +pub(crate) fn clean_trust_scores( + trust_scores: Vec, + ranked_coverage: &[RankedCoverage], +) -> Vec { + let any_boosted_hexes = ranked_coverage.iter().any(|hex| hex.boosted.is_some()); + + if any_boosted_hexes { + trust_scores + .into_iter() + .map(LocationTrust::into_boosted) + .collect() + } else { + trust_scores + } +} - Self { - multiplier, - trust_scores: cleaned_scores, - } +pub(crate) fn multiplier(radio_type: RadioType, trust_scores: &[LocationTrust]) -> Decimal { + // CBRS radios are always trusted because they have internal GPS + if radio_type.is_cbrs() { + return dec!(1); } + + let count = Decimal::from(trust_scores.len()); + let scores: Decimal = trust_scores.iter().map(|l| l.trust_score).sum(); + + scores / count } + impl LocationTrust { fn into_boosted(self) -> Self { // Cap multipliers to 0.25x when a radio covers _any_ boosted hex @@ -70,13 +62,6 @@ impl LocationTrust { } } -fn multiplier(trust_scores: &[LocationTrust]) -> Decimal { - let count = Decimal::from(trust_scores.len()); - let scores: Decimal = trust_scores.iter().map(|l| l.trust_score).sum(); - - scores / count -} - #[cfg(test)] mod tests { use std::num::NonZeroU32; @@ -98,15 +83,11 @@ mod tests { trust_score: dec!(0.5), }, ]; - let boosted = LocationTrustScores::new( - RadioType::IndoorWifi, - trust_scores.clone(), - &boosted_ranked_coverage(), - ); - let unboosted = LocationTrustScores::new(RadioType::IndoorWifi, trust_scores, &[]); - - assert_eq!(dec!(0.5), boosted.multiplier); - assert_eq!(dec!(0.5), unboosted.multiplier); + let boosted = clean_trust_scores(trust_scores.clone(), &boosted_ranked_coverage()); + let unboosted = clean_trust_scores(trust_scores, &[]); + + assert_eq!(dec!(0.5), multiplier(RadioType::IndoorWifi, &boosted)); + assert_eq!(dec!(0.5), multiplier(RadioType::IndoorWifi, &unboosted)); } #[test] @@ -122,15 +103,11 @@ mod tests { }, ]; - let boosted = LocationTrustScores::new( - RadioType::IndoorWifi, - trust_scores.clone(), - &boosted_ranked_coverage(), - ); - let unboosted = LocationTrustScores::new(RadioType::IndoorWifi, trust_scores, &[]); + let boosted = clean_trust_scores(trust_scores.clone(), &boosted_ranked_coverage()); + let unboosted = clean_trust_scores(trust_scores, &[]); - assert_eq!(dec!(0.25), boosted.multiplier); - assert_eq!(dec!(0.5), unboosted.multiplier); + assert_eq!(dec!(0.25), multiplier(RadioType::IndoorWifi, &boosted)); + assert_eq!(dec!(0.5), multiplier(RadioType::IndoorWifi, &unboosted)); } #[test] @@ -146,18 +123,14 @@ mod tests { }, ]; - let boosted = LocationTrustScores::new( - RadioType::IndoorWifi, - trust_scores.clone(), - &boosted_ranked_coverage(), - ); - let unboosted = LocationTrustScores::new(RadioType::IndoorWifi, trust_scores, &[]); + let boosted = clean_trust_scores(trust_scores.clone(), &boosted_ranked_coverage()); + let unboosted = clean_trust_scores(trust_scores, &[]); // location past distance limit trust score is degraded let degraded_mult = (dec!(0.5) + dec!(0.25)) / dec!(2); - assert_eq!(degraded_mult, boosted.multiplier); + assert_eq!(degraded_mult, multiplier(RadioType::IndoorWifi, &boosted)); // location past distance limit trust score is untouched - assert_eq!(dec!(0.5), unboosted.multiplier); + assert_eq!(dec!(0.5), multiplier(RadioType::IndoorWifi, &unboosted)); } #[test] @@ -170,15 +143,11 @@ mod tests { trust_score: dec!(0), }]; - let boosted = LocationTrustScores::new( - RadioType::IndoorCbrs, - trust_scores.clone(), - &boosted_ranked_coverage(), - ); - let unboosted = LocationTrustScores::new(RadioType::IndoorCbrs, trust_scores, &[]); + let boosted = clean_trust_scores(trust_scores.clone(), &boosted_ranked_coverage()); + let unboosted = clean_trust_scores(trust_scores, &[]); - assert_eq!(dec!(1), boosted.multiplier); - assert_eq!(dec!(1), unboosted.multiplier); + assert_eq!(dec!(1), multiplier(RadioType::IndoorCbrs, &boosted)); + assert_eq!(dec!(1), multiplier(RadioType::IndoorCbrs, &unboosted)); } fn boosted_ranked_coverage() -> Vec { From 5991da7564ca0b532244c0a477dd6fd2cbc9a5ff Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 7 Jun 2024 14:23:51 -0700 Subject: [PATCH 103/115] remove wrapping CoveredHexes struct Same as location, we want to deal more directly with data, while keeping the only way to use the crate be through the top level calculate_coverage_points function. --- coverage_point_calculator/src/hexes.rs | 101 +++++++++++-------------- coverage_point_calculator/src/lib.rs | 10 ++- 2 files changed, 52 insertions(+), 59 deletions(-) diff --git a/coverage_point_calculator/src/hexes.rs b/coverage_point_calculator/src/hexes.rs index 2170edd4d..9c9d23afc 100644 --- a/coverage_point_calculator/src/hexes.rs +++ b/coverage_point_calculator/src/hexes.rs @@ -5,11 +5,6 @@ use rust_decimal_macros::dec; use crate::{BoostedHexStatus, RadioType, Result}; -#[derive(Debug, Clone)] -pub struct CoveredHexes { - pub hexes: Vec, -} - #[derive(Debug, Clone)] pub struct CoveredHex { pub hex: hextree::Cell, @@ -28,60 +23,56 @@ pub struct CoveredHex { pub boosted_multiplier: Option, } -impl CoveredHexes { - pub fn new( - radio_type: RadioType, - ranked_coverage: Vec, - boosted_hex_status: BoostedHexStatus, - ) -> Result { - let ranked_coverage = if !boosted_hex_status.is_eligible() { - ranked_coverage - .into_iter() - .map(|ranked| RankedCoverage { - boosted: None, - ..ranked - }) - .collect() - } else { - ranked_coverage - }; - - // verify all hexes can obtain a base coverage point - let covered_hexes = ranked_coverage +pub(crate) fn clean_covered_hexes( + radio_type: RadioType, + ranked_coverage: Vec, + boosted_hex_status: BoostedHexStatus, +) -> Result> { + let ranked_coverage = if !boosted_hex_status.is_eligible() { + ranked_coverage .into_iter() - .map(|ranked| { - let base_coverage_points = radio_type.base_coverage_points(&ranked.signal_level)?; - let rank_multiplier = radio_type.rank_multiplier(ranked.rank); - let assignment_multiplier = ranked.assignments.boosting_multiplier(); - let boosted_multiplier = ranked.boosted.map(|boost| boost.get()).map(Decimal::from); + .map(|ranked| RankedCoverage { + boosted: None, + ..ranked + }) + .collect() + } else { + ranked_coverage + }; - let calculated_coverage_points = base_coverage_points - * assignment_multiplier - * rank_multiplier - * boosted_multiplier.unwrap_or(dec!(1)); + // verify all hexes can obtain a base coverage point + let covered_hexes = ranked_coverage + .into_iter() + .map(|ranked| { + let base_coverage_points = radio_type.base_coverage_points(&ranked.signal_level)?; + let rank_multiplier = radio_type.rank_multiplier(ranked.rank); + let assignment_multiplier = ranked.assignments.boosting_multiplier(); + let boosted_multiplier = ranked.boosted.map(|boost| boost.get()).map(Decimal::from); - Ok(CoveredHex { - hex: ranked.hex, - base_coverage_points, - calculated_coverage_points, - assignments: ranked.assignments, - assignment_multiplier, - rank: ranked.rank, - rank_multiplier, - boosted_multiplier, - }) - }) - .collect::>>()?; + let calculated_coverage_points = base_coverage_points + * assignment_multiplier + * rank_multiplier + * boosted_multiplier.unwrap_or(dec!(1)); - Ok(Self { - hexes: covered_hexes, + Ok(CoveredHex { + hex: ranked.hex, + base_coverage_points, + calculated_coverage_points, + assignments: ranked.assignments, + assignment_multiplier, + rank: ranked.rank, + rank_multiplier, + boosted_multiplier, + }) }) - } + .collect::>>()?; + + Ok(covered_hexes) +} - pub fn calculated_coverage_points(&self) -> Decimal { - self.hexes - .iter() - .map(|hex| hex.calculated_coverage_points) - .sum() - } +pub(crate) fn calculated_coverage_points(covered_hexes: &[CoveredHex]) -> Decimal { + covered_hexes + .iter() + .map(|hex| hex.calculated_coverage_points) + .sum() } diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index b020c3505..aa7a0b76f 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -53,7 +53,7 @@ //! [mobile-poc-blog]: https://docs.helium.com/mobile/proof-of-coverage //! [boosted-hex-restriction]: https://github.com/helium/oracles/pull/808 //! -use crate::{hexes::CoveredHexes, location::LocationTrust, speedtest::Speedtest}; +use crate::{location::LocationTrust, speedtest::Speedtest}; use coverage_map::{RankedCoverage, SignalLevel}; use hexes::CoveredHex; use rust_decimal::{Decimal, RoundingStrategy}; @@ -123,10 +123,12 @@ pub fn calculate_coverage_points( let boosted_hex_status = BoostedHexStatus::new(&radio_type, location_trust_multiplier, &radio_threshold); - let covered_hexes = CoveredHexes::new(radio_type, ranked_coverage, boosted_hex_status)?; + let covered_hexes = + hexes::clean_covered_hexes(radio_type, ranked_coverage, boosted_hex_status)?; + let hex_coverage_points = hexes::calculated_coverage_points(&covered_hexes); + let speedtests = Speedtests::new(speedtests); - let hex_coverage_points = covered_hexes.calculated_coverage_points(); let speedtest_multiplier = speedtests.multiplier; let coverage_points = hex_coverage_points * location_trust_multiplier * speedtest_multiplier; @@ -142,7 +144,7 @@ pub fn calculate_coverage_points( boosted_hex_eligibility: boosted_hex_status, speedtests: speedtests.speedtests.iter().map(|s| s.clone()).collect(), location_trust_scores, - covered_hexes: covered_hexes.hexes.iter().map(|h| h.clone()).collect(), + covered_hexes, }) } From bfa23d222407402f01a5da25d4ef55b6baabf64a Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 7 Jun 2024 14:48:10 -0700 Subject: [PATCH 104/115] remove wrapping struct from speedtests dealing more directly with data. forcing use through the top level api. --- coverage_point_calculator/src/lib.rs | 40 +++++++------- coverage_point_calculator/src/speedtest.rs | 53 +++++++------------ .../tests/coverage_point_calculator.rs | 5 +- 3 files changed, 38 insertions(+), 60 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index aa7a0b76f..fc2a11747 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -53,16 +53,18 @@ //! [mobile-poc-blog]: https://docs.helium.com/mobile/proof-of-coverage //! [boosted-hex-restriction]: https://github.com/helium/oracles/pull/808 //! -use crate::{location::LocationTrust, speedtest::Speedtest}; -use coverage_map::{RankedCoverage, SignalLevel}; -use hexes::CoveredHex; +pub use crate::{ + hexes::CoveredHex, + location::LocationTrust, + speedtest::{BytesPs, Speedtest}, +}; +use coverage_map::SignalLevel; use rust_decimal::{Decimal, RoundingStrategy}; use rust_decimal_macros::dec; -use speedtest::Speedtests; -pub mod hexes; -pub mod location; -pub mod speedtest; +mod hexes; +mod location; +mod speedtest; pub type Result = std::result::Result; @@ -113,23 +115,20 @@ pub fn calculate_coverage_points( radio_type: RadioType, radio_threshold: RadioThreshold, speedtests: Vec, - location_trust_scores: Vec, - ranked_coverage: Vec, + trust_scores: Vec, + ranked_coverage: Vec, ) -> Result { - let location_trust_scores = - location::clean_trust_scores(location_trust_scores, &ranked_coverage); + let location_trust_scores = location::clean_trust_scores(trust_scores, &ranked_coverage); let location_trust_multiplier = location::multiplier(radio_type, &location_trust_scores); - let boosted_hex_status = + let boost_eligibility = BoostedHexStatus::new(&radio_type, location_trust_multiplier, &radio_threshold); - let covered_hexes = - hexes::clean_covered_hexes(radio_type, ranked_coverage, boosted_hex_status)?; + let covered_hexes = hexes::clean_covered_hexes(radio_type, ranked_coverage, boost_eligibility)?; let hex_coverage_points = hexes::calculated_coverage_points(&covered_hexes); - let speedtests = Speedtests::new(speedtests); - - let speedtest_multiplier = speedtests.multiplier; + let speedtests = speedtest::clean_speedtests(speedtests); + let speedtest_multiplier = speedtest::multiplier(&speedtests); let coverage_points = hex_coverage_points * location_trust_multiplier * speedtest_multiplier; let total_coverage_points = coverage_points.round_dp_with_strategy(2, RoundingStrategy::ToZero); @@ -141,8 +140,8 @@ pub fn calculate_coverage_points( speedtest_multiplier, radio_type, radio_threshold, - boosted_hex_eligibility: boosted_hex_status, - speedtests: speedtests.speedtests.iter().map(|s| s.clone()).collect(), + boosted_hex_eligibility: boost_eligibility, + speedtests, location_trust_scores, covered_hexes, }) @@ -263,10 +262,9 @@ mod tests { use std::num::NonZeroU32; - use crate::speedtest::BytesPs; - use super::*; use chrono::Utc; + use coverage_map::RankedCoverage; use hex_assignments::{assignment::HexAssignments, Assignment}; use rust_decimal_macros::dec; diff --git a/coverage_point_calculator/src/speedtest.rs b/coverage_point_calculator/src/speedtest.rs index eb607e864..cc586225a 100644 --- a/coverage_point_calculator/src/speedtest.rs +++ b/coverage_point_calculator/src/speedtest.rs @@ -24,38 +24,21 @@ impl BytesPs { } } -#[derive(Debug, Clone)] -pub struct Speedtests { - pub multiplier: Decimal, - pub speedtests: Vec, +pub(crate) fn clean_speedtests(speedtests: Vec) -> Vec { + let mut cleaned = speedtests; + // sort newest to oldest + cleaned.sort_by_key(|test| std::cmp::Reverse(test.timestamp)); + cleaned.truncate(MAX_ALLOWED_SPEEDTEST_SAMPLES); + cleaned } -impl Speedtests { - pub fn new(speedtests: Vec) -> Self { - // sort Newest to Oldest - let mut sorted_speedtests = speedtests; - sorted_speedtests.sort_by_key(|test| std::cmp::Reverse(test.timestamp)); - - let sorted_speedtests: Vec<_> = sorted_speedtests - .into_iter() - .take(MAX_ALLOWED_SPEEDTEST_SAMPLES) - .collect(); - - let multiplier = if sorted_speedtests.len() < MIN_REQUIRED_SPEEDTEST_SAMPLES { - SpeedtestTier::Fail.multiplier() - } else { - Speedtest::avg(&sorted_speedtests).multiplier() - }; - - Self { - multiplier, - speedtests: sorted_speedtests, - } +pub(crate) fn multiplier(speedtests: &[Speedtest]) -> Decimal { + if speedtests.len() < MIN_REQUIRED_SPEEDTEST_SAMPLES { + return dec!(0); } - pub fn avg(&self) -> Speedtest { - Speedtest::avg(&self.speedtests) - } + let avg = Speedtest::avg(speedtests); + avg.multiplier() } #[derive(Debug, Default, Clone, Copy, PartialEq)] @@ -185,15 +168,15 @@ mod tests { latency_millis: 15, timestamp: Utc::now(), }; - let speedtests = |num: usize| std::iter::repeat(speedtest).take(num).collect(); + let speedtests = |num: usize| std::iter::repeat(speedtest).take(num).collect::>(); assert_eq!( dec!(0), - Speedtests::new(speedtests(MIN_REQUIRED_SPEEDTEST_SAMPLES - 1)).multiplier + multiplier(&speedtests(MIN_REQUIRED_SPEEDTEST_SAMPLES - 1)) ); assert_eq!( dec!(1), - Speedtests::new(speedtests(MIN_REQUIRED_SPEEDTEST_SAMPLES)).multiplier + multiplier(&speedtests(MIN_REQUIRED_SPEEDTEST_SAMPLES)) ); } @@ -206,9 +189,9 @@ mod tests { timestamp: Utc::now(), }; let speedtests = std::iter::repeat(base).take(10).collect(); - let speedtests = Speedtests::new(speedtests); + let speedtests = clean_speedtests(speedtests); - assert_eq!(MAX_ALLOWED_SPEEDTEST_SAMPLES, speedtests.speedtests.len()); + assert_eq!(MAX_ALLOWED_SPEEDTEST_SAMPLES, speedtests.len()); } #[test] @@ -223,7 +206,7 @@ mod tests { // Intersperse new and old speedtests. // new speedtests have 1.0 multipliers // old speedtests have 0.0 multipliers - let speedtests = Speedtests::new(vec![ + let speedtests = clean_speedtests(vec![ make_speedtest(date(2024, 4, 6), 15), make_speedtest(date(2022, 4, 6), 999), // -- @@ -244,7 +227,7 @@ mod tests { ]); // Old speedtests should be unused - assert_eq!(dec!(1), speedtests.multiplier); + assert_eq!(dec!(1), multiplier(&speedtests)); } fn date(year: i32, month: u32, day: u32) -> DateTime { diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index a150716e2..366c0c2ef 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -3,10 +3,7 @@ use std::num::NonZeroU32; use chrono::Utc; use coverage_map::{RankedCoverage, SignalLevel}; use coverage_point_calculator::{ - calculate_coverage_points, - location::LocationTrust, - speedtest::{BytesPs, Speedtest}, - RadioThreshold, RadioType, + calculate_coverage_points, BytesPs, LocationTrust, RadioThreshold, RadioType, Speedtest, }; use hex_assignments::{assignment::HexAssignments, Assignment}; use rust_decimal_macros::dec; From 3da915f889e2056e61986935a2c4829c18ef871b Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 7 Jun 2024 14:48:44 -0700 Subject: [PATCH 105/115] add more links to docs --- coverage_point_calculator/src/lib.rs | 23 +++++++++++----------- coverage_point_calculator/src/speedtest.rs | 1 + 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index fc2a11747..ef515a23d 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -4,44 +4,43 @@ //! thorough explanation of many of them. It is not exhaustive, but a great //! place to start. //! -//! ## Fields: -//! - modeled_coverage_points +//! ## Important Fields +//! - [CoveredHex::base_coverage_points] //! - [HIP-74][modeled-coverage] //! - reduced cbrs radio coverage points [HIP-113][cbrs-experimental] //! -//! - assignment_multiplier +//! - [CoveredHex::assignment_multiplier] //! - [HIP-103][oracle-boosting] //! -//! - rank +//! - [CoveredHex::rank] //! - [HIP-105][hex-limits] //! -//! - hex_boost_multiplier +//! - [CoveredHex::boosted_multiplier] //! - must meet minimum subscriber thresholds [HIP-84][provider-boosting] //! - Wifi Location trust score >0.75 for boosted hex eligibility [HIP-93][wifi-aps] //! -//! - location_trust_score_multiplier +//! - [CoveragePoints::location_trust_multiplier] //! - [HIP-98][qos-score] //! - states 30m requirement for boosted hexes [HIP-107][prevent-gaming] //! - increase Boosted hex restriction, 30m -> 50m [Pull Request][boosted-hex-restriction] //! -//! - speedtest_multiplier +//! - [CoveragePoints::speedtest_multiplier] //! - [HIP-74][modeled-coverage] //! - added "Good" speedtest tier [HIP-98][qos-score] //! - latency is explicitly under limit in HIP //! //! ## Notable Conditions: -//! - Location +//! - [LocationTrust] //! - If a Radio covers any boosted hexes, [LocationTrust] scores must meet distance requirements, or be degraded. //! - CBRS Radio's location is always trusted because of GPS. //! -//! - Speedtests +//! - [Speedtest] //! - The latest 6 speedtests will be used. //! - There must be more than 2 speedtests. //! -//! - Covered Hexes -//! - If a Radio is not [BoostedHexStatus::Eligible], boost values are removed before calculations. [CoveredHexes::new] +//! - [CoveredHex] +//! - If a Radio is not [BoostedHexStatus::Eligible], boost values are removed before calculations. //! -//! ## References: //! [modeled-coverage]: https://github.com/helium/HIP/blob/main/0074-mobile-poc-modeled-coverage-rewards.md#outdoor-radios //! [provider-boosting]: https://github.com/helium/HIP/blob/main/0084-service-provider-hex-boosting.md //! [wifi-aps]: https://github.com/helium/HIP/blob/main/0093-addition-of-wifi-aps-to-mobile-subdao.md diff --git a/coverage_point_calculator/src/speedtest.rs b/coverage_point_calculator/src/speedtest.rs index cc586225a..20ff711b9 100644 --- a/coverage_point_calculator/src/speedtest.rs +++ b/coverage_point_calculator/src/speedtest.rs @@ -7,6 +7,7 @@ const MAX_ALLOWED_SPEEDTEST_SAMPLES: usize = 6; type Millis = u32; +/// Bytes per second #[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)] pub struct BytesPs(u64); From 958b2d11cbafd4b2265d8ffae5f7cf661c5de8ac Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 7 Jun 2024 14:51:50 -0700 Subject: [PATCH 106/115] move calcuating coverage points into constructor I thought `coverage_point_calculator::calculate_coverage_points()` didn't read very well. And since we now have a single struct to care about (outside of providing arguments), it seemed to me `coverage_point_calculator::CoveragePoints::new()` read rather nicely. --- coverage_point_calculator/src/lib.rs | 97 ++++++++++--------- .../tests/coverage_point_calculator.rs | 6 +- 2 files changed, 54 insertions(+), 49 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index ef515a23d..f8fea1d85 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -110,40 +110,45 @@ pub struct CoveragePoints { pub covered_hexes: Vec, } -pub fn calculate_coverage_points( - radio_type: RadioType, - radio_threshold: RadioThreshold, - speedtests: Vec, - trust_scores: Vec, - ranked_coverage: Vec, -) -> Result { - let location_trust_scores = location::clean_trust_scores(trust_scores, &ranked_coverage); - let location_trust_multiplier = location::multiplier(radio_type, &location_trust_scores); - - let boost_eligibility = - BoostedHexStatus::new(&radio_type, location_trust_multiplier, &radio_threshold); - - let covered_hexes = hexes::clean_covered_hexes(radio_type, ranked_coverage, boost_eligibility)?; - let hex_coverage_points = hexes::calculated_coverage_points(&covered_hexes); - - let speedtests = speedtest::clean_speedtests(speedtests); - let speedtest_multiplier = speedtest::multiplier(&speedtests); - - let coverage_points = hex_coverage_points * location_trust_multiplier * speedtest_multiplier; - let total_coverage_points = coverage_points.round_dp_with_strategy(2, RoundingStrategy::ToZero); - - Ok(CoveragePoints { - total_coverage_points, - hex_coverage_points, - location_trust_multiplier, - speedtest_multiplier, - radio_type, - radio_threshold, - boosted_hex_eligibility: boost_eligibility, - speedtests, - location_trust_scores, - covered_hexes, - }) +impl CoveragePoints { + pub fn new( + radio_type: RadioType, + radio_threshold: RadioThreshold, + speedtests: Vec, + trust_scores: Vec, + ranked_coverage: Vec, + ) -> Result { + let location_trust_scores = location::clean_trust_scores(trust_scores, &ranked_coverage); + let location_trust_multiplier = location::multiplier(radio_type, &location_trust_scores); + + let boost_eligibility = + BoostedHexStatus::new(&radio_type, location_trust_multiplier, &radio_threshold); + + let covered_hexes = + hexes::clean_covered_hexes(radio_type, ranked_coverage, boost_eligibility)?; + let hex_coverage_points = hexes::calculated_coverage_points(&covered_hexes); + + let speedtests = speedtest::clean_speedtests(speedtests); + let speedtest_multiplier = speedtest::multiplier(&speedtests); + + let coverage_points = + hex_coverage_points * location_trust_multiplier * speedtest_multiplier; + let total_coverage_points = + coverage_points.round_dp_with_strategy(2, RoundingStrategy::ToZero); + + Ok(CoveragePoints { + total_coverage_points, + hex_coverage_points, + location_trust_multiplier, + speedtest_multiplier, + radio_type, + radio_threshold, + boosted_hex_eligibility: boost_eligibility, + speedtests, + location_trust_scores, + covered_hexes, + }) + } } #[derive(Debug, Clone, Copy)] @@ -270,7 +275,7 @@ mod tests { #[test] fn hip_84_radio_meets_minimum_subscriber_threshold_for_boosted_hexes() { let calculate_wifi = |radio_verified: RadioThreshold| { - calculate_coverage_points( + CoveragePoints::new( RadioType::IndoorWifi, radio_verified, speedtest_maximum(), @@ -306,7 +311,7 @@ mod tests { #[test] fn hip_93_wifi_with_low_location_score_receives_no_boosted_hexes() { let calculate_wifi = |location_trust_scores: Vec| { - calculate_coverage_points( + CoveragePoints::new( RadioType::IndoorWifi, RadioThreshold::Verified, speedtest_maximum(), @@ -344,7 +349,7 @@ mod tests { #[test] fn speedtest() { let calculate_indoor_cbrs = |speedtests: Vec| { - calculate_coverage_points( + CoveragePoints::new( RadioType::IndoorCbrs, RadioThreshold::Verified, speedtests, @@ -432,7 +437,7 @@ mod tests { } use Assignment::*; - let indoor_cbrs = calculate_coverage_points( + let indoor_cbrs = CoveragePoints::new( RadioType::IndoorCbrs, RadioThreshold::Verified, speedtest_maximum(), @@ -489,7 +494,7 @@ mod tests { #[case] rank: usize, #[case] expected_points: Decimal, ) { - let outdoor_wifi = calculate_coverage_points( + let outdoor_wifi = CoveragePoints::new( radio_type, RadioThreshold::Verified, speedtest_maximum(), @@ -518,7 +523,7 @@ mod tests { #[case] rank: usize, #[case] expected_points: Decimal, ) { - let indoor_wifi = calculate_coverage_points( + let indoor_wifi = CoveragePoints::new( radio_type, RadioThreshold::Verified, speedtest_maximum(), @@ -561,7 +566,7 @@ mod tests { #[test] fn location_trust_score_multiplier() { // Location scores are averaged together - let indoor_wifi = calculate_coverage_points( + let indoor_wifi = CoveragePoints::new( RadioType::IndoorWifi, RadioThreshold::Verified, speedtest_maximum(), @@ -605,7 +610,7 @@ mod tests { boosted: NonZeroU32::new(4), }, ]; - let indoor_wifi = calculate_coverage_points( + let indoor_wifi = CoveragePoints::new( RadioType::IndoorWifi, RadioThreshold::Verified, speedtest_maximum(), @@ -628,7 +633,7 @@ mod tests { #[case] signal_level: SignalLevel, #[case] expected: Decimal, ) { - let outdoor_cbrs = calculate_coverage_points( + let outdoor_cbrs = CoveragePoints::new( RadioType::OutdoorCbrs, RadioThreshold::Verified, speedtest_maximum(), @@ -655,7 +660,7 @@ mod tests { #[case] signal_level: SignalLevel, #[case] expected: Decimal, ) { - let indoor_cbrs = calculate_coverage_points( + let indoor_cbrs = CoveragePoints::new( RadioType::IndoorCbrs, RadioThreshold::Verified, speedtest_maximum(), @@ -684,7 +689,7 @@ mod tests { #[case] signal_level: SignalLevel, #[case] expected: Decimal, ) { - let outdoor_wifi = calculate_coverage_points( + let outdoor_wifi = CoveragePoints::new( RadioType::OutdoorWifi, RadioThreshold::Verified, speedtest_maximum(), @@ -711,7 +716,7 @@ mod tests { #[case] signal_level: SignalLevel, #[case] expected: Decimal, ) { - let indoor_wifi = calculate_coverage_points( + let indoor_wifi = CoveragePoints::new( RadioType::IndoorWifi, RadioThreshold::Verified, speedtest_maximum(), diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index 366c0c2ef..fdc4d0be9 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -3,7 +3,7 @@ use std::num::NonZeroU32; use chrono::Utc; use coverage_map::{RankedCoverage, SignalLevel}; use coverage_point_calculator::{ - calculate_coverage_points, BytesPs, LocationTrust, RadioThreshold, RadioType, Speedtest, + BytesPs, CoveragePoints, LocationTrust, RadioThreshold, RadioType, Speedtest, }; use hex_assignments::{assignment::HexAssignments, Assignment}; use rust_decimal_macros::dec; @@ -49,7 +49,7 @@ fn base_radio_coverage_points() { (RadioType::OutdoorWifi, dec!(16)), (RadioType::OutdoorCbrs, dec!(4)), ] { - let coverage_points = calculate_coverage_points( + let coverage_points = CoveragePoints::new( radio_type, RadioThreshold::Verified, speedtests.clone(), @@ -110,7 +110,7 @@ fn radios_with_coverage() { (RadioType::OutdoorWifi, 25), (RadioType::OutdoorCbrs, 100), ] { - let coverage_points = calculate_coverage_points( + let coverage_points = CoveragePoints::new( radio_type, RadioThreshold::Verified, default_speedtests.clone(), From dc02abc95c3dcb68f491eee731ff628d0aef3780 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Fri, 7 Jun 2024 14:56:21 -0700 Subject: [PATCH 107/115] Removing derive Default for Speedtest I don't think a zeroed out Speedtest is a sensible default --- coverage_point_calculator/src/speedtest.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coverage_point_calculator/src/speedtest.rs b/coverage_point_calculator/src/speedtest.rs index 20ff711b9..779cf9d7a 100644 --- a/coverage_point_calculator/src/speedtest.rs +++ b/coverage_point_calculator/src/speedtest.rs @@ -42,7 +42,7 @@ pub(crate) fn multiplier(speedtests: &[Speedtest]) -> Decimal { avg.multiplier() } -#[derive(Debug, Default, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Speedtest { pub upload_speed: BytesPs, pub download_speed: BytesPs, From 75e3d4bc60c5a854303bcc4f049354822078e8ae Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Mon, 10 Jun 2024 15:06:25 -0700 Subject: [PATCH 108/115] Fix incorrect speedtests conversion I had typed the wrong value when bringing over megabytes per second conversion. We found some values in a real database during testing that should have gotten a different speedtest tier. --- coverage_point_calculator/src/speedtest.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/coverage_point_calculator/src/speedtest.rs b/coverage_point_calculator/src/speedtest.rs index 779cf9d7a..37980ce09 100644 --- a/coverage_point_calculator/src/speedtest.rs +++ b/coverage_point_calculator/src/speedtest.rs @@ -12,16 +12,18 @@ type Millis = u32; pub struct BytesPs(u64); impl BytesPs { + const BYTES_PER_MEGABYTE: u64 = 125_000; + pub fn new(bytes_per_second: u64) -> Self { Self(bytes_per_second) } pub fn mbps(megabytes_per_second: u64) -> Self { - Self(megabytes_per_second * 12500) + Self(megabytes_per_second * Self::BYTES_PER_MEGABYTE) } fn as_mbps(&self) -> u64 { - self.0 / 12500 + self.0 / Self::BYTES_PER_MEGABYTE } } @@ -231,6 +233,17 @@ mod tests { assert_eq!(dec!(1), multiplier(&speedtests)); } + #[test] + fn test_real_bytes_per_second() { + // Random sampling from database for a download speed that should be + // "Acceptable". Other situational tests go through the ::mbps() + // constructor, so will always be consistent with each other. + assert_eq!( + SpeedtestTier::Acceptable, + SpeedtestTier::from_download(BytesPs::new(11_702_687)) + ); + } + fn date(year: i32, month: u32, day: u32) -> DateTime { chrono::NaiveDate::from_ymd_opt(year, month, day) .unwrap() From 1abeb844837be9ad715cf346031b5e5ca008a05f Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Mon, 10 Jun 2024 17:12:22 -0700 Subject: [PATCH 109/115] Take responsibility of rounding shares and total coverage points For consistency, the calculator is now truncating values that would have been truncated be the user of the calculator, so we avoid a situation where 2 users of the calculator are using different rounding strategies. The fields that make up the truncated values are provided untouched. Truncating fully to a u64 causes calculations to be off by quite a margin. --- coverage_point_calculator/src/lib.rs | 39 +++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index f8fea1d85..b06396b07 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -58,7 +58,7 @@ pub use crate::{ speedtest::{BytesPs, Speedtest}, }; use coverage_map::SignalLevel; -use rust_decimal::{Decimal, RoundingStrategy}; +use rust_decimal::Decimal; use rust_decimal_macros::dec; mod hexes; @@ -88,13 +88,27 @@ pub enum Error { /// have no boosted_multiplier values. #[derive(Debug, Clone)] pub struct CoveragePoints { - /// Value used when calculating poc_reward + /// Total Rewards Shares earned by the Radio. + /// + /// Includes Coverage and Backhaul. + /// Hex Coverage points * location trust multiplier * speedtest trust multiplier + pub reward_shares: Decimal, + /// Total Points of Coverage for a Radio. + /// + /// Does not include Backhaul. + /// Hex coverage points * location trust multiplier pub total_coverage_points: Decimal, /// Coverage Points collected from each Covered Hex + /// + /// Before location trust multiplier is applied. pub hex_coverage_points: Decimal, /// Location Trust Multiplier, maximum of 1 + /// + /// Coverage trust of a Radio pub location_trust_multiplier: Decimal, /// Speedtest Mulitplier, maximum of 1 + /// + /// Backhaul of a Radio pub speedtest_multiplier: Decimal, /// Input Radio Type pub radio_type: RadioType, @@ -104,9 +118,9 @@ pub struct CoveragePoints { pub boosted_hex_eligibility: BoostedHexStatus, /// Speedtests used in calculcation pub speedtests: Vec, - /// Locaiton Trust Scores used in calculations + /// Location Trust Scores used in calculation pub location_trust_scores: Vec, - /// Covered Hexes used in calculations + /// Covered Hexes used in calculation pub covered_hexes: Vec, } @@ -131,12 +145,23 @@ impl CoveragePoints { let speedtests = speedtest::clean_speedtests(speedtests); let speedtest_multiplier = speedtest::multiplier(&speedtests); - let coverage_points = - hex_coverage_points * location_trust_multiplier * speedtest_multiplier; + let reward_shares = hex_coverage_points * location_trust_multiplier * speedtest_multiplier; + let total_coverage_points = hex_coverage_points * location_trust_multiplier; + + // Values to be used directly are truncated here. + // The values that make them up, go forward untruncated. + // let reward_shares = reward_shares.to_u64().unwrap_or_default(); + // let total_coverage_points = total_coverage_points.to_u64().unwrap_or_default(); + + // TODO: poc_reward calculations are done with fractional shares, + // truncating shares and points here breaks calculations + let reward_shares = + reward_shares.round_dp_with_strategy(2, rust_decimal::RoundingStrategy::ToZero); let total_coverage_points = - coverage_points.round_dp_with_strategy(2, RoundingStrategy::ToZero); + total_coverage_points.round_dp_with_strategy(2, rust_decimal::RoundingStrategy::ToZero); Ok(CoveragePoints { + reward_shares, total_coverage_points, hex_coverage_points, location_trust_multiplier, From f252947d8669fe59cf107a3a6308998cabdf5553 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 11 Jun 2024 10:40:43 -0700 Subject: [PATCH 110/115] Remove rounding To keep consistent with the reward calculations this crate is replacing, any rounding of values needs to be avoided before rewards are calculated. This means users of the values will need to handle routing going into protobufs themselves. --- coverage_point_calculator/src/lib.rs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index b06396b07..c5c3cacf2 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -148,18 +148,6 @@ impl CoveragePoints { let reward_shares = hex_coverage_points * location_trust_multiplier * speedtest_multiplier; let total_coverage_points = hex_coverage_points * location_trust_multiplier; - // Values to be used directly are truncated here. - // The values that make them up, go forward untruncated. - // let reward_shares = reward_shares.to_u64().unwrap_or_default(); - // let total_coverage_points = total_coverage_points.to_u64().unwrap_or_default(); - - // TODO: poc_reward calculations are done with fractional shares, - // truncating shares and points here breaks calculations - let reward_shares = - reward_shares.round_dp_with_strategy(2, rust_decimal::RoundingStrategy::ToZero); - let total_coverage_points = - total_coverage_points.round_dp_with_strategy(2, rust_decimal::RoundingStrategy::ToZero); - Ok(CoveragePoints { reward_shares, total_coverage_points, From ec39a070232a7ebeb499fc0eb5c8e82e9fd3a539 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 11 Jun 2024 10:57:38 -0700 Subject: [PATCH 111/115] hip-103: oracles and provider boosting crossover When a hex is boosted by a provider, it's oracle (assignment) boosting multiplier is automatically pushed to 1x. hip-103: top level oracle/provider boosting test --- coverage_point_calculator/src/hexes.rs | 67 +++++++++++++++++++++++++- coverage_point_calculator/src/lib.rs | 46 ++++++++++++++++-- 2 files changed, 109 insertions(+), 4 deletions(-) diff --git a/coverage_point_calculator/src/hexes.rs b/coverage_point_calculator/src/hexes.rs index 9c9d23afc..2cb73edfe 100644 --- a/coverage_point_calculator/src/hexes.rs +++ b/coverage_point_calculator/src/hexes.rs @@ -46,9 +46,16 @@ pub(crate) fn clean_covered_hexes( .map(|ranked| { let base_coverage_points = radio_type.base_coverage_points(&ranked.signal_level)?; let rank_multiplier = radio_type.rank_multiplier(ranked.rank); - let assignment_multiplier = ranked.assignments.boosting_multiplier(); let boosted_multiplier = ranked.boosted.map(|boost| boost.get()).map(Decimal::from); + // hip-103: if a hex is boosted by a service provider >1x, the oracle + // multiplier will automatically be 1x, regardless of boosted_hex_status. + let assignment_multiplier = if ranked.boosted.is_some() { + dec!(1) + } else { + ranked.assignments.boosting_multiplier() + }; + let calculated_coverage_points = base_coverage_points * assignment_multiplier * rank_multiplier @@ -76,3 +83,61 @@ pub(crate) fn calculated_coverage_points(covered_hexes: &[CoveredHex]) -> Decima .map(|hex| hex.calculated_coverage_points) .sum() } + +#[cfg(test)] +mod tests { + use std::num::NonZeroU32; + + use coverage_map::SignalLevel; + use hex_assignments::Assignment; + + use super::*; + + #[test] + fn hip_103_provider_boosted_hex_receives_maximum_oracle_boost() { + let unboosted_coverage = RankedCoverage { + hotspot_key: vec![1], + cbsd_id: None, + hex: hextree::Cell::from_raw(0x8c2681a3064edff).unwrap(), + rank: 1, + signal_level: SignalLevel::High, + assignments: HexAssignments { + footfall: Assignment::C, + landtype: Assignment::C, + urbanized: Assignment::C, + }, + boosted: NonZeroU32::new(0), + }; + let boosted_coverage = RankedCoverage { + boosted: NonZeroU32::new(5), + ..unboosted_coverage.clone() + }; + + let covered_hexes = clean_covered_hexes( + RadioType::IndoorWifi, + vec![unboosted_coverage.clone(), boosted_coverage], + BoostedHexStatus::Eligible, + ) + .unwrap(); + + let unboosted = &covered_hexes[0]; + let boosted = &covered_hexes[1]; + + // unboosted receives original multiplier + assert_eq!(dec!(0), unboosted.calculated_coverage_points); + assert_eq!( + unboosted_coverage.assignments.boosting_multiplier(), + unboosted.assignment_multiplier + ); + + // provider boosted gets oracle assignment bumped to 1x + assert_eq!(dec!(1), boosted.assignment_multiplier); + assert_eq!( + RadioType::IndoorWifi + .base_coverage_points(&SignalLevel::High) + .unwrap_or_default() + * dec!(5), + boosted.calculated_coverage_points + ); + } +} diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index c5c3cacf2..08588cf27 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -11,6 +11,7 @@ //! //! - [CoveredHex::assignment_multiplier] //! - [HIP-103][oracle-boosting] +//! - provider boosted hexes increase oracle boosting to 1x //! //! - [CoveredHex::rank] //! - [HIP-105][hex-limits] @@ -40,6 +41,7 @@ //! //! - [CoveredHex] //! - If a Radio is not [BoostedHexStatus::Eligible], boost values are removed before calculations. +//! - If a Hex is boosted by a Provider, the Oracle Assignment multiplier is automatically 1x. //! //! [modeled-coverage]: https://github.com/helium/HIP/blob/main/0074-mobile-poc-modeled-coverage-rewards.md#outdoor-radios //! [provider-boosting]: https://github.com/helium/HIP/blob/main/0084-service-provider-hex-boosting.md @@ -177,12 +179,12 @@ impl BoostedHexStatus { location_trust_score: Decimal, radio_threshold: &RadioThreshold, ) -> Self { - // hip93: if radio is wifi & location_trust score multiplier < 0.75, no boosting + // hip-93: if radio is wifi & location_trust score multiplier < 0.75, no boosting if radio_type.is_wifi() && location_trust_score < dec!(0.75) { return Self::WifiLocationScoreBelowThreshold(location_trust_score); } - // hip84: if radio has not met minimum data and subscriber thresholds, no boosting + // hip-84: if radio has not met minimum data and subscriber thresholds, no boosting if !radio_threshold.is_met() { return Self::RadioThresholdNotMet; } @@ -285,6 +287,36 @@ mod tests { use hex_assignments::{assignment::HexAssignments, Assignment}; use rust_decimal_macros::dec; + #[rstest] + #[case::unboosted(0, dec!(0))] + #[case::minimum_boosted(1, dec!(400))] + #[case::boosted(5, dec!(2000))] + fn hip_103_provider_boost_can_raise_oracle_boost( + #[case] boost_multiplier: u32, + #[case] expected_points: Decimal, + ) { + let wifi = CoveragePoints::new( + RadioType::IndoorWifi, + RadioThreshold::Verified, + speedtest_maximum(), + location_trust_maximum(), + vec![RankedCoverage { + hotspot_key: pubkey(), + cbsd_id: None, + hex: hex_location(), + rank: 1, + signal_level: SignalLevel::High, + assignments: assignments_from(Assignment::C), + boosted: NonZeroU32::new(boost_multiplier), + }], + ) + .unwrap(); + + // A Hex with the worst possible oracle boosting assignment. + // The boosting assignment multiplier will be 1x when the hex is provider boosted. + assert_eq!(expected_points, wifi.total_coverage_points); + } + #[test] fn hip_84_radio_meets_minimum_subscriber_threshold_for_boosted_hexes() { let calculate_wifi = |radio_verified: RadioThreshold| { @@ -360,7 +392,7 @@ mod tests { } #[test] - fn speedtest() { + fn speedtests_effect_coverage_points() { let calculate_indoor_cbrs = |speedtests: Vec| { CoveragePoints::new( RadioType::IndoorCbrs, @@ -761,6 +793,14 @@ mod tests { } } + fn assignments_from(assignment: Assignment) -> HexAssignments { + HexAssignments { + footfall: assignment, + landtype: assignment, + urbanized: assignment, + } + } + fn speedtest_maximum() -> Vec { vec![ Speedtest { From cfc884a55c77ceb6ecb43df9ceb46a6f37f89016 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 11 Jun 2024 16:21:34 -0700 Subject: [PATCH 112/115] Bring over scenario tests Come up with names for different scenarios, they are a combination of factors that effect readio scores. --- coverage_point_calculator/src/lib.rs | 2 +- .../tests/coverage_point_calculator.rs | 346 +++++++++++++++++- 2 files changed, 345 insertions(+), 3 deletions(-) diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index 08588cf27..b1ee7de6b 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -57,7 +57,7 @@ pub use crate::{ hexes::CoveredHex, location::LocationTrust, - speedtest::{BytesPs, Speedtest}, + speedtest::{BytesPs, Speedtest, SpeedtestTier}, }; use coverage_map::SignalLevel; use rust_decimal::Decimal; diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index fdc4d0be9..385afc3c9 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -1,9 +1,10 @@ use std::num::NonZeroU32; use chrono::Utc; -use coverage_map::{RankedCoverage, SignalLevel}; +use coverage_map::{BoostedHexMap, RankedCoverage, SignalLevel, UnrankedCoverage}; use coverage_point_calculator::{ - BytesPs, CoveragePoints, LocationTrust, RadioThreshold, RadioType, Speedtest, + BytesPs, CoveragePoints, LocationTrust, RadioThreshold, RadioType, Result, Speedtest, + SpeedtestTier, }; use hex_assignments::{assignment::HexAssignments, Assignment}; use rust_decimal_macros::dec; @@ -122,3 +123,344 @@ fn radios_with_coverage() { assert_eq!(dec!(400), coverage_points.total_coverage_points); } } + +#[test] +fn cbrs_with_mixed_signal_level_coverage() -> Result { + // Scenario One + let coverage_points = indoor_cbrs_radio( + SpeedtestTier::Good, + &[ + top_ranked_coverage(0x8c2681a3064d9ff, SignalLevel::High), + top_ranked_coverage(0x8c2681a3065d3ff, SignalLevel::Low), + top_ranked_coverage(0x8c2681a306635ff, SignalLevel::Low), + top_ranked_coverage(0x8c2681a3066e7ff, SignalLevel::Low), + top_ranked_coverage(0x8c2681a3065adff, SignalLevel::Low), + top_ranked_coverage(0x8c2681a339a4bff, SignalLevel::Low), + top_ranked_coverage(0x8c2681a3065d7ff, SignalLevel::Low), + ], + )?; + + assert_eq!(dec!(250), coverage_points.reward_shares); + + Ok(()) +} + +#[test] +fn cbrs_with_partially_overlapping_coverage_and_differing_speedtests() -> Result { + // Scenario two + // Two radios, with a single overlapping hex and differing speedtest scores. + let radio_1 = indoor_cbrs_radio( + SpeedtestTier::Degraded, + &[ + top_ranked_coverage(0x8c2681a3064d9ff, SignalLevel::High), + second_ranked_coverage(0x8c2681a3065d3ff, SignalLevel::Low), // This hex is shared + top_ranked_coverage(0x8c2681a306635ff, SignalLevel::Low), + top_ranked_coverage(0x8c2681a3066e7ff, SignalLevel::Low), + top_ranked_coverage(0x8c2681a3065adff, SignalLevel::Low), + top_ranked_coverage(0x8c2681a339a4bff, SignalLevel::Low), + top_ranked_coverage(0x8c2681a3065d7ff, SignalLevel::Low), + ], + )?; + + let radio_2 = indoor_cbrs_radio( + SpeedtestTier::Good, + &[ + top_ranked_coverage(0x8c2681a30641dff, SignalLevel::High), + top_ranked_coverage(0x8c2681a3065d3ff, SignalLevel::Low), // This hex is shared + top_ranked_coverage(0x8c2681a3066a9ff, SignalLevel::Low), + top_ranked_coverage(0x8c2681a306607ff, SignalLevel::Low), + top_ranked_coverage(0x8c2681a3066e9ff, SignalLevel::Low), + top_ranked_coverage(0x8c2681a306481ff, SignalLevel::Low), + top_ranked_coverage(0x8c2681a302991ff, SignalLevel::Low), + ], + )?; + + assert_eq!(dec!(112.5), radio_1.reward_shares); + assert_eq!(dec!(250), radio_2.reward_shares); + + Ok(()) +} + +#[test] +fn cbrs_with_wholly_overlapping_coverage_and_differing_speedtests() -> Result { + // Scenario Three + // All radios cover the same hexes. + // Seniority timestamps determine rank. + // Only the first ranked radio (radio_4) should receive rewards. + + let mut coverage_map_builder = coverage_map::CoverageMapBuilder::default(); + let mut insert_coverage = |cbsd_id: &str, timestamp: &str| { + coverage_map_builder.insert_coverage_object(coverage_map::CoverageObject { + indoor: true, + hotspot_key: vec![], + cbsd_id: Some(cbsd_id.to_string()), + seniority_timestamp: timestamp.parse().expect("valid timestamp"), + coverage: vec![ + unranked_coverage(0x8c2681a3064d9ff, SignalLevel::High), + unranked_coverage(0x8c2681a3065d3ff, SignalLevel::Low), + unranked_coverage(0x8c2681a306635ff, SignalLevel::Low), + unranked_coverage(0x8c2681a3066e7ff, SignalLevel::Low), + unranked_coverage(0x8c2681a3065adff, SignalLevel::Low), + unranked_coverage(0x8c2681a339a4bff, SignalLevel::Low), + unranked_coverage(0x8c2681a3065d7ff, SignalLevel::Low), + ], + }) + }; + + insert_coverage("serial-1", "2022-02-01 00:00:00.000000000 UTC"); + insert_coverage("serial-2", "2022-02-01 00:00:00.000000000 UTC"); + insert_coverage("serial-3", "2022-02-01 00:00:00.000000000 UTC"); + insert_coverage("serial-4", "2022-01-31 00:00:00.000000000 UTC"); // earliest + insert_coverage("serial-5", "2022-02-01 00:00:00.000000000 UTC"); + insert_coverage("serial-6", "2022-02-02 00:00:00.000000000 UTC"); // latest + + let map = coverage_map_builder.build(&NoBoostedHexes, Utc::now()); + + let radio_1 = indoor_cbrs_radio(SpeedtestTier::Poor, map.get_cbrs_coverage("serial-1"))?; + let radio_2 = indoor_cbrs_radio(SpeedtestTier::Poor, map.get_cbrs_coverage("serial-2"))?; + let radio_3 = indoor_cbrs_radio(SpeedtestTier::Good, map.get_cbrs_coverage("serial-3"))?; + let radio_4 = indoor_cbrs_radio(SpeedtestTier::Good, map.get_cbrs_coverage("serial-4"))?; + let radio_5 = indoor_cbrs_radio(SpeedtestTier::Fail, map.get_cbrs_coverage("serial-5"))?; + let radio_6 = indoor_cbrs_radio(SpeedtestTier::Good, map.get_cbrs_coverage("serial-6"))?; + + assert_eq!(dec!(0), radio_1.reward_shares); + assert_eq!(dec!(0), radio_2.reward_shares); + assert_eq!(dec!(0), radio_3.reward_shares); + assert_eq!(dec!(250), radio_4.reward_shares); + assert_eq!(dec!(0), radio_5.reward_shares); + assert_eq!(dec!(0), radio_6.reward_shares); + + Ok(()) +} + +#[test] +fn cbrs_outdoor_with_mixed_signal_level_coverage() -> Result { + // Scenario four + // Outdoor Cbrs with mixed signal level coverage + + let radio = CoveragePoints::new( + RadioType::OutdoorCbrs, + RadioThreshold::Verified, + speedtests(SpeedtestTier::Good), + vec![], // Location Trust is ignored for Cbrs + vec![ + top_ranked_coverage(0x8c2681a3064d9ff, SignalLevel::High), + top_ranked_coverage(0x8c2681a3065d3ff, SignalLevel::High), + top_ranked_coverage(0x8c2681a306635ff, SignalLevel::Medium), + top_ranked_coverage(0x8c2681a3066e7ff, SignalLevel::Medium), + top_ranked_coverage(0x8c2681a3065adff, SignalLevel::Medium), + top_ranked_coverage(0x8c2681a339a4bff, SignalLevel::Low), + top_ranked_coverage(0x8c2681a3065d7ff, SignalLevel::Low), + top_ranked_coverage(0x8c2681a306481ff, SignalLevel::Low), + top_ranked_coverage(0x8c2681a30648bff, SignalLevel::Low), + top_ranked_coverage(0x8c2681a30646bff, SignalLevel::Low), + ], + ) + .unwrap(); + + assert_eq!(dec!(19), radio.reward_shares); + + Ok(()) +} + +#[test] +fn cbrs_outdoor_with_single_overlapping_coverage() -> Result { + // Scenario Five + // 2 radios overlapping a single hex with a medium Signal Level. + // First radio has seniority. + + let mut coverage_map_builder = coverage_map::CoverageMapBuilder::default(); + + coverage_map_builder.insert_coverage_object(coverage_map::CoverageObject { + indoor: false, + hotspot_key: vec![1], + cbsd_id: Some("serial-1".to_string()), + seniority_timestamp: "2022-02-01 00:00:00.000000000 UTC" + .parse() + .expect("valid timestamp"), + coverage: vec![ + unranked_coverage(0x8c2681a302991ff, SignalLevel::High), + unranked_coverage(0x8c2681a306601ff, SignalLevel::High), + unranked_coverage(0x8c2681a306697ff, SignalLevel::High), + unranked_coverage(0x8c2681a3028a7ff, SignalLevel::Medium), // This hex is shared + unranked_coverage(0x8c2681a3064c1ff, SignalLevel::Medium), + unranked_coverage(0x8c2681a30671bff, SignalLevel::Low), + unranked_coverage(0x8c2681a306493ff, SignalLevel::Low), + unranked_coverage(0x8c2681a30659dff, SignalLevel::Low), + ], + }); + coverage_map_builder.insert_coverage_object(coverage_map::CoverageObject { + indoor: false, + hotspot_key: vec![2], + cbsd_id: Some("serial-2".to_string()), + seniority_timestamp: "2022-02-01 00:00:01.000000000 UTC" + .parse() + .expect("valid timestamp"), + coverage: vec![ + unranked_coverage(0x8c2681a3066abff, SignalLevel::High), + unranked_coverage(0x8c2681a3028a7ff, SignalLevel::Medium), // This hex is shared + unranked_coverage(0x8c2681a3066a9ff, SignalLevel::Low), + unranked_coverage(0x8c2681a3066a5ff, SignalLevel::Low), + unranked_coverage(0x8c2681a30640dff, SignalLevel::Low), + ], + }); + + let map = coverage_map_builder.build(&NoBoostedHexes, Utc::now()); + + let radio_1 = outdoor_cbrs_radio(SpeedtestTier::Degraded, map.get_cbrs_coverage("serial-1"))?; + let radio_2 = outdoor_cbrs_radio(SpeedtestTier::Good, map.get_cbrs_coverage("serial-2"))?; + + assert_eq!(dec!(19) * dec!(0.5), radio_1.reward_shares); + assert_eq!(dec!(8), radio_2.reward_shares); + + Ok(()) +} + +#[test] +fn cbrs_indoor_with_wholly_overlapping_coverage_and_no_failing_speedtests() -> Result { + // Scenario Six + // Similar to Scenario Three, but there are no failing speedtests. + // Radios have the same coverage. + + let mut coverage_map_builder = coverage_map::CoverageMapBuilder::default(); + let mut insert_coverage = |cbsd_id: &str, timestamp: &str| { + coverage_map_builder.insert_coverage_object(coverage_map::CoverageObject { + indoor: true, + hotspot_key: vec![0], + cbsd_id: Some(cbsd_id.to_string()), + seniority_timestamp: timestamp.parse().expect("valid timestamp"), + coverage: vec![ + unranked_coverage(0x8c2681a3064d9ff, SignalLevel::High), + unranked_coverage(0x8c2681a3065d3ff, SignalLevel::Low), + unranked_coverage(0x8c2681a306635ff, SignalLevel::Low), + unranked_coverage(0x8c2681a3066e7ff, SignalLevel::Low), + unranked_coverage(0x8c2681a3065adff, SignalLevel::Low), + unranked_coverage(0x8c2681a339a4bff, SignalLevel::Low), + unranked_coverage(0x8c2681a3065d7ff, SignalLevel::Low), + ], + }) + }; + + insert_coverage("serial-1", "2022-02-01 00:00:00.000000000 UTC"); + insert_coverage("serial-2", "2022-01-31 00:00:00.000000000 UTC"); // Oldest + insert_coverage("serial-3", "2022-02-01 00:00:00.000000000 UTC"); + insert_coverage("serial-4", "2022-02-01 00:00:00.000000000 UTC"); + insert_coverage("serial-5", "2022-02-01 00:00:00.000000000 UTC"); + insert_coverage("serial-6", "2022-02-02 00:00:00.000000000 UTC"); // Newest + let map = coverage_map_builder.build(&NoBoostedHexes, Utc::now()); + + let radio_1 = indoor_cbrs_radio(SpeedtestTier::Poor, map.get_cbrs_coverage("serial-1"))?; + let radio_2 = indoor_cbrs_radio(SpeedtestTier::Poor, map.get_cbrs_coverage("serial-2"))?; + let radio_3 = indoor_cbrs_radio(SpeedtestTier::Good, map.get_cbrs_coverage("serial-3"))?; + let radio_4 = indoor_cbrs_radio(SpeedtestTier::Good, map.get_cbrs_coverage("serial-4"))?; + let radio_5 = indoor_cbrs_radio(SpeedtestTier::Good, map.get_cbrs_coverage("serial-5"))?; + let radio_6 = indoor_cbrs_radio(SpeedtestTier::Good, map.get_cbrs_coverage("serial-6"))?; + + assert_eq!(dec!(0), radio_1.reward_shares); + assert_eq!(dec!(62.5), radio_2.reward_shares); + assert_eq!(dec!(0), radio_3.reward_shares); + assert_eq!(dec!(0), radio_4.reward_shares); + assert_eq!(dec!(0), radio_5.reward_shares); + assert_eq!(dec!(0), radio_6.reward_shares); + + Ok(()) +} + +fn indoor_cbrs_radio( + speedtest_tier: SpeedtestTier, + coverage: &[RankedCoverage], +) -> Result { + CoveragePoints::new( + RadioType::IndoorCbrs, + RadioThreshold::Verified, + speedtests(speedtest_tier), + vec![], + coverage.to_owned(), + ) +} + +fn outdoor_cbrs_radio( + speedtest_tier: SpeedtestTier, + coverage: &[RankedCoverage], +) -> Result { + CoveragePoints::new( + RadioType::OutdoorCbrs, + RadioThreshold::Verified, + speedtests(speedtest_tier), + vec![], + coverage.to_owned(), + ) +} + +struct NoBoostedHexes; +impl BoostedHexMap for NoBoostedHexes { + fn get_current_multiplier( + &self, + _cell: hextree::Cell, + _ts: chrono::DateTime, + ) -> Option { + None + } +} + +fn speedtests(tier: SpeedtestTier) -> Vec { + let upload_speed = BytesPs::mbps(match tier { + SpeedtestTier::Good => 10, + SpeedtestTier::Acceptable => 8, + SpeedtestTier::Degraded => 5, + SpeedtestTier::Poor => 2, + SpeedtestTier::Fail => 0, + }); + + vec![ + Speedtest { + upload_speed: upload_speed.clone(), + download_speed: BytesPs::mbps(150), + latency_millis: 0, + timestamp: Utc::now(), + }, + Speedtest { + upload_speed: upload_speed.clone(), + download_speed: BytesPs::mbps(150), + latency_millis: 0, + timestamp: Utc::now(), + }, + ] +} + +fn top_ranked_coverage(hex: u64, signal_level: SignalLevel) -> RankedCoverage { + ranked_coverage(hex, 1, signal_level) +} + +fn second_ranked_coverage(hex: u64, signal_level: SignalLevel) -> RankedCoverage { + ranked_coverage(hex, 2, signal_level) +} + +fn ranked_coverage(hex: u64, rank: usize, signal_level: SignalLevel) -> RankedCoverage { + RankedCoverage { + hex: hextree::Cell::from_raw(hex).expect("valid h3 hex"), + rank, + hotspot_key: vec![1], + cbsd_id: Some("serial".to_string()), + assignments: HexAssignments { + footfall: Assignment::A, + landtype: Assignment::A, + urbanized: Assignment::A, + }, + boosted: None, + signal_level, + } +} + +fn unranked_coverage(hex: u64, signal_level: SignalLevel) -> UnrankedCoverage { + UnrankedCoverage { + location: hextree::Cell::from_raw(hex).expect("valid h3 hex"), + signal_power: 42, + signal_level, + assignments: HexAssignments { + footfall: Assignment::A, + landtype: Assignment::A, + urbanized: Assignment::A, + }, + } +} From 419b273645557ad1507cab0a0eb72270f4a99227 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 11 Jun 2024 16:28:59 -0700 Subject: [PATCH 113/115] BytesPs is Copy, no need to .clone() --- .../tests/coverage_point_calculator.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/coverage_point_calculator/tests/coverage_point_calculator.rs b/coverage_point_calculator/tests/coverage_point_calculator.rs index 385afc3c9..2ab08a945 100644 --- a/coverage_point_calculator/tests/coverage_point_calculator.rs +++ b/coverage_point_calculator/tests/coverage_point_calculator.rs @@ -404,6 +404,8 @@ impl BoostedHexMap for NoBoostedHexes { } fn speedtests(tier: SpeedtestTier) -> Vec { + // SpeedtestTier is determined solely by upload_speed. + // Other values are far surpassing ::Good. let upload_speed = BytesPs::mbps(match tier { SpeedtestTier::Good => 10, SpeedtestTier::Acceptable => 8, @@ -414,13 +416,13 @@ fn speedtests(tier: SpeedtestTier) -> Vec { vec![ Speedtest { - upload_speed: upload_speed.clone(), + upload_speed, download_speed: BytesPs::mbps(150), latency_millis: 0, timestamp: Utc::now(), }, Speedtest { - upload_speed: upload_speed.clone(), + upload_speed, download_speed: BytesPs::mbps(150), latency_millis: 0, timestamp: Utc::now(), From 87b6b3a0dbd831172cd7fe648fe6c7cfa6281d47 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 12 Jun 2024 09:19:44 -0700 Subject: [PATCH 114/115] hip-103: provider boost increases oracle boost always If a hex is boosted by a provider, that hex is deemed valuable from an oracle context, and should always be 1x. Hexes were being "cleaned" of their boost values _before_ trying to determine the oracle multplier, which coupled a hexes boost effect to the assignment multiplier to wether or not the radio was eligible for boosting rewards. That was incorrect. Hexes are now "cleaned" at the same time as determining the multipliers. --- coverage_point_calculator/src/hexes.rs | 50 +++++++++++--------------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/coverage_point_calculator/src/hexes.rs b/coverage_point_calculator/src/hexes.rs index 2cb73edfe..29360edd6 100644 --- a/coverage_point_calculator/src/hexes.rs +++ b/coverage_point_calculator/src/hexes.rs @@ -28,27 +28,20 @@ pub(crate) fn clean_covered_hexes( ranked_coverage: Vec, boosted_hex_status: BoostedHexStatus, ) -> Result> { - let ranked_coverage = if !boosted_hex_status.is_eligible() { - ranked_coverage - .into_iter() - .map(|ranked| RankedCoverage { - boosted: None, - ..ranked - }) - .collect() - } else { - ranked_coverage - }; - // verify all hexes can obtain a base coverage point let covered_hexes = ranked_coverage .into_iter() .map(|ranked| { let base_coverage_points = radio_type.base_coverage_points(&ranked.signal_level)?; let rank_multiplier = radio_type.rank_multiplier(ranked.rank); - let boosted_multiplier = ranked.boosted.map(|boost| boost.get()).map(Decimal::from); - // hip-103: if a hex is boosted by a service provider >1x, the oracle + let boosted_multiplier = if boosted_hex_status.is_eligible() { + ranked.boosted.map(|boost| boost.get()).map(Decimal::from) + } else { + None + }; + + // hip-103: if a hex is boosted by a service provider >=1x, the oracle // multiplier will automatically be 1x, regardless of boosted_hex_status. let assignment_multiplier = if ranked.boosted.is_some() { dec!(1) @@ -86,6 +79,7 @@ pub(crate) fn calculated_coverage_points(covered_hexes: &[CoveredHex]) -> Decima #[cfg(test)] mod tests { + use rstest::rstest; use std::num::NonZeroU32; use coverage_map::SignalLevel; @@ -93,8 +87,15 @@ mod tests { use super::*; - #[test] - fn hip_103_provider_boosted_hex_receives_maximum_oracle_boost() { + #[rstest] + #[case(BoostedHexStatus::Eligible)] + #[case(BoostedHexStatus::WifiLocationScoreBelowThreshold(dec!(999)))] + #[case(BoostedHexStatus::RadioThresholdNotMet)] + fn hip_103_provider_boosted_hex_receives_maximum_oracle_boost( + #[case] boost_status: BoostedHexStatus, + ) { + // Regardless of the radio's eligibility to receive provider boosted + // rewards, a boosted hex increases the oracle assignment. let unboosted_coverage = RankedCoverage { hotspot_key: vec![1], cbsd_id: None, @@ -115,8 +116,8 @@ mod tests { let covered_hexes = clean_covered_hexes( RadioType::IndoorWifi, - vec![unboosted_coverage.clone(), boosted_coverage], - BoostedHexStatus::Eligible, + vec![unboosted_coverage, boosted_coverage], + boost_status, ) .unwrap(); @@ -124,20 +125,9 @@ mod tests { let boosted = &covered_hexes[1]; // unboosted receives original multiplier - assert_eq!(dec!(0), unboosted.calculated_coverage_points); - assert_eq!( - unboosted_coverage.assignments.boosting_multiplier(), - unboosted.assignment_multiplier - ); + assert_eq!(dec!(0), unboosted.assignment_multiplier); // provider boosted gets oracle assignment bumped to 1x assert_eq!(dec!(1), boosted.assignment_multiplier); - assert_eq!( - RadioType::IndoorWifi - .base_coverage_points(&SignalLevel::High) - .unwrap_or_default() - * dec!(5), - boosted.calculated_coverage_points - ); } } From 0399213fa18240581bc9c2fdc690a1566cbe11cf Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Wed, 12 Jun 2024 09:21:52 -0700 Subject: [PATCH 115/115] move collections to the end I could see an argument for the collection being moved to the front as well. But I think either is a better case than in the middle. I chose the end here because I think it reads easier when testing to have the function, then small decision making structs, _then_ the collection that could be constructed in place. --- coverage_point_calculator/src/hexes.rs | 4 ++-- coverage_point_calculator/src/lib.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/coverage_point_calculator/src/hexes.rs b/coverage_point_calculator/src/hexes.rs index 29360edd6..95e973c70 100644 --- a/coverage_point_calculator/src/hexes.rs +++ b/coverage_point_calculator/src/hexes.rs @@ -25,8 +25,8 @@ pub struct CoveredHex { pub(crate) fn clean_covered_hexes( radio_type: RadioType, - ranked_coverage: Vec, boosted_hex_status: BoostedHexStatus, + ranked_coverage: Vec, ) -> Result> { // verify all hexes can obtain a base coverage point let covered_hexes = ranked_coverage @@ -116,8 +116,8 @@ mod tests { let covered_hexes = clean_covered_hexes( RadioType::IndoorWifi, - vec![unboosted_coverage, boosted_coverage], boost_status, + vec![unboosted_coverage, boosted_coverage], ) .unwrap(); diff --git a/coverage_point_calculator/src/lib.rs b/coverage_point_calculator/src/lib.rs index b1ee7de6b..90b986aec 100644 --- a/coverage_point_calculator/src/lib.rs +++ b/coverage_point_calculator/src/lib.rs @@ -141,7 +141,7 @@ impl CoveragePoints { BoostedHexStatus::new(&radio_type, location_trust_multiplier, &radio_threshold); let covered_hexes = - hexes::clean_covered_hexes(radio_type, ranked_coverage, boost_eligibility)?; + hexes::clean_covered_hexes(radio_type, boost_eligibility, ranked_coverage)?; let hex_coverage_points = hexes::calculated_coverage_points(&covered_hexes); let speedtests = speedtest::clean_speedtests(speedtests);