Skip to content

Commit

Permalink
Add coverage_map crate to provide CoverageMapBuilder and `Coverag…
Browse files Browse the repository at this point in the history
…eMap` data structures (#819)

* Initial commit for coverage-map

* Add TODO with questions

* Move around code, more TODO questions

* separate get_coverage into get_wifi_coverage and get_cbrs_coverage

* Add test for outdoor radios

* Add indoor tests

* Change rank to base-1

* Make mobile config depend on coverage map and not the other way around

* Fmt

* move things around

* Add tests for submap builder method

* Fix bug with indoor iterator

* Remove TODO
  • Loading branch information
Matthew Plant authored Jun 5, 2024
1 parent 6337f86 commit 6420804
Show file tree
Hide file tree
Showing 8 changed files with 943 additions and 0 deletions.
14 changes: 14 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ debug = true
[workspace]
members = [
"boost_manager",
"coverage_map",
"custom_tracing",
"db_store",
"denylist",
Expand Down
17 changes: 17 additions & 0 deletions coverage_map/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[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 }
uuid = { workspace = true }
297 changes: 297 additions & 0 deletions coverage_map/src/indoor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
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 crate::{BoostedHexMap, CoverageObject, RankedCoverage, SignalLevel, UnrankedCoverage};

pub type IndoorCellTree = HashMap<Cell, BTreeMap<SignalLevel, BinaryHeap<IndoorCoverageLevel>>>;

#[derive(Eq, Debug, Clone)]
pub struct IndoorCoverageLevel {
hotspot_key: PublicKeyBinary,
cbsd_id: Option<String>,
seniority_timestamp: DateTime<Utc>,
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<Ordering> {
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<String>,
seniority_timestamp: DateTime<Utc>,
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: &impl BoostedHexMap,
epoch_start: DateTime<Utc>,
) -> impl Iterator<Item = RankedCoverage> + '_ {
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)]
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, &NoBoostedHexes, Utc::now())
.map(|x| (x.cbsd_id.clone().unwrap(), x))
.collect();
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]
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, &NoBoostedHexes, Utc::now())
.map(|x| (x.cbsd_id.clone().unwrap(), x))
.collect();
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 {
HexAssignments {
footfall: Assignment::A,
urbanized: Assignment::A,
landtype: Assignment::A,
}
}

fn date(year: i32, month: u32, day: u32) -> DateTime<Utc> {
NaiveDate::from_ymd_opt(year, month, day)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap()
.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::<Vec<_>>();
// 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()
.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<Utc>,
) -> 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(),
}],
}
}

fn indoor_cbrs_coverage_with_loc(
cbsd_id: &str,
location: Cell,
seniority_timestamp: DateTime<Utc>,
) -> 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(),
}],
}
}
}
Loading

0 comments on commit 6420804

Please sign in to comment.