From c2322b21cb24701af84b90c8608431d96a2ba1b6 Mon Sep 17 00:00:00 2001 From: Matthew Plant Date: Thu, 30 May 2024 16:15:05 -0400 Subject: [PATCH 01/13] Initial commit for coverage-map --- Cargo.lock | 14 +++ Cargo.toml | 1 + coverage_map/Cargo.toml | 18 ++++ coverage_map/src/indoor.rs | 125 ++++++++++++++++++++++++++ coverage_map/src/lib.rs | 175 ++++++++++++++++++++++++++++++++++++ coverage_map/src/outdoor.rs | 128 ++++++++++++++++++++++++++ 6 files changed, 461 insertions(+) create mode 100644 coverage_map/Cargo.toml create mode 100644 coverage_map/src/indoor.rs create mode 100644 coverage_map/src/lib.rs create mode 100644 coverage_map/src/outdoor.rs diff --git a/Cargo.lock b/Cargo.lock index 43726fc94..d94501eaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2121,6 +2121,20 @@ dependencies = [ "volatile-register", ] +[[package]] +name = "coverage_map" +version = "0.1.0" +dependencies = [ + "chrono", + "h3o", + "helium-crypto", + "helium-proto", + "hex-assignments", + "hextree", + "mobile-config", + "uuid", +] + [[package]] name = "cpufeatures" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index a858438fa..46474b62e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ debug = true [workspace] members = [ "boost_manager", + "coverage_map", "custom_tracing", "db_store", "denylist", diff --git a/coverage_map/Cargo.toml b/coverage_map/Cargo.toml new file mode 100644 index 000000000..66eda3374 --- /dev/null +++ b/coverage_map/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "coverage_map" +version = "0.1.0" +authors.workspace = true +license.workspace = true +edition.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +chrono = { workspace = true } +h3o = { workspace = true } +helium-crypto = { workspace = true } +helium-proto = { workspace = true } +hex-assignments = { path = "../hex_assignments" } +hextree = { workspace = true } +mobile-config = { path = "../mobile_config" } +uuid = { workspace = true } diff --git a/coverage_map/src/indoor.rs b/coverage_map/src/indoor.rs new file mode 100644 index 000000000..e38341aa6 --- /dev/null +++ b/coverage_map/src/indoor.rs @@ -0,0 +1,125 @@ +use std::{ + cmp::Ordering, + collections::{hash_map::Entry, BTreeMap, BinaryHeap, HashMap}, +}; + +use chrono::{DateTime, Utc}; +use helium_crypto::PublicKeyBinary; +use hex_assignments::assignment::HexAssignments; +use hextree::Cell; +use mobile_config::boosted_hex_info::BoostedHexes; + +use crate::{ + CoverageObject, Rank, RankedCoverage, SignalLevel, UnrankedCoverage, + MAX_INDOOR_RADIOS_PER_RES12_HEX, +}; + +pub type IndoorCellTree = HashMap>>; + +#[derive(Eq, Debug, Clone)] +pub struct IndoorCoverageLevel { + hotspot_key: PublicKeyBinary, + cbsd_id: Option, + seniority_timestamp: DateTime, + signal_level: SignalLevel, + assignments: HexAssignments, +} + +impl PartialEq for IndoorCoverageLevel { + fn eq(&self, other: &Self) -> bool { + self.seniority_timestamp == other.seniority_timestamp + } +} + +impl PartialOrd for IndoorCoverageLevel { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for IndoorCoverageLevel { + fn cmp(&self, other: &Self) -> Ordering { + self.seniority_timestamp.cmp(&other.seniority_timestamp) + } +} + +pub fn insert_indoor_coverage_object(indoor: &mut IndoorCellTree, coverage_object: CoverageObject) { + for hex_coverage in coverage_object.coverage.into_iter() { + insert_indoor_coverage( + indoor, + &coverage_object.hotspot_key, + &coverage_object.cbsd_id, + coverage_object.seniority_timestamp, + hex_coverage, + ); + } +} + +pub fn insert_indoor_coverage( + indoor: &mut IndoorCellTree, + hotspot: &PublicKeyBinary, + cbsd_id: &Option, + seniority_timestamp: DateTime, + hex_coverage: UnrankedCoverage, +) { + indoor + .entry(hex_coverage.location) + .or_default() + .entry(hex_coverage.signal_level) + .or_default() + .push(IndoorCoverageLevel { + hotspot_key: hotspot.clone(), + cbsd_id: cbsd_id.clone(), + seniority_timestamp, + signal_level: hex_coverage.signal_level, + assignments: hex_coverage.assignments, + }) +} + +pub fn clone_indoor_coverage_into_submap( + submap: &mut IndoorCellTree, + from: &IndoorCellTree, + coverage_obj: &CoverageObject, +) { + for coverage in &coverage_obj.coverage { + if let Entry::Vacant(e) = submap.entry(coverage.location) { + if let Some(old_coverage_data) = from.get(&coverage.location) { + e.insert(old_coverage_data.clone()); + } + } + } +} + +pub fn into_indoor_coverage_map( + indoor: IndoorCellTree, + boosted_hexes: &BoostedHexes, + epoch_start: DateTime, +) -> impl Iterator + '_ { + indoor + .into_iter() + .flat_map(move |(hex, mut radios)| { + let boosted = boosted_hexes.get_current_multiplier(hex, epoch_start); + radios.pop_last().map(move |(_, radios)| { + radios + .into_sorted_vec() + .into_iter() + .take(MAX_INDOOR_RADIOS_PER_RES12_HEX) + .enumerate() + .flat_map(move |(rank, cov)| { + Rank::from_indoor_index(rank).map(move |rank| { + let key = cov.hotspot_key; + let cov = RankedCoverage { + hex, + rank, + cbsd_id: cov.cbsd_id, + assignments: cov.assignments, + boosted, + signal_level: cov.signal_level, + }; + (key, cov) + }) + }) + }) + }) + .flatten() +} diff --git a/coverage_map/src/lib.rs b/coverage_map/src/lib.rs new file mode 100644 index 000000000..325359709 --- /dev/null +++ b/coverage_map/src/lib.rs @@ -0,0 +1,175 @@ +use std::{collections::HashMap, num::NonZeroU32}; + +use chrono::{DateTime, Utc}; +use helium_crypto::PublicKeyBinary; +use hex_assignments::assignment::HexAssignments; +use hextree::Cell; +use mobile_config::boosted_hex_info::BoostedHexes; + +mod indoor; +mod outdoor; + +use indoor::*; +use outdoor::*; + +/// Data structure for keeping track of the ranking the coverage in each hex cell for indoor +/// and outdoor CBRS and WiFi radios. +#[derive(Default)] +pub struct CoverageMapBuilder { + indoor_cbrs: IndoorCellTree, + indoor_wifi: IndoorCellTree, + outdoor_cbrs: OutdoorCellTree, + outdoor_wifi: OutdoorCellTree, +} + +impl CoverageMapBuilder { + /// Inserts a new coverage object into the builder. + pub fn insert_coverage_object(&mut self, coverage_obj: CoverageObject) { + match (coverage_obj.indoor, coverage_obj.cbsd_id.is_some()) { + (true, true) => insert_indoor_coverage_object(&mut self.indoor_cbrs, coverage_obj), + (true, false) => insert_indoor_coverage_object(&mut self.indoor_wifi, coverage_obj), + (false, true) => insert_outdoor_coverage_object(&mut self.outdoor_cbrs, coverage_obj), + (false, false) => insert_outdoor_coverage_object(&mut self.outdoor_wifi, coverage_obj), + } + } + + /// Creates a submap from the current `CoverageMapBuilder` and the provided `coverage_objs`. + /// + /// A submap only contains the hexes that exist in the provided `coverage_objs` arguments. This + /// allows for one to determine the potential ranking of new coverage objects without having + /// to clone the entire CoverageMapBuilder. + pub fn submap(&self, coverage_objs: Vec) -> Self { + // A different way to implement this function would be to insert all of the coverage_objs into + // the submap, and then reconstruct the coverage objs from only the relevant hexes and then + // insert them into the new coverage object builder. + let mut new_submap = Self::default(); + for coverage_obj in coverage_objs { + // Clone each of the hexes in the current coverage from the old map into the new submap: + match (coverage_obj.indoor, coverage_obj.cbsd_id.is_some()) { + (true, true) => clone_indoor_coverage_into_submap( + &mut new_submap.indoor_cbrs, + &self.indoor_cbrs, + &coverage_obj, + ), + (true, false) => clone_indoor_coverage_into_submap( + &mut new_submap.indoor_wifi, + &self.indoor_wifi, + &coverage_obj, + ), + (false, true) => clone_outdoor_coverage_into_submap( + &mut new_submap.outdoor_cbrs, + &self.outdoor_cbrs, + &coverage_obj, + ), + (false, false) => clone_outdoor_coverage_into_submap( + &mut new_submap.outdoor_wifi, + &self.outdoor_wifi, + &coverage_obj, + ), + } + // Now that we are sure that each of the hexes in this coverage object are in the new + // submap, we can insert the new coverage obj. + new_submap.insert_coverage_object(coverage_obj); + } + new_submap + } + + /// Constructs a [CoverageMap] from the current `CoverageMapBuilder` + pub fn build(self, boosted_hexes: &BoostedHexes, epoch_start: DateTime) -> CoverageMap { + let mut hotspots = HashMap::<_, Vec>::new(); + for (radio, coverage) in + into_indoor_coverage_map(self.indoor_cbrs, boosted_hexes, epoch_start) + .chain(into_indoor_coverage_map( + self.indoor_wifi, + boosted_hexes, + epoch_start, + )) + .chain(into_outdoor_coverage_map( + self.outdoor_cbrs, + boosted_hexes, + epoch_start, + )) + .chain(into_outdoor_coverage_map( + self.outdoor_wifi, + boosted_hexes, + epoch_start, + )) + { + hotspots.entry(radio).or_default().push(coverage); + } + CoverageMap { hotspots } + } +} + +/// Data structure from mapping hotspots to their ranked hex coverage +pub struct CoverageMap { + hotspots: HashMap>, +} + +impl CoverageMap { + /// Returns the hexes covered by the hotspot. The returned slice can be empty, indicating that + /// the hotspot did not meet the criteria to be ranked in any hex. + pub fn get_coverage(&self, hotspot: &PublicKeyBinary) -> &[RankedCoverage] { + self.hotspots.get(hotspot).map(Vec::as_slice).unwrap_or(&[]) + } +} + +/// Coverage data given as input to the [CoverageMapBuilder] +pub struct CoverageObject { + pub indoor: bool, + pub hotspot_key: PublicKeyBinary, + pub cbsd_id: Option, + pub seniority_timestamp: DateTime, + pub coverage: Vec, +} + +/// Unranked hex coverage data given as input to the [CoverageMapBuilder] +pub struct UnrankedCoverage { + pub location: Cell, + pub signal_power: i32, + pub signal_level: SignalLevel, + pub assignments: HexAssignments, +} + +/// Ranked hex coverage given as output from the [CoverageMap] +pub struct RankedCoverage { + pub hex: Cell, + pub rank: Rank, + pub cbsd_id: Option, + pub assignments: HexAssignments, + pub boosted: Option, + pub signal_level: SignalLevel, +} + +/// Rank of the hex coverage. +pub enum Rank { + First, + Second, + Third, +} + +#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub enum SignalLevel { + None, + Low, + Medium, + High, +} + +pub const MAX_INDOOR_RADIOS_PER_RES12_HEX: usize = 1; +pub const MAX_OUTDOOR_RADIOS_PER_RES12_HEX: usize = 3; + +impl Rank { + pub(crate) fn from_outdoor_index(idx: usize) -> Option { + match idx { + 0 => Some(Self::First), + 1 => Some(Self::Second), + 2 => Some(Self::Third), + _ => None, + } + } + + pub(crate) fn from_indoor_index(idx: usize) -> Option { + (idx == 0).then_some(Self::First) + } +} diff --git a/coverage_map/src/outdoor.rs b/coverage_map/src/outdoor.rs new file mode 100644 index 000000000..1310d74ef --- /dev/null +++ b/coverage_map/src/outdoor.rs @@ -0,0 +1,128 @@ +use std::{ + cmp::Ordering, + collections::{hash_map::Entry, BinaryHeap, HashMap}, +}; + +use chrono::{DateTime, Utc}; +use helium_crypto::PublicKeyBinary; +use hex_assignments::assignment::HexAssignments; +use hextree::Cell; +use mobile_config::boosted_hex_info::BoostedHexes; + +use crate::{ + CoverageObject, Rank, RankedCoverage, SignalLevel, UnrankedCoverage, + MAX_OUTDOOR_RADIOS_PER_RES12_HEX, +}; + +/// Data structure for storing outdoor radios ranked by their coverage level +pub type OutdoorCellTree = HashMap>; + +#[derive(Eq, Debug, Clone)] +pub struct OutdoorCoverageLevel { + hotspot_key: PublicKeyBinary, + cbsd_id: Option, + seniority_timestamp: DateTime, + signal_power: i32, + signal_level: SignalLevel, + assignments: HexAssignments, +} + +impl PartialEq for OutdoorCoverageLevel { + fn eq(&self, other: &Self) -> bool { + self.signal_power == other.signal_power + && self.seniority_timestamp == other.seniority_timestamp + } +} + +impl PartialOrd for OutdoorCoverageLevel { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for OutdoorCoverageLevel { + fn cmp(&self, other: &Self) -> Ordering { + self.signal_power + .cmp(&other.signal_power) + .reverse() + .then_with(|| self.seniority_timestamp.cmp(&other.seniority_timestamp)) + } +} + +pub fn insert_outdoor_coverage_object( + indoor: &mut OutdoorCellTree, + coverage_object: CoverageObject, +) { + for hex_coverage in coverage_object.coverage.into_iter() { + insert_outdoor_coverage( + indoor, + &coverage_object.hotspot_key, + &coverage_object.cbsd_id, + coverage_object.seniority_timestamp, + hex_coverage, + ); + } +} + +pub fn insert_outdoor_coverage( + outdoor: &mut OutdoorCellTree, + hotspot: &PublicKeyBinary, + cbsd_id: &Option, + seniority_timestamp: DateTime, + hex_coverage: UnrankedCoverage, +) { + outdoor + .entry(hex_coverage.location) + .or_default() + .push(OutdoorCoverageLevel { + hotspot_key: hotspot.clone(), + cbsd_id: cbsd_id.clone(), + seniority_timestamp, + signal_level: hex_coverage.signal_level, + signal_power: hex_coverage.signal_power, + assignments: hex_coverage.assignments, + }); +} + +pub fn clone_outdoor_coverage_into_submap( + submap: &mut OutdoorCellTree, + from: &OutdoorCellTree, + coverage_obj: &CoverageObject, +) { + for coverage in &coverage_obj.coverage { + if let Entry::Vacant(e) = submap.entry(coverage.location) { + if let Some(old_coverage_data) = from.get(&coverage.location) { + e.insert(old_coverage_data.clone()); + } + } + } +} + +pub fn into_outdoor_coverage_map( + outdoor: OutdoorCellTree, + boosted_hexes: &BoostedHexes, + epoch_start: DateTime, +) -> impl Iterator + '_ { + outdoor.into_iter().flat_map(move |(hex, radios)| { + let boosted = boosted_hexes.get_current_multiplier(hex, epoch_start); + radios + .into_sorted_vec() + .into_iter() + .take(MAX_OUTDOOR_RADIOS_PER_RES12_HEX) + .enumerate() + .flat_map(move |(rank, cov)| { + Rank::from_outdoor_index(rank).map(move |rank| { + let key = cov.hotspot_key; + let cov = RankedCoverage { + rank, + hex, + cbsd_id: cov.cbsd_id, + assignments: cov.assignments, + boosted, + signal_level: cov.signal_level, + }; + (key, cov) + }) + }) + }) +} From ee6fc573f01be738993ed3dd257ef713406fd7ac Mon Sep 17 00:00:00 2001 From: Matthew Plant Date: Thu, 30 May 2024 16:28:50 -0400 Subject: [PATCH 02/13] Add TODO with questions --- coverage_map/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/coverage_map/src/lib.rs b/coverage_map/src/lib.rs index 325359709..c6fb05949 100644 --- a/coverage_map/src/lib.rs +++ b/coverage_map/src/lib.rs @@ -38,6 +38,9 @@ impl CoverageMapBuilder { /// A submap only contains the hexes that exist in the provided `coverage_objs` arguments. This /// allows for one to determine the potential ranking of new coverage objects without having /// to clone the entire CoverageMapBuilder. + // TODO(map): Should this return a `CoverageMap` instead? I don't really see the purpose of + // having this return a `CoverageMapBuilder` since it will probably always be converted instantly + // to a `CoverageMap`. pub fn submap(&self, coverage_objs: Vec) -> Self { // A different way to implement this function would be to insert all of the coverage_objs into // the submap, and then reconstruct the coverage objs from only the relevant hexes and then @@ -75,6 +78,7 @@ impl CoverageMapBuilder { } /// Constructs a [CoverageMap] from the current `CoverageMapBuilder` + // TODO(map): Should this take &self and clone the data? pub fn build(self, boosted_hexes: &BoostedHexes, epoch_start: DateTime) -> CoverageMap { let mut hotspots = HashMap::<_, Vec>::new(); for (radio, coverage) in From ad29ef1274a509ad6479475fa023da04484981c2 Mon Sep 17 00:00:00 2001 From: Matthew Plant Date: Thu, 30 May 2024 16:35:07 -0400 Subject: [PATCH 03/13] Move around code, more TODO questions --- coverage_map/src/lib.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/coverage_map/src/lib.rs b/coverage_map/src/lib.rs index c6fb05949..bab3d211a 100644 --- a/coverage_map/src/lib.rs +++ b/coverage_map/src/lib.rs @@ -137,6 +137,7 @@ pub struct UnrankedCoverage { /// Ranked hex coverage given as output from the [CoverageMap] pub struct RankedCoverage { + // TODO(map): Does this need to indicate whether the coverage is indoor or outdoor? pub hex: Cell, pub rank: Rank, pub cbsd_id: Option, @@ -146,23 +147,13 @@ pub struct RankedCoverage { } /// Rank of the hex coverage. +// TODO(map): Should this be split into Indoor and OutdoorRank? pub enum Rank { First, Second, Third, } -#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] -pub enum SignalLevel { - None, - Low, - Medium, - High, -} - -pub const MAX_INDOOR_RADIOS_PER_RES12_HEX: usize = 1; -pub const MAX_OUTDOOR_RADIOS_PER_RES12_HEX: usize = 3; - impl Rank { pub(crate) fn from_outdoor_index(idx: usize) -> Option { match idx { @@ -177,3 +168,14 @@ impl Rank { (idx == 0).then_some(Self::First) } } + +#[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub enum SignalLevel { + None, + Low, + Medium, + High, +} + +pub const MAX_INDOOR_RADIOS_PER_RES12_HEX: usize = 1; +pub const MAX_OUTDOOR_RADIOS_PER_RES12_HEX: usize = 3; From a85e9ebe86fab8009ca243bbf6e18bf3769bcaeb Mon Sep 17 00:00:00 2001 From: Matthew Plant Date: Fri, 31 May 2024 11:32:23 -0400 Subject: [PATCH 04/13] separate get_coverage into get_wifi_coverage and get_cbrs_coverage --- coverage_map/src/indoor.rs | 21 +++++----- coverage_map/src/lib.rs | 77 +++++++++++++++++++++++++------------ coverage_map/src/outdoor.rs | 21 +++++----- 3 files changed, 70 insertions(+), 49 deletions(-) diff --git a/coverage_map/src/indoor.rs b/coverage_map/src/indoor.rs index e38341aa6..61aeac86e 100644 --- a/coverage_map/src/indoor.rs +++ b/coverage_map/src/indoor.rs @@ -94,7 +94,7 @@ pub fn into_indoor_coverage_map( indoor: IndoorCellTree, boosted_hexes: &BoostedHexes, epoch_start: DateTime, -) -> impl Iterator + '_ { +) -> impl Iterator + '_ { indoor .into_iter() .flat_map(move |(hex, mut radios)| { @@ -106,17 +106,14 @@ pub fn into_indoor_coverage_map( .take(MAX_INDOOR_RADIOS_PER_RES12_HEX) .enumerate() .flat_map(move |(rank, cov)| { - Rank::from_indoor_index(rank).map(move |rank| { - let key = cov.hotspot_key; - let cov = RankedCoverage { - hex, - rank, - cbsd_id: cov.cbsd_id, - assignments: cov.assignments, - boosted, - signal_level: cov.signal_level, - }; - (key, cov) + Rank::from_indoor_index(rank).map(move |rank| RankedCoverage { + hex, + rank, + hotspot_key: cov.hotspot_key, + cbsd_id: cov.cbsd_id, + assignments: cov.assignments, + boosted, + signal_level: cov.signal_level, }) }) }) diff --git a/coverage_map/src/lib.rs b/coverage_map/src/lib.rs index bab3d211a..513cb56f8 100644 --- a/coverage_map/src/lib.rs +++ b/coverage_map/src/lib.rs @@ -80,41 +80,67 @@ impl CoverageMapBuilder { /// Constructs a [CoverageMap] from the current `CoverageMapBuilder` // TODO(map): Should this take &self and clone the data? pub fn build(self, boosted_hexes: &BoostedHexes, epoch_start: DateTime) -> CoverageMap { - let mut hotspots = HashMap::<_, Vec>::new(); - for (radio, coverage) in - into_indoor_coverage_map(self.indoor_cbrs, boosted_hexes, epoch_start) - .chain(into_indoor_coverage_map( - self.indoor_wifi, - boosted_hexes, - epoch_start, - )) - .chain(into_outdoor_coverage_map( - self.outdoor_cbrs, - boosted_hexes, - epoch_start, - )) - .chain(into_outdoor_coverage_map( - self.outdoor_wifi, - boosted_hexes, - epoch_start, - )) + let mut wifi_hotspots = HashMap::<_, Vec>::new(); + let mut cbrs_radios = HashMap::<_, Vec>::new(); + for coverage in into_indoor_coverage_map(self.indoor_cbrs, boosted_hexes, epoch_start) + .chain(into_indoor_coverage_map( + self.indoor_wifi, + boosted_hexes, + epoch_start, + )) + .chain(into_outdoor_coverage_map( + self.outdoor_cbrs, + boosted_hexes, + epoch_start, + )) + .chain(into_outdoor_coverage_map( + self.outdoor_wifi, + boosted_hexes, + epoch_start, + )) { - hotspots.entry(radio).or_default().push(coverage); + if let Some(ref cbsd_id) = coverage.cbsd_id { + cbrs_radios + .entry(cbsd_id.clone()) + .or_default() + .push(coverage); + } else { + wifi_hotspots + .entry(coverage.hotspot_key.clone()) + .or_default() + .push(coverage); + } + } + CoverageMap { + wifi_hotspots, + cbrs_radios, } - CoverageMap { hotspots } } } -/// Data structure from mapping hotspots to their ranked hex coverage +/// Data structure from mapping radios to their ranked hex coverage pub struct CoverageMap { - hotspots: HashMap>, + wifi_hotspots: HashMap>, + cbrs_radios: HashMap>, } impl CoverageMap { - /// Returns the hexes covered by the hotspot. The returned slice can be empty, indicating that + /// Returns the hexes covered by the WiFi hotspot. The returned slice can be empty, indicating that /// the hotspot did not meet the criteria to be ranked in any hex. - pub fn get_coverage(&self, hotspot: &PublicKeyBinary) -> &[RankedCoverage] { - self.hotspots.get(hotspot).map(Vec::as_slice).unwrap_or(&[]) + pub fn get_wifi_coverage(&self, wifi_hotspot: &PublicKeyBinary) -> &[RankedCoverage] { + self.wifi_hotspots + .get(wifi_hotspot) + .map(Vec::as_slice) + .unwrap_or(&[]) + } + + /// Returns the hexes covered by the CBRS radio. The returned slice can be empty, indicating that + /// the radio did not meet the criteria to be ranked in any hex. + pub fn get_cbrs_coverage(&self, cbrs_radio: &str) -> &[RankedCoverage] { + self.cbrs_radios + .get(cbrs_radio) + .map(Vec::as_slice) + .unwrap_or(&[]) } } @@ -140,6 +166,7 @@ pub struct RankedCoverage { // TODO(map): Does this need to indicate whether the coverage is indoor or outdoor? pub hex: Cell, pub rank: Rank, + pub hotspot_key: PublicKeyBinary, pub cbsd_id: Option, pub assignments: HexAssignments, pub boosted: Option, diff --git a/coverage_map/src/outdoor.rs b/coverage_map/src/outdoor.rs index 1310d74ef..8d9669d18 100644 --- a/coverage_map/src/outdoor.rs +++ b/coverage_map/src/outdoor.rs @@ -102,7 +102,7 @@ pub fn into_outdoor_coverage_map( outdoor: OutdoorCellTree, boosted_hexes: &BoostedHexes, epoch_start: DateTime, -) -> impl Iterator + '_ { +) -> impl Iterator + '_ { outdoor.into_iter().flat_map(move |(hex, radios)| { let boosted = boosted_hexes.get_current_multiplier(hex, epoch_start); radios @@ -111,17 +111,14 @@ pub fn into_outdoor_coverage_map( .take(MAX_OUTDOOR_RADIOS_PER_RES12_HEX) .enumerate() .flat_map(move |(rank, cov)| { - Rank::from_outdoor_index(rank).map(move |rank| { - let key = cov.hotspot_key; - let cov = RankedCoverage { - rank, - hex, - cbsd_id: cov.cbsd_id, - assignments: cov.assignments, - boosted, - signal_level: cov.signal_level, - }; - (key, cov) + Rank::from_outdoor_index(rank).map(move |rank| RankedCoverage { + rank, + hex, + hotspot_key: cov.hotspot_key, + cbsd_id: cov.cbsd_id, + assignments: cov.assignments, + boosted, + signal_level: cov.signal_level, }) }) }) From 496d22810f6e50e79ce7b5514caf25bc745bca6c Mon Sep 17 00:00:00 2001 From: Matthew Plant Date: Fri, 31 May 2024 12:47:24 -0400 Subject: [PATCH 05/13] Add test for outdoor radios --- coverage_map/src/lib.rs | 1 + coverage_map/src/outdoor.rs | 76 ++++++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/coverage_map/src/lib.rs b/coverage_map/src/lib.rs index 513cb56f8..1da6fb594 100644 --- a/coverage_map/src/lib.rs +++ b/coverage_map/src/lib.rs @@ -175,6 +175,7 @@ pub struct RankedCoverage { /// Rank of the hex coverage. // TODO(map): Should this be split into Indoor and OutdoorRank? +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum Rank { First, Second, diff --git a/coverage_map/src/outdoor.rs b/coverage_map/src/outdoor.rs index 8d9669d18..e917d52a2 100644 --- a/coverage_map/src/outdoor.rs +++ b/coverage_map/src/outdoor.rs @@ -50,12 +50,12 @@ impl Ord for OutdoorCoverageLevel { } pub fn insert_outdoor_coverage_object( - indoor: &mut OutdoorCellTree, + outdoor: &mut OutdoorCellTree, coverage_object: CoverageObject, ) { for hex_coverage in coverage_object.coverage.into_iter() { insert_outdoor_coverage( - indoor, + outdoor, &coverage_object.hotspot_key, &coverage_object.cbsd_id, coverage_object.seniority_timestamp, @@ -123,3 +123,75 @@ pub fn into_outdoor_coverage_map( }) }) } + +#[cfg(test)] +mod test { + use super::*; + use crate::*; + use chrono::NaiveDate; + use hex_assignments::Assignment; + use hextree::Cell; + + #[test] + fn ensure_outdoor_radios_ranked_by_power() { + let mut outdoor_coverage = OutdoorCellTree::default(); + for cov_obj in vec![ + outdoor_cbrs_coverage("1", -946, date(2022, 8, 1)), + outdoor_cbrs_coverage("2", -936, date(2022, 12, 5)), + outdoor_cbrs_coverage("3", -887, date(2022, 12, 2)), + outdoor_cbrs_coverage("4", -887, date(2022, 12, 1)), + outdoor_cbrs_coverage("5", -773, date(2023, 5, 1)), + ] + .into_iter() + { + insert_outdoor_coverage_object(&mut outdoor_coverage, cov_obj); + } + let ranked: HashMap<_, _> = + into_outdoor_coverage_map(outdoor_coverage, &BoostedHexes::default(), Utc::now()) + .map(|x| (x.cbsd_id.clone().unwrap(), x)) + .collect(); + assert_eq!(ranked.get("5").unwrap().rank, Rank::First); + assert_eq!(ranked.get("4").unwrap().rank, Rank::Second); + assert_eq!(ranked.get("3").unwrap().rank, Rank::Third); + assert!(ranked.get("1").is_none()); + assert!(ranked.get("2").is_none()); + } + + fn hex_assignments_mock() -> HexAssignments { + HexAssignments { + footfall: Assignment::A, + urbanized: Assignment::A, + landtype: Assignment::A, + } + } + + fn date(year: i32, month: u32, day: u32) -> DateTime { + NaiveDate::from_ymd_opt(year, month, day) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap() + .and_utc() + } + + fn outdoor_cbrs_coverage( + cbsd_id: &str, + signal_power: i32, + seniority_timestamp: DateTime, + ) -> CoverageObject { + let owner: PublicKeyBinary = "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6" + .parse() + .expect("failed owner parse"); + CoverageObject { + indoor: false, + hotspot_key: owner, + seniority_timestamp, + cbsd_id: Some(cbsd_id.to_string()), + coverage: vec![UnrankedCoverage { + location: Cell::from_raw(0x8a1fb46622dffff).expect("valid h3 cell"), + signal_power, + signal_level: SignalLevel::High, + assignments: hex_assignments_mock(), + }], + } + } +} From e1829381f426a09207458269b07de7ee3ea0bfb9 Mon Sep 17 00:00:00 2001 From: Matthew Plant Date: Fri, 31 May 2024 13:54:10 -0400 Subject: [PATCH 06/13] Add indoor tests --- coverage_map/src/indoor.rs | 125 +++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/coverage_map/src/indoor.rs b/coverage_map/src/indoor.rs index 61aeac86e..5e6d57713 100644 --- a/coverage_map/src/indoor.rs +++ b/coverage_map/src/indoor.rs @@ -120,3 +120,128 @@ pub fn into_indoor_coverage_map( }) .flatten() } + +#[cfg(test)] +mod test { + use super::*; + use crate::*; + use chrono::NaiveDate; + use hex_assignments::Assignment; + use hextree::Cell; + + #[test] + fn ensure_max_signal_level_selected() { + let mut indoor_coverage = IndoorCellTree::default(); + for cov_obj in vec![ + indoor_cbrs_coverage("1", SignalLevel::None), + indoor_cbrs_coverage("2", SignalLevel::Low), + indoor_cbrs_coverage("3", SignalLevel::High), + indoor_cbrs_coverage("4", SignalLevel::Low), + indoor_cbrs_coverage("5", SignalLevel::None), + ] + .into_iter() + { + insert_indoor_coverage_object(&mut indoor_coverage, cov_obj); + } + let ranked: HashMap<_, _> = + into_indoor_coverage_map(indoor_coverage, &BoostedHexes::default(), Utc::now()) + .map(|x| (x.cbsd_id.clone().unwrap(), x)) + .collect(); + assert_eq!(ranked.get("3").unwrap().rank, Rank::First); + assert!(ranked.get("1").is_none()); + assert!(ranked.get("2").is_none()); + assert!(ranked.get("4").is_none()); + assert!(ranked.get("5").is_none()); + } + + #[test] + fn ensure_oldest_radio_selected() { + let mut indoor_coverage = IndoorCellTree::default(); + for cov_obj in vec![ + indoor_cbrs_coverage_with_date("1", SignalLevel::High, date(1980, 1, 1)), + indoor_cbrs_coverage_with_date("2", SignalLevel::High, date(1970, 1, 5)), + indoor_cbrs_coverage_with_date("3", SignalLevel::High, date(1990, 2, 2)), + indoor_cbrs_coverage_with_date("4", SignalLevel::High, date(1970, 1, 4)), + indoor_cbrs_coverage_with_date("5", SignalLevel::High, date(1975, 3, 3)), + indoor_cbrs_coverage_with_date("6", SignalLevel::High, date(1970, 1, 3)), + indoor_cbrs_coverage_with_date("7", SignalLevel::High, date(1974, 2, 2)), + indoor_cbrs_coverage_with_date("8", SignalLevel::High, date(1970, 1, 2)), + indoor_cbrs_coverage_with_date("9", SignalLevel::High, date(1976, 5, 2)), + indoor_cbrs_coverage_with_date("10", SignalLevel::High, date(1970, 1, 1)), + ] + .into_iter() + { + insert_indoor_coverage_object(&mut indoor_coverage, cov_obj); + } + let ranked: HashMap<_, _> = + into_indoor_coverage_map(indoor_coverage, &BoostedHexes::default(), Utc::now()) + .map(|x| (x.cbsd_id.clone().unwrap(), x)) + .collect(); + assert_eq!(ranked.get("10").unwrap().rank, Rank::First); + assert!(ranked.get("1").is_none()); + assert!(ranked.get("2").is_none()); + assert!(ranked.get("3").is_none()); + assert!(ranked.get("4").is_none()); + assert!(ranked.get("5").is_none()); + assert!(ranked.get("6").is_none()); + assert!(ranked.get("7").is_none()); + assert!(ranked.get("8").is_none()); + assert!(ranked.get("9").is_none()); + } + + fn hex_assignments_mock() -> HexAssignments { + HexAssignments { + footfall: Assignment::A, + urbanized: Assignment::A, + landtype: Assignment::A, + } + } + + fn date(year: i32, month: u32, day: u32) -> DateTime { + NaiveDate::from_ymd_opt(year, month, day) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap() + .and_utc() + } + + fn indoor_cbrs_coverage(cbsd_id: &str, signal_level: SignalLevel) -> CoverageObject { + let owner: PublicKeyBinary = "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6" + .parse() + .expect("failed owner parse"); + CoverageObject { + indoor: true, + hotspot_key: owner, + seniority_timestamp: Utc::now(), + cbsd_id: Some(cbsd_id.to_string()), + coverage: vec![UnrankedCoverage { + location: Cell::from_raw(0x8a1fb46622dffff).expect("valid h3 cell"), + signal_power: 0, + signal_level, + assignments: hex_assignments_mock(), + }], + } + } + + fn indoor_cbrs_coverage_with_date( + cbsd_id: &str, + signal_level: SignalLevel, + seniority_timestamp: DateTime, + ) -> CoverageObject { + let owner: PublicKeyBinary = "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6" + .parse() + .expect("failed owner parse"); + CoverageObject { + indoor: true, + hotspot_key: owner, + seniority_timestamp, + cbsd_id: Some(cbsd_id.to_string()), + coverage: vec![UnrankedCoverage { + location: Cell::from_raw(0x8a1fb46622dffff).expect("valid h3 cell"), + signal_power: 0, + signal_level, + assignments: hex_assignments_mock(), + }], + } + } +} From a1cc6a5287b75673446aaecfa2f5731c1b620859 Mon Sep 17 00:00:00 2001 From: Matthew Plant Date: Fri, 31 May 2024 14:22:28 -0400 Subject: [PATCH 07/13] Change rank to base-1 --- coverage_map/src/indoor.rs | 84 ++++++++++++++++++++----------------- coverage_map/src/lib.rs | 37 ++++------------ coverage_map/src/outdoor.rs | 35 +++++++--------- 3 files changed, 67 insertions(+), 89 deletions(-) diff --git a/coverage_map/src/indoor.rs b/coverage_map/src/indoor.rs index 5e6d57713..564e4acd2 100644 --- a/coverage_map/src/indoor.rs +++ b/coverage_map/src/indoor.rs @@ -9,10 +9,7 @@ use hex_assignments::assignment::HexAssignments; use hextree::Cell; use mobile_config::boosted_hex_info::BoostedHexes; -use crate::{ - CoverageObject, Rank, RankedCoverage, SignalLevel, UnrankedCoverage, - MAX_INDOOR_RADIOS_PER_RES12_HEX, -}; +use crate::{CoverageObject, RankedCoverage, SignalLevel, UnrankedCoverage}; pub type IndoorCellTree = HashMap>>; @@ -97,28 +94,25 @@ pub fn into_indoor_coverage_map( ) -> impl Iterator + '_ { indoor .into_iter() - .flat_map(move |(hex, mut radios)| { + .flat_map(move |(hex, radios)| { let boosted = boosted_hexes.get_current_multiplier(hex, epoch_start); - radios.pop_last().map(move |(_, radios)| { - radios - .into_sorted_vec() - .into_iter() - .take(MAX_INDOOR_RADIOS_PER_RES12_HEX) - .enumerate() - .flat_map(move |(rank, cov)| { - Rank::from_indoor_index(rank).map(move |rank| RankedCoverage { - hex, - rank, - hotspot_key: cov.hotspot_key, - cbsd_id: cov.cbsd_id, - assignments: cov.assignments, - boosted, - signal_level: cov.signal_level, - }) - }) - }) + radios + .into_values() + .rev() + .flat_map(move |radios| radios.into_sorted_vec().into_iter()) + .map(move |cov| (hex, boosted, cov)) + }) + .enumerate() + .map(move |(rank, (hex, boosted, cov))| RankedCoverage { + hex, + rank: rank + 1, + indoor: true, + hotspot_key: cov.hotspot_key, + cbsd_id: cov.cbsd_id, + assignments: cov.assignments, + boosted, + signal_level: cov.signal_level, }) - .flatten() } #[cfg(test)] @@ -147,11 +141,23 @@ mod test { into_indoor_coverage_map(indoor_coverage, &BoostedHexes::default(), Utc::now()) .map(|x| (x.cbsd_id.clone().unwrap(), x)) .collect(); - assert_eq!(ranked.get("3").unwrap().rank, Rank::First); - assert!(ranked.get("1").is_none()); - assert!(ranked.get("2").is_none()); - assert!(ranked.get("4").is_none()); - assert!(ranked.get("5").is_none()); + assert_eq!(ranked.get("3").unwrap().rank, 1); + assert!({ + let rank = ranked.get("2").unwrap().rank; + rank == 2 || rank == 3 + }); + assert!({ + let rank = ranked.get("4").unwrap().rank; + rank == 2 || rank == 3 + }); + assert!({ + let rank = ranked.get("1").unwrap().rank; + rank == 4 || rank == 5 + }); + assert!({ + let rank = ranked.get("5").unwrap().rank; + rank == 4 || rank == 5 + }); } #[test] @@ -177,16 +183,16 @@ mod test { into_indoor_coverage_map(indoor_coverage, &BoostedHexes::default(), Utc::now()) .map(|x| (x.cbsd_id.clone().unwrap(), x)) .collect(); - assert_eq!(ranked.get("10").unwrap().rank, Rank::First); - assert!(ranked.get("1").is_none()); - assert!(ranked.get("2").is_none()); - assert!(ranked.get("3").is_none()); - assert!(ranked.get("4").is_none()); - assert!(ranked.get("5").is_none()); - assert!(ranked.get("6").is_none()); - assert!(ranked.get("7").is_none()); - assert!(ranked.get("8").is_none()); - assert!(ranked.get("9").is_none()); + assert_eq!(ranked.get("1").unwrap().rank, 9); + assert_eq!(ranked.get("2").unwrap().rank, 5); + assert_eq!(ranked.get("3").unwrap().rank, 10); + assert_eq!(ranked.get("4").unwrap().rank, 4); + assert_eq!(ranked.get("5").unwrap().rank, 7); + assert_eq!(ranked.get("6").unwrap().rank, 3); + assert_eq!(ranked.get("7").unwrap().rank, 6); + assert_eq!(ranked.get("8").unwrap().rank, 2); + assert_eq!(ranked.get("9").unwrap().rank, 8); + assert_eq!(ranked.get("10").unwrap().rank, 1); } fn hex_assignments_mock() -> HexAssignments { diff --git a/coverage_map/src/lib.rs b/coverage_map/src/lib.rs index 1da6fb594..5803e4a4d 100644 --- a/coverage_map/src/lib.rs +++ b/coverage_map/src/lib.rs @@ -14,7 +14,7 @@ use outdoor::*; /// Data structure for keeping track of the ranking the coverage in each hex cell for indoor /// and outdoor CBRS and WiFi radios. -#[derive(Default)] +#[derive(Clone, Default, Debug)] pub struct CoverageMapBuilder { indoor_cbrs: IndoorCellTree, indoor_wifi: IndoorCellTree, @@ -78,7 +78,6 @@ impl CoverageMapBuilder { } /// Constructs a [CoverageMap] from the current `CoverageMapBuilder` - // TODO(map): Should this take &self and clone the data? pub fn build(self, boosted_hexes: &BoostedHexes, epoch_start: DateTime) -> CoverageMap { let mut wifi_hotspots = HashMap::<_, Vec>::new(); let mut cbrs_radios = HashMap::<_, Vec>::new(); @@ -119,6 +118,7 @@ impl CoverageMapBuilder { } /// Data structure from mapping radios to their ranked hex coverage +#[derive(Clone, Default, Debug)] pub struct CoverageMap { wifi_hotspots: HashMap>, cbrs_radios: HashMap>, @@ -145,6 +145,7 @@ impl CoverageMap { } /// Coverage data given as input to the [CoverageMapBuilder] +#[derive(Clone, Debug)] pub struct CoverageObject { pub indoor: bool, pub hotspot_key: PublicKeyBinary, @@ -154,6 +155,7 @@ pub struct CoverageObject { } /// Unranked hex coverage data given as input to the [CoverageMapBuilder] +#[derive(Clone, Debug)] pub struct UnrankedCoverage { pub location: Cell, pub signal_power: i32, @@ -162,10 +164,12 @@ pub struct UnrankedCoverage { } /// Ranked hex coverage given as output from the [CoverageMap] +#[derive(Clone, Debug)] pub struct RankedCoverage { // TODO(map): Does this need to indicate whether the coverage is indoor or outdoor? pub hex: Cell, - pub rank: Rank, + pub rank: usize, + pub indoor: bool, pub hotspot_key: PublicKeyBinary, pub cbsd_id: Option, pub assignments: HexAssignments, @@ -173,30 +177,6 @@ pub struct RankedCoverage { pub signal_level: SignalLevel, } -/// Rank of the hex coverage. -// TODO(map): Should this be split into Indoor and OutdoorRank? -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub enum Rank { - First, - Second, - Third, -} - -impl Rank { - pub(crate) fn from_outdoor_index(idx: usize) -> Option { - match idx { - 0 => Some(Self::First), - 1 => Some(Self::Second), - 2 => Some(Self::Third), - _ => None, - } - } - - pub(crate) fn from_indoor_index(idx: usize) -> Option { - (idx == 0).then_some(Self::First) - } -} - #[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] pub enum SignalLevel { None, @@ -204,6 +184,3 @@ pub enum SignalLevel { Medium, High, } - -pub const MAX_INDOOR_RADIOS_PER_RES12_HEX: usize = 1; -pub const MAX_OUTDOOR_RADIOS_PER_RES12_HEX: usize = 3; diff --git a/coverage_map/src/outdoor.rs b/coverage_map/src/outdoor.rs index e917d52a2..fcbc00ca7 100644 --- a/coverage_map/src/outdoor.rs +++ b/coverage_map/src/outdoor.rs @@ -9,10 +9,7 @@ use hex_assignments::assignment::HexAssignments; use hextree::Cell; use mobile_config::boosted_hex_info::BoostedHexes; -use crate::{ - CoverageObject, Rank, RankedCoverage, SignalLevel, UnrankedCoverage, - MAX_OUTDOOR_RADIOS_PER_RES12_HEX, -}; +use crate::{CoverageObject, RankedCoverage, SignalLevel, UnrankedCoverage}; /// Data structure for storing outdoor radios ranked by their coverage level pub type OutdoorCellTree = HashMap>; @@ -108,18 +105,16 @@ pub fn into_outdoor_coverage_map( radios .into_sorted_vec() .into_iter() - .take(MAX_OUTDOOR_RADIOS_PER_RES12_HEX) .enumerate() - .flat_map(move |(rank, cov)| { - Rank::from_outdoor_index(rank).map(move |rank| RankedCoverage { - rank, - hex, - hotspot_key: cov.hotspot_key, - cbsd_id: cov.cbsd_id, - assignments: cov.assignments, - boosted, - signal_level: cov.signal_level, - }) + .map(move |(rank, cov)| RankedCoverage { + hex, + rank: rank + 1, + indoor: false, + hotspot_key: cov.hotspot_key, + cbsd_id: cov.cbsd_id, + assignments: cov.assignments, + boosted, + signal_level: cov.signal_level, }) }) } @@ -150,11 +145,11 @@ mod test { into_outdoor_coverage_map(outdoor_coverage, &BoostedHexes::default(), Utc::now()) .map(|x| (x.cbsd_id.clone().unwrap(), x)) .collect(); - assert_eq!(ranked.get("5").unwrap().rank, Rank::First); - assert_eq!(ranked.get("4").unwrap().rank, Rank::Second); - assert_eq!(ranked.get("3").unwrap().rank, Rank::Third); - assert!(ranked.get("1").is_none()); - assert!(ranked.get("2").is_none()); + assert_eq!(ranked.get("5").unwrap().rank, 1); + assert_eq!(ranked.get("4").unwrap().rank, 2); + assert_eq!(ranked.get("3").unwrap().rank, 3); + assert_eq!(ranked.get("1").unwrap().rank, 5); + assert_eq!(ranked.get("2").unwrap().rank, 4); } fn hex_assignments_mock() -> HexAssignments { From c25e2223f7bcc88c669061cd4fff5848864b7d65 Mon Sep 17 00:00:00 2001 From: Matthew Plant Date: Fri, 31 May 2024 14:32:53 -0400 Subject: [PATCH 08/13] Make mobile config depend on coverage map and not the other way around --- Cargo.lock | 2 +- coverage_map/Cargo.toml | 3 +-- coverage_map/src/indoor.rs | 9 ++++----- coverage_map/src/lib.rs | 17 +++++++++++++++-- coverage_map/src/outdoor.rs | 7 +++---- mobile_config/Cargo.toml | 1 + mobile_config/src/boosted_hex_info.rs | 6 ++++++ 7 files changed, 31 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d94501eaa..bd2fbd6d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2131,7 +2131,6 @@ dependencies = [ "helium-proto", "hex-assignments", "hextree", - "mobile-config", "uuid", ] @@ -4616,6 +4615,7 @@ dependencies = [ "chrono", "clap 4.4.8", "config", + "coverage_map", "custom-tracing", "db-store", "file-store", diff --git a/coverage_map/Cargo.toml b/coverage_map/Cargo.toml index 66eda3374..d08ec321e 100644 --- a/coverage_map/Cargo.toml +++ b/coverage_map/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "coverage_map" +name = "coverage-map" version = "0.1.0" authors.workspace = true license.workspace = true @@ -14,5 +14,4 @@ helium-crypto = { workspace = true } helium-proto = { workspace = true } hex-assignments = { path = "../hex_assignments" } hextree = { workspace = true } -mobile-config = { path = "../mobile_config" } uuid = { workspace = true } diff --git a/coverage_map/src/indoor.rs b/coverage_map/src/indoor.rs index 564e4acd2..4f4b50293 100644 --- a/coverage_map/src/indoor.rs +++ b/coverage_map/src/indoor.rs @@ -7,9 +7,8 @@ use chrono::{DateTime, Utc}; use helium_crypto::PublicKeyBinary; use hex_assignments::assignment::HexAssignments; use hextree::Cell; -use mobile_config::boosted_hex_info::BoostedHexes; -use crate::{CoverageObject, RankedCoverage, SignalLevel, UnrankedCoverage}; +use crate::{BoostedHexMap, CoverageObject, RankedCoverage, SignalLevel, UnrankedCoverage}; pub type IndoorCellTree = HashMap>>; @@ -89,7 +88,7 @@ pub fn clone_indoor_coverage_into_submap( pub fn into_indoor_coverage_map( indoor: IndoorCellTree, - boosted_hexes: &BoostedHexes, + boosted_hexes: &impl BoostedHexMap, epoch_start: DateTime, ) -> impl Iterator + '_ { indoor @@ -138,7 +137,7 @@ mod test { insert_indoor_coverage_object(&mut indoor_coverage, cov_obj); } let ranked: HashMap<_, _> = - into_indoor_coverage_map(indoor_coverage, &BoostedHexes::default(), Utc::now()) + into_indoor_coverage_map(indoor_coverage, &NoBoostedHexes, Utc::now()) .map(|x| (x.cbsd_id.clone().unwrap(), x)) .collect(); assert_eq!(ranked.get("3").unwrap().rank, 1); @@ -180,7 +179,7 @@ mod test { insert_indoor_coverage_object(&mut indoor_coverage, cov_obj); } let ranked: HashMap<_, _> = - into_indoor_coverage_map(indoor_coverage, &BoostedHexes::default(), Utc::now()) + into_indoor_coverage_map(indoor_coverage, &NoBoostedHexes, Utc::now()) .map(|x| (x.cbsd_id.clone().unwrap(), x)) .collect(); assert_eq!(ranked.get("1").unwrap().rank, 9); diff --git a/coverage_map/src/lib.rs b/coverage_map/src/lib.rs index 5803e4a4d..48ab86199 100644 --- a/coverage_map/src/lib.rs +++ b/coverage_map/src/lib.rs @@ -4,7 +4,6 @@ use chrono::{DateTime, Utc}; use helium_crypto::PublicKeyBinary; use hex_assignments::assignment::HexAssignments; use hextree::Cell; -use mobile_config::boosted_hex_info::BoostedHexes; mod indoor; mod outdoor; @@ -78,7 +77,7 @@ impl CoverageMapBuilder { } /// Constructs a [CoverageMap] from the current `CoverageMapBuilder` - pub fn build(self, boosted_hexes: &BoostedHexes, epoch_start: DateTime) -> CoverageMap { + pub fn build(self, boosted_hexes: &impl BoostedHexMap, epoch_start: DateTime) -> CoverageMap { let mut wifi_hotspots = HashMap::<_, Vec>::new(); let mut cbrs_radios = HashMap::<_, Vec>::new(); for coverage in into_indoor_coverage_map(self.indoor_cbrs, boosted_hexes, epoch_start) @@ -184,3 +183,17 @@ pub enum SignalLevel { Medium, High, } + +pub trait BoostedHexMap { + fn get_current_multiplier(&self, cell: Cell, ts: DateTime) -> Option; +} + +#[cfg(test)] +pub(crate) struct NoBoostedHexes; + +#[cfg(test)] +impl BoostedHexMap for NoBoostedHexes { + fn get_current_multiplier(&self, _cell: Cell, _ts: DateTime) -> Option { + None + } +} diff --git a/coverage_map/src/outdoor.rs b/coverage_map/src/outdoor.rs index fcbc00ca7..12bf9cc44 100644 --- a/coverage_map/src/outdoor.rs +++ b/coverage_map/src/outdoor.rs @@ -7,9 +7,8 @@ use chrono::{DateTime, Utc}; use helium_crypto::PublicKeyBinary; use hex_assignments::assignment::HexAssignments; use hextree::Cell; -use mobile_config::boosted_hex_info::BoostedHexes; -use crate::{CoverageObject, RankedCoverage, SignalLevel, UnrankedCoverage}; +use crate::{BoostedHexMap, CoverageObject, RankedCoverage, SignalLevel, UnrankedCoverage}; /// Data structure for storing outdoor radios ranked by their coverage level pub type OutdoorCellTree = HashMap>; @@ -97,7 +96,7 @@ pub fn clone_outdoor_coverage_into_submap( pub fn into_outdoor_coverage_map( outdoor: OutdoorCellTree, - boosted_hexes: &BoostedHexes, + boosted_hexes: &impl BoostedHexMap, epoch_start: DateTime, ) -> impl Iterator + '_ { outdoor.into_iter().flat_map(move |(hex, radios)| { @@ -142,7 +141,7 @@ mod test { insert_outdoor_coverage_object(&mut outdoor_coverage, cov_obj); } let ranked: HashMap<_, _> = - into_outdoor_coverage_map(outdoor_coverage, &BoostedHexes::default(), Utc::now()) + into_outdoor_coverage_map(outdoor_coverage, &NoBoostedHexes, Utc::now()) .map(|x| (x.cbsd_id.clone().unwrap(), x)) .collect(); assert_eq!(ranked.get("5").unwrap().rank, 1); diff --git a/mobile_config/Cargo.toml b/mobile_config/Cargo.toml index 87b55125c..b1a396c35 100644 --- a/mobile_config/Cargo.toml +++ b/mobile_config/Cargo.toml @@ -45,6 +45,7 @@ task-manager = { path = "../task_manager" } solana-sdk = { workspace = true } custom-tracing = { path = "../custom_tracing", features = ["grpc"] } humantime-serde = { workspace = true } +coverage-map = { path = "../coverage_map" } [dev-dependencies] rand = { workspace = true } diff --git a/mobile_config/src/boosted_hex_info.rs b/mobile_config/src/boosted_hex_info.rs index 6a745e346..cedb34fd9 100644 --- a/mobile_config/src/boosted_hex_info.rs +++ b/mobile_config/src/boosted_hex_info.rs @@ -177,6 +177,12 @@ impl BoostedHexes { } } +impl coverage_map::BoostedHexMap for BoostedHexes { + fn get_current_multiplier(&self, location: Cell, ts: DateTime) -> Option { + self.get_current_multiplier(location, ts) + } +} + pub(crate) mod db { use super::{to_end_ts, to_start_ts, BoostedHexInfo}; use chrono::{DateTime, Duration, Utc}; From b7937058b15ef3cfb41e62db90f82a334e056edb Mon Sep 17 00:00:00 2001 From: Matthew Plant Date: Fri, 31 May 2024 14:35:38 -0400 Subject: [PATCH 09/13] Fmt --- Cargo.lock | 4 ++-- coverage_map/src/lib.rs | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd2fbd6d3..da0281013 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2122,7 +2122,7 @@ dependencies = [ ] [[package]] -name = "coverage_map" +name = "coverage-map" version = "0.1.0" dependencies = [ "chrono", @@ -4615,7 +4615,7 @@ dependencies = [ "chrono", "clap 4.4.8", "config", - "coverage_map", + "coverage-map", "custom-tracing", "db-store", "file-store", diff --git a/coverage_map/src/lib.rs b/coverage_map/src/lib.rs index 48ab86199..02e91537e 100644 --- a/coverage_map/src/lib.rs +++ b/coverage_map/src/lib.rs @@ -77,7 +77,11 @@ impl CoverageMapBuilder { } /// Constructs a [CoverageMap] from the current `CoverageMapBuilder` - pub fn build(self, boosted_hexes: &impl BoostedHexMap, epoch_start: DateTime) -> CoverageMap { + pub fn build( + self, + boosted_hexes: &impl BoostedHexMap, + epoch_start: DateTime, + ) -> CoverageMap { let mut wifi_hotspots = HashMap::<_, Vec>::new(); let mut cbrs_radios = HashMap::<_, Vec>::new(); for coverage in into_indoor_coverage_map(self.indoor_cbrs, boosted_hexes, epoch_start) From 82f9c1411db88b1e60b9e0e3800b80964f4503c2 Mon Sep 17 00:00:00 2001 From: Matthew Plant Date: Fri, 31 May 2024 15:05:11 -0400 Subject: [PATCH 10/13] move things around --- coverage_map/src/indoor.rs | 2 -- coverage_map/src/lib.rs | 8 +++----- coverage_map/src/outdoor.rs | 1 - 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/coverage_map/src/indoor.rs b/coverage_map/src/indoor.rs index 4f4b50293..9b52d20eb 100644 --- a/coverage_map/src/indoor.rs +++ b/coverage_map/src/indoor.rs @@ -97,7 +97,6 @@ pub fn into_indoor_coverage_map( let boosted = boosted_hexes.get_current_multiplier(hex, epoch_start); radios .into_values() - .rev() .flat_map(move |radios| radios.into_sorted_vec().into_iter()) .map(move |cov| (hex, boosted, cov)) }) @@ -105,7 +104,6 @@ pub fn into_indoor_coverage_map( .map(move |(rank, (hex, boosted, cov))| RankedCoverage { hex, rank: rank + 1, - indoor: true, hotspot_key: cov.hotspot_key, cbsd_id: cov.cbsd_id, assignments: cov.assignments, diff --git a/coverage_map/src/lib.rs b/coverage_map/src/lib.rs index 02e91537e..ad04c22dd 100644 --- a/coverage_map/src/lib.rs +++ b/coverage_map/src/lib.rs @@ -169,10 +169,8 @@ pub struct UnrankedCoverage { /// Ranked hex coverage given as output from the [CoverageMap] #[derive(Clone, Debug)] pub struct RankedCoverage { - // TODO(map): Does this need to indicate whether the coverage is indoor or outdoor? pub hex: Cell, pub rank: usize, - pub indoor: bool, pub hotspot_key: PublicKeyBinary, pub cbsd_id: Option, pub assignments: HexAssignments, @@ -182,10 +180,10 @@ pub struct RankedCoverage { #[derive(Copy, Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] pub enum SignalLevel { - None, - Low, - Medium, High, + Medium, + Low, + None, } pub trait BoostedHexMap { diff --git a/coverage_map/src/outdoor.rs b/coverage_map/src/outdoor.rs index 12bf9cc44..8105cf4c4 100644 --- a/coverage_map/src/outdoor.rs +++ b/coverage_map/src/outdoor.rs @@ -108,7 +108,6 @@ pub fn into_outdoor_coverage_map( .map(move |(rank, cov)| RankedCoverage { hex, rank: rank + 1, - indoor: false, hotspot_key: cov.hotspot_key, cbsd_id: cov.cbsd_id, assignments: cov.assignments, From c5485deaefd74e4f388658d692397abd05cb1892 Mon Sep 17 00:00:00 2001 From: Matthew Plant Date: Tue, 4 Jun 2024 11:25:11 -0400 Subject: [PATCH 11/13] Add tests for submap builder method --- coverage_map/src/lib.rs | 219 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) diff --git a/coverage_map/src/lib.rs b/coverage_map/src/lib.rs index ad04c22dd..536b227bc 100644 --- a/coverage_map/src/lib.rs +++ b/coverage_map/src/lib.rs @@ -199,3 +199,222 @@ impl BoostedHexMap for NoBoostedHexes { None } } + +#[cfg(test)] +mod test { + use super::*; + use hex_assignments::Assignment; + + #[test] + fn test_indoor_cbrs_submap() { + let mut coverage_map_builder = CoverageMapBuilder::default(); + coverage_map_builder.insert_coverage_object(indoor_cbrs_coverage( + "1", + 0x8a1fb46622dffff, + SignalLevel::High, + )); + coverage_map_builder.insert_coverage_object(indoor_cbrs_coverage( + "2", + 0x8c2681a3064d9ff, + SignalLevel::Low, + )); + let submap_builder = coverage_map_builder.submap(vec![indoor_cbrs_coverage( + "3", + 0x8c2681a3064d9ff, + SignalLevel::High, + )]); + let submap = submap_builder.build(&NoBoostedHexes, Utc::now()); + let cov_1 = submap.get_cbrs_coverage("1"); + assert_eq!(cov_1.len(), 0); + let cov_2 = submap.get_cbrs_coverage("2"); + assert_eq!(cov_2.len(), 1); + assert_eq!(cov_2[0].rank, 2); + let cov_3 = submap.get_cbrs_coverage("3"); + assert_eq!(cov_3.len(), 1); + assert_eq!(cov_3[0].rank, 1); + } + + #[test] + fn test_indoor_wifi_submap() { + let mut coverage_map_builder = CoverageMapBuilder::default(); + coverage_map_builder.insert_coverage_object(indoor_wifi_coverage( + "11xtYwQYnvkFYnJ9iZ8kmnetYKwhdi87Mcr36e1pVLrhBMPLjV9", + 0x8a1fb46622dffff, + SignalLevel::High, + )); + coverage_map_builder.insert_coverage_object(indoor_wifi_coverage( + "11PGVtgW9aM9ynfvns5USUsynYQ7EsMpxVqWuDKqFogKQX7etkR", + 0x8c2681a3064d9ff, + SignalLevel::Low, + )); + let submap_builder = coverage_map_builder.submap(vec![indoor_wifi_coverage( + "11ibmJmQXTL6qMh4cq9pJ7tUtrpafWaVjjT6qhY7CNvjyvY9g1", + 0x8c2681a3064d9ff, + SignalLevel::High, + )]); + let submap = submap_builder.build(&NoBoostedHexes, Utc::now()); + let cov_1 = submap.get_wifi_coverage( + &"11xtYwQYnvkFYnJ9iZ8kmnetYKwhdi87Mcr36e1pVLrhBMPLjV9" + .parse() + .unwrap(), + ); + assert_eq!(cov_1.len(), 0); + let cov_2 = submap.get_wifi_coverage( + &"11PGVtgW9aM9ynfvns5USUsynYQ7EsMpxVqWuDKqFogKQX7etkR" + .parse() + .unwrap(), + ); + assert_eq!(cov_2.len(), 1); + assert_eq!(cov_2[0].rank, 2); + let cov_3 = submap.get_wifi_coverage( + &"11ibmJmQXTL6qMh4cq9pJ7tUtrpafWaVjjT6qhY7CNvjyvY9g1" + .parse() + .unwrap(), + ); + assert_eq!(cov_3.len(), 1); + assert_eq!(cov_3[0].rank, 1); + } + + #[test] + fn test_outdoor_cbrs_submap() { + let mut coverage_map_builder = CoverageMapBuilder::default(); + coverage_map_builder.insert_coverage_object(outdoor_cbrs_coverage( + "1", + 0x8a1fb46622dffff, + 3, + )); + coverage_map_builder.insert_coverage_object(outdoor_cbrs_coverage( + "2", + 0x8c2681a3064d9ff, + 1, + )); + let submap_builder = + coverage_map_builder.submap(vec![outdoor_cbrs_coverage("3", 0x8c2681a3064d9ff, 2)]); + let submap = submap_builder.build(&NoBoostedHexes, Utc::now()); + let cov_1 = submap.get_cbrs_coverage("1"); + assert_eq!(cov_1.len(), 0); + let cov_2 = submap.get_cbrs_coverage("2"); + assert_eq!(cov_2.len(), 1); + assert_eq!(cov_2[0].rank, 2); + let cov_3 = submap.get_cbrs_coverage("3"); + assert_eq!(cov_3.len(), 1); + assert_eq!(cov_3[0].rank, 1); + } + + #[test] + fn test_outdoor_wifi_submap() { + let mut coverage_map_builder = CoverageMapBuilder::default(); + coverage_map_builder.insert_coverage_object(outdoor_wifi_coverage( + "11xtYwQYnvkFYnJ9iZ8kmnetYKwhdi87Mcr36e1pVLrhBMPLjV9", + 0x8a1fb46622dffff, + 3, + )); + coverage_map_builder.insert_coverage_object(outdoor_wifi_coverage( + "11PGVtgW9aM9ynfvns5USUsynYQ7EsMpxVqWuDKqFogKQX7etkR", + 0x8c2681a3064d9ff, + 1, + )); + let submap_builder = coverage_map_builder.submap(vec![outdoor_wifi_coverage( + "11ibmJmQXTL6qMh4cq9pJ7tUtrpafWaVjjT6qhY7CNvjyvY9g1", + 0x8c2681a3064d9ff, + 2, + )]); + let submap = submap_builder.build(&NoBoostedHexes, Utc::now()); + let cov_1 = submap.get_wifi_coverage( + &"11xtYwQYnvkFYnJ9iZ8kmnetYKwhdi87Mcr36e1pVLrhBMPLjV9" + .parse() + .unwrap(), + ); + assert_eq!(cov_1.len(), 0); + let cov_2 = submap.get_wifi_coverage( + &"11PGVtgW9aM9ynfvns5USUsynYQ7EsMpxVqWuDKqFogKQX7etkR" + .parse() + .unwrap(), + ); + assert_eq!(cov_2.len(), 1); + assert_eq!(cov_2[0].rank, 2); + let cov_3 = submap.get_wifi_coverage( + &"11ibmJmQXTL6qMh4cq9pJ7tUtrpafWaVjjT6qhY7CNvjyvY9g1" + .parse() + .unwrap(), + ); + assert_eq!(cov_3.len(), 1); + assert_eq!(cov_3[0].rank, 1); + } + + fn hex_assignments_mock() -> HexAssignments { + HexAssignments { + footfall: Assignment::A, + urbanized: Assignment::A, + landtype: Assignment::A, + } + } + + fn indoor_cbrs_coverage(cbsd_id: &str, hex: u64, signal_level: SignalLevel) -> CoverageObject { + let owner: PublicKeyBinary = "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6" + .parse() + .expect("failed owner parse"); + CoverageObject { + indoor: true, + hotspot_key: owner, + seniority_timestamp: Utc::now(), + cbsd_id: Some(cbsd_id.to_string()), + coverage: vec![UnrankedCoverage { + location: Cell::from_raw(hex).expect("valid h3 cell"), + signal_power: 0, + signal_level, + assignments: hex_assignments_mock(), + }], + } + } + + fn indoor_wifi_coverage(owner: &str, hex: u64, signal_level: SignalLevel) -> CoverageObject { + let owner: PublicKeyBinary = owner.parse().expect("failed owner parse"); + CoverageObject { + indoor: true, + hotspot_key: owner, + seniority_timestamp: Utc::now(), + cbsd_id: None, + coverage: vec![UnrankedCoverage { + location: Cell::from_raw(hex).expect("valid h3 cell"), + signal_power: 0, + signal_level, + assignments: hex_assignments_mock(), + }], + } + } + + fn outdoor_cbrs_coverage(cbsd_id: &str, hex: u64, signal_power: i32) -> CoverageObject { + let owner: PublicKeyBinary = "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6" + .parse() + .expect("failed owner parse"); + CoverageObject { + indoor: false, + hotspot_key: owner, + seniority_timestamp: Utc::now(), + cbsd_id: Some(cbsd_id.to_string()), + coverage: vec![UnrankedCoverage { + location: Cell::from_raw(hex).expect("valid h3 cell"), + signal_power, + signal_level: SignalLevel::None, + assignments: hex_assignments_mock(), + }], + } + } + + fn outdoor_wifi_coverage(owner: &str, hex: u64, signal_power: i32) -> CoverageObject { + let owner: PublicKeyBinary = owner.parse().expect("failed owner parse"); + CoverageObject { + indoor: false, + hotspot_key: owner, + seniority_timestamp: Utc::now(), + cbsd_id: None, + coverage: vec![UnrankedCoverage { + location: Cell::from_raw(hex).expect("valid h3 cell"), + signal_power, + signal_level: SignalLevel::None, + assignments: hex_assignments_mock(), + }], + } + } +} From daa636eb7a6174bbd0a9b43b06e529b78daba5fb Mon Sep 17 00:00:00 2001 From: Matthew Plant Date: Tue, 4 Jun 2024 14:26:03 -0400 Subject: [PATCH 12/13] Fix bug with indoor iterator --- coverage_map/src/indoor.rs | 85 +++++++++++++++++++++++++++++--------- 1 file changed, 66 insertions(+), 19 deletions(-) diff --git a/coverage_map/src/indoor.rs b/coverage_map/src/indoor.rs index 9b52d20eb..46442d929 100644 --- a/coverage_map/src/indoor.rs +++ b/coverage_map/src/indoor.rs @@ -91,25 +91,22 @@ pub fn into_indoor_coverage_map( boosted_hexes: &impl BoostedHexMap, epoch_start: DateTime, ) -> impl Iterator + '_ { - indoor - .into_iter() - .flat_map(move |(hex, radios)| { - let boosted = boosted_hexes.get_current_multiplier(hex, epoch_start); - radios - .into_values() - .flat_map(move |radios| radios.into_sorted_vec().into_iter()) - .map(move |cov| (hex, boosted, cov)) - }) - .enumerate() - .map(move |(rank, (hex, boosted, cov))| RankedCoverage { - hex, - rank: rank + 1, - hotspot_key: cov.hotspot_key, - cbsd_id: cov.cbsd_id, - assignments: cov.assignments, - boosted, - signal_level: cov.signal_level, - }) + indoor.into_iter().flat_map(move |(hex, radios)| { + let boosted = boosted_hexes.get_current_multiplier(hex, epoch_start); + radios + .into_values() + .flat_map(move |radios| radios.into_sorted_vec().into_iter()) + .enumerate() + .map(move |(rank, cov)| RankedCoverage { + hex, + rank: rank + 1, + hotspot_key: cov.hotspot_key, + cbsd_id: cov.cbsd_id, + assignments: cov.assignments, + boosted, + signal_level: cov.signal_level, + }) + }) } #[cfg(test)] @@ -208,6 +205,34 @@ mod test { .and_utc() } + #[test] + fn single_radio() { + let mut indoor_coverage = IndoorCellTree::default(); + + insert_indoor_coverage_object( + &mut indoor_coverage, + indoor_cbrs_coverage_with_loc( + "1", + Cell::from_raw(0x8c2681a3064d9ff).unwrap(), + date(2022, 2, 2), + ), + ); + insert_indoor_coverage_object( + &mut indoor_coverage, + indoor_cbrs_coverage_with_loc( + "1", + Cell::from_raw(0x8c2681a3064dbff).unwrap(), + date(2022, 2, 2), + ), + ); + + let coverage = into_indoor_coverage_map(indoor_coverage, &NoBoostedHexes, Utc::now()) + .collect::>(); + // Both coverages should be ranked 1 + assert_eq!(coverage[0].rank, 1); + assert_eq!(coverage[1].rank, 1); + } + fn indoor_cbrs_coverage(cbsd_id: &str, signal_level: SignalLevel) -> CoverageObject { let owner: PublicKeyBinary = "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6" .parse() @@ -247,4 +272,26 @@ mod test { }], } } + + fn indoor_cbrs_coverage_with_loc( + cbsd_id: &str, + location: Cell, + seniority_timestamp: DateTime, + ) -> CoverageObject { + let owner: PublicKeyBinary = "112NqN2WWMwtK29PMzRby62fDydBJfsCLkCAf392stdok48ovNT6" + .parse() + .expect("failed owner parse"); + CoverageObject { + indoor: true, + hotspot_key: owner, + seniority_timestamp, + cbsd_id: Some(cbsd_id.to_string()), + coverage: vec![UnrankedCoverage { + location, + signal_power: 0, + signal_level: SignalLevel::High, + assignments: hex_assignments_mock(), + }], + } + } } From 49ed92603795fcf11e6d1c1c95cdfd010a64ae86 Mon Sep 17 00:00:00 2001 From: Matthew Plant Date: Wed, 5 Jun 2024 12:39:38 -0400 Subject: [PATCH 13/13] Remove TODO --- coverage_map/src/lib.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/coverage_map/src/lib.rs b/coverage_map/src/lib.rs index 536b227bc..3a565a1be 100644 --- a/coverage_map/src/lib.rs +++ b/coverage_map/src/lib.rs @@ -37,9 +37,6 @@ impl CoverageMapBuilder { /// A submap only contains the hexes that exist in the provided `coverage_objs` arguments. This /// allows for one to determine the potential ranking of new coverage objects without having /// to clone the entire CoverageMapBuilder. - // TODO(map): Should this return a `CoverageMap` instead? I don't really see the purpose of - // having this return a `CoverageMapBuilder` since it will probably always be converted instantly - // to a `CoverageMap`. pub fn submap(&self, coverage_objs: Vec) -> Self { // A different way to implement this function would be to insert all of the coverage_objs into // the submap, and then reconstruct the coverage objs from only the relevant hexes and then