diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index a3adea8..f748656 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,6 @@ **/*.rs.bk Cargo.lock tarpaulin-report.html +.direnv/ +.pre-commit-config.yaml + diff --git a/Cargo.toml b/Cargo.toml index 8ed01fe..88cae85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,13 +23,14 @@ serde_support = ["serde"] metrics = [] [dependencies] -byteorder = "1.3" +byteorder = "1.4" num = "0.4" rand = "0.8" -rayon = "1.3.1" +rayon = "1.5" md5 = { version = "0.7", optional = true } md4 = { version = "0.10", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } +thiserror = "1" [dev-dependencies] serde_json = { version = "1.0" } diff --git a/bacon.toml b/bacon.toml new file mode 100644 index 0000000..17a3e98 --- /dev/null +++ b/bacon.toml @@ -0,0 +1,66 @@ +# This is a configuration file for the bacon tool +# More info at https://github.com/Canop/bacon + +default_job = "check" + +[jobs] + +[jobs.check] +command = ["cargo", "check", "--color", "always"] +need_stdout = false + +[jobs.check-all] +command = ["cargo", "check", "--all-targets", "--color", "always"] +need_stdout = false +watch = ["tests", "benches", "examples"] + +[jobs.clippy] +command = ["cargo", "clippy", "--color", "always"] +need_stdout = false + +[jobs.clippy-all] +command = ["cargo", "clippy", "--all-targets", "--color", "always"] +need_stdout = false +watch = ["tests", "benches", "examples"] + +[jobs.clippy-all-features] +command = ["cargo", "clippy", "--all-features", "--color", "always"] +need_stdout = false +watch = ["tests", "benches", "examples"] + +[jobs.test] +command = ["cargo", "test", "--color", "always"] +need_stdout = true +watch = ["tests"] + +[jobs.doc] +command = ["cargo", "doc", "--color", "always", "--no-deps"] +need_stdout = false + +# if the doc compiles, then it opens in your browser and bacon switches +# to the previous job +[jobs.doc-open] +command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"] +need_stdout = false +on_success = "back" # so that we don't open the browser at each change + +# You can run your application and have the result displayed in bacon, +# *if* it makes sense for this crate. You can run an example the same +# way. Don't forget the `--color always` part or the errors won't be +# properly parsed. +[jobs.run] +command = ["cargo", "run", "--color", "always"] +need_stdout = true +allow_warnings = true + +# You may define here keybindings that would be specific to +# a project, for example a shortcut to launch a specific job. +# Shortcuts to internal functions (scrolling, toggling, etc.) +# should go in your personal prefs.toml file instead. +[keybindings] +a = "job:check-all" +i = "job:initial" +c = "job:clippy" +d = "job:doc-open" +t = "job:test" +r = "job:run" diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..13edae3 --- /dev/null +++ b/flake.lock @@ -0,0 +1,116 @@ +{ + "nodes": { + "flake-utils": { + "locked": { + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "locked": { + "lastModified": 1644229661, + "narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "3cecb5b042f7f209c56ffd8371b2711a290ec797", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1665643254, + "narHash": "sha256-IBVWNJxGCsshwh62eRfR6+ry3bSXmulB3VQRzLQo3hk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ba187fbdc5e35322c7dff556ef2c47bddfd6e8d7", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1645655918, + "narHash": "sha256-ZfbEFRW7o237+A1P7eTKhXje435FCAoe0blj2n20Was=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "77a7a4197740213879b9a1d2e1788c6c8ade4274", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "pre-commit-hooks": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1665584211, + "narHash": "sha256-Qc9zn43UjLpP823BP416hAsoaXugwWw+nKPVqsNhqdY=", + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "rev": "94b0f300dd9a23d4e851aa2a947a1511d3410e2d", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "pre-commit-hooks.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": "pre-commit-hooks", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "flake-utils": [ + "flake-utils" + ], + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1665802870, + "narHash": "sha256-02x6xx56WY6eDqamQUK7gIVSiKW14I25EMKozwtGf00=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "f55e3d741c6fe357d1e1bea50f5916863c831fdc", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..adfcbc1 --- /dev/null +++ b/flake.nix @@ -0,0 +1,83 @@ +{ + description = "Minimal flake environment"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + rust-overlay = { url = "github:oxalica/rust-overlay"; inputs.nixpkgs.follows = "nixpkgs"; inputs.flake-utils.follows = "flake-utils"; }; + pre-commit-hooks.url = "github:cachix/pre-commit-hooks.nix"; + }; + + outputs = { self, nixpkgs, flake-utils, rust-overlay, pre-commit-hooks }: + with flake-utils.lib; + + let overlays = [ rust-overlay.overlays.default (_self: super: { rustc = super.rust-bin.stable.latest.default; }) ]; in + eachDefaultSystem (system: + let pkgs = import nixpkgs { inherit system overlays; }; in + { + checks = { + pre-commit-check = pre-commit-hooks.lib.${system}.run { + src = ./.; + hooks = { + nixpkgs-fmt.enable = true; + nix-linter.enable = true; + clippy = + let + wrapper = pkgs.symlinkJoin { + name = "clippy-wrapped"; + paths = [ pkgs.rustc ]; + nativeBuildInputs = [ pkgs.makeWrapper ]; + postBuild = '' + wrapProgram $out/bin/cargo-clippy \ + --prefix PATH : ${lib.makeBinPath [ pkgs.rustc ]} + ''; + }; + in + { + name = "clippy"; + description = "Lint Rust code."; + entry = "${wrapper}/bin/cargo-clippy clippy"; + files = "\\.(rs|toml)$"; + pass_filenames = false; + }; + rustfmt = + let + wrapper = pkgs.symlinkJoin { + name = "rustfmt-wrapped"; + paths = [ pkgs.rustc ]; + nativeBuildInputs = [ pkgs.makeWrapper ]; + postBuild = '' + wrapProgram $out/bin/cargo-fmt \ + --prefix PATH : ${lib.makeBinPath [ pkgs.rustc ]} + ''; + }; + in + { + name = "rustfmt"; + description = "Format Rust code."; + entry = "${wrapper}/bin/cargo-fmt fmt -- --check --color always"; + files = "\\.(rs|toml)$"; + pass_filenames = false; + }; + }; + }; + }; + + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + bacon + cargo-audit + cargo-outdated + cargo-watch + crate2nix + openssl.dev + pkg-config + rustc + ]; + + shellHook = '' + ${self.checks.${system}.pre-commit-check.shellHook} + ''; + }; + }); +} diff --git a/src/data_structure.rs b/src/data_structure.rs index 7729869..f084aab 100644 --- a/src/data_structure.rs +++ b/src/data_structure.rs @@ -102,7 +102,6 @@ where .unwrap(); // Return cell index - #[allow(clippy::integer_arithmetic)] (digest_value as usize % self.filter.len()).as_() }) .collect::>() @@ -126,7 +125,6 @@ where } #[cfg(feature = "metrics")] - #[allow(clippy::integer_arithmetic)] { if *v == U::zero() { // Cell is not marked @@ -155,7 +153,6 @@ where } } - #[allow(unused_variables)] /// Constructor of the SBF data structure /// /// - `cells`: Number of cells in the filter, @@ -168,7 +165,7 @@ where hash_number: usize, max_input_size: usize, hash_function: HashFunction, - area_number: U, + #[cfg(feature = "metrics")] area_number: U, ) -> Result { assert!(cells > U::zero()); @@ -224,10 +221,10 @@ where /// Constructor of the SBF data structure using optimal parameters pub fn new_optimal( expected_inserts: usize, - area_number: U, max_fpp: f64, max_input_size: usize, hash_function: HashFunction, + #[cfg(feature = "metrics")] area_number: U, ) -> Result { let optimal_cells = (-(expected_inserts as f64) * max_fpp.ln() / (2.0f64.ln().powi(2))) as u64; @@ -238,6 +235,7 @@ where hash_number, max_input_size, hash_function, + #[cfg(feature = "metrics")] area_number, ) } diff --git a/src/error.rs b/src/error.rs index 1be4d5e..2e238d0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,21 +2,13 @@ #[cfg(feature = "serde_support")] use serde::{Deserialize, Serialize}; +use thiserror::Error; /// Custom error definitions -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Error)] #[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] pub enum Error { /// Access index is larger than the maximum size allowed + #[error("Index out of bounds")] IndexOutOfBounds, } - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", match &self { - Error::IndexOutOfBounds => "Index out of bounds", - }) - } -} - -impl std::error::Error for Error {} \ No newline at end of file diff --git a/src/metrics.rs b/src/metrics.rs index 9ebf2d6..9082ae1 100644 --- a/src/metrics.rs +++ b/src/metrics.rs @@ -9,57 +9,58 @@ use serde::{Deserialize, Serialize}; /// /// This data structure is automatically added to each `SBF` if the feature `metrics` is enabled. /// It's not necessary and is disabled by default. -#[cfg(feature = "metrics")] #[derive(Clone, Debug)] #[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] pub struct Metrics { /// Number of cells in the filter, the size of the filter - pub(crate) cells: usize, + pub cells: usize, /// Number of hash functions - pub(crate) hash_number: usize, + pub hash_number: usize, /// Number of inserted values - pub(crate) members: usize, + pub members: usize, /// Number of collisions occurred - pub(crate) collisions: usize, + pub collisions: usize, /// Safeness probability over the entire filter - pub(crate) safeness: f64, + pub safeness: f64, /// Number of areas - pub(crate) area_number: usize, + pub area_number: usize, /// Number of members per area - pub(crate) area_members: Vec, + pub area_members: Vec, /// Expected number of cells for each area - pub(crate) area_expected_cells: Vec, + pub area_expected_cells: Vec, /// Number of cells occupied by each area - pub(crate) area_cells: Vec, + pub area_cells: Vec, /// Number of collision of the same area value on the same cell - pub(crate) area_self_collisions: Vec, + pub area_self_collisions: Vec, /// Prior false positive probability for each area - pub(crate) area_prior_fpp: Vec, + pub area_prior_fpp: Vec, /// Posterior false positive probability for each area - pub(crate) area_fpp: Vec, + pub area_fpp: Vec, /// Prior inter-set error probability for each area - pub(crate) area_prior_isep: Vec, + pub area_prior_isep: Vec, /// Posterior inter-set error probability for each area - pub(crate) area_isep: Vec, + pub area_isep: Vec, /// Prior area-specific safeness probability - pub(crate) area_prior_safep: Vec, + pub area_prior_safep: Vec, } -#[cfg(feature = "metrics")] impl Metrics { /// Returns the number of inserted elements for the input area pub fn get_area_members(&self, index: usize) -> Option { self.area_members.get(index).cloned() } + /// Returns the sparsity of the entire SBF pub fn get_filter_sparsity(&self) -> f64 { - let sum: usize = self.area_cells + let sum: usize = self + .area_cells .par_iter() .skip(1) // Skip the index 0 .cloned() .sum(); 1.0 - (sum as f64 / self.hash_number as f64) } + /// Returns the posterior false positive probability over the entire filter /// (i.e. not area-specific) pub fn get_filter_fpp(&self) -> f64 { @@ -67,16 +68,15 @@ impl Metrics { let p = non_zero_cells as f64 / self.cells as f64; p.pow(self.hash_number as f64) } + /// Returns the expected emersion value for the input area pub fn get_expected_area_emersion(&self, area: usize) -> f64 { - let cells_with_greater_area_index: usize = self.area_members - .par_iter() - .skip(area) - .skip(1) - .sum(); + let cells_with_greater_area_index: usize = + self.area_members.par_iter().skip(area).skip(1).sum(); let p = 1.0f64 - 1.0f64 / self.cells as f64; p.pow(self.hash_number as f64 * cells_with_greater_area_index as f64) } + /// Returns the emersion value for the input area pub fn get_area_emersion(&self, area: usize) -> Option { if self.area_cells[area] == 0 || self.hash_number == 0 { @@ -85,124 +85,106 @@ impl Metrics { match ( self.area_cells.get(area), self.area_members.get(area), - self.area_self_collisions.get(area)) { - ( - Some(&area_cells), - Some(&area_members), - Some(&area_self_collisions) - ) => { + self.area_self_collisions.get(area), + ) { + (Some(&area_cells), Some(&area_members), Some(&area_self_collisions)) => { let a = area_cells as f64; - let b = area_members as f64 * self.hash_number as f64 - - area_self_collisions as f64; + let b = + area_members as f64 * self.hash_number as f64 - area_self_collisions as f64; Some(a / b) } - _ => None + _ => None, } } } + /// Returns the prior false positive probability over the entire filter pub fn get_filter_prior_fpp(&self) -> f64 { let p = 1.0 - 1.0 / self.cells as f64; let p = 1.0 - p.powf(self.hash_number as f64 * self.members as f64); p.powf(self.hash_number as f64) } + /// Computes posterior area-specific false positives probability (fpp) - #[allow(clippy::integer_arithmetic)] pub fn set_area_fpp(&mut self) { println!("AREA NUMBER: {}", self.area_number); - (1..self.area_number) - .rev() - .for_each(|i| { - let c: usize = (i..self.area_number) - .map(|j| self.area_cells[j]) - .sum(); - - let p = c as f64 / self.cells as f64; - let p = p.powi(self.hash_number as i32); - - self.area_fpp[i] = p; - - (i..self.area_number - 1) - .for_each(|j| { - self.area_fpp[i] -= self.area_fpp[j + 1]; - }); - self.area_fpp[i] = self.area_fpp[i].max(0.0); - }) + (1..self.area_number).rev().for_each(|i| { + let c: usize = (i..self.area_number).map(|j| self.area_cells[j]).sum(); + + let p = c as f64 / self.cells as f64; + let p = p.powi(self.hash_number as i32); + + self.area_fpp[i] = p; + + (i..self.area_number - 1).for_each(|j| { + self.area_fpp[i] -= self.area_fpp[j + 1]; + }); + self.area_fpp[i] = self.area_fpp[i].max(0.0); + }) } + /// Computes prior area-specific false positives probability (prior_fpp) - #[allow(clippy::integer_arithmetic)] pub fn set_prior_area_fpp(&mut self) { - (1..self.area_number) - .rev() - .for_each(|i| { - let c: usize = (i..self.area_number) - .map(|j| self.area_members[j]) - .sum(); - - let p = 1.0 - 1.0 / self.cells as f64; - let p = 1.0 - p.powi((self.hash_number * c) as i32); - let p = p.powi(self.hash_number as i32); - - self.area_fpp[i] = p; - - (i..self.area_number - 1) - .for_each(|j| { - self.area_prior_fpp[i] -= self.area_prior_fpp[j + 1]; - }); - self.area_prior_fpp[i] = self.area_prior_fpp[i].max(0.0); - }) + (1..self.area_number).rev().for_each(|i| { + let c: usize = (i..self.area_number).map(|j| self.area_members[j]).sum(); + + let p = 1.0 - 1.0 / self.cells as f64; + let p = 1.0 - p.powi((self.hash_number * c) as i32); + let p = p.powi(self.hash_number as i32); + + self.area_fpp[i] = p; + + (i..self.area_number - 1).for_each(|j| { + self.area_prior_fpp[i] -= self.area_prior_fpp[j + 1]; + }); + self.area_prior_fpp[i] = self.area_prior_fpp[i].max(0.0); + }) } + /// Computes posterior area-specific inter-set error probability (isep) pub fn set_area_isep(&mut self) { - (1..self.area_number) - .rev() - .for_each(|i| { - let p = 1.0 - self.get_area_emersion(i).unwrap_or(-1.0); - let p = p.powi(self.hash_number as i32); - - self.area_isep[i] = p; - }) + (1..self.area_number).rev().for_each(|i| { + let p = 1.0 - self.get_area_emersion(i).unwrap_or(-1.0); + let p = p.powi(self.hash_number as i32); + + self.area_isep[i] = p; + }) } + /// Computes prior area-specific inter-set error probability (prior_isep), /// computes prior area-specific safeness probability (prior_safep) and /// the overall safeness probability for the entire filter (safeness) pub fn set_prior_area_isep(&mut self) { let mut p3 = 1.0; - (1..self.area_number) - .rev() - .for_each(|i| { - let n_fill: usize = (i..self.area_number) - .skip(1) // first element - .map(|j| self.area_members[j]) - .sum(); + (1..self.area_number).rev().for_each(|i| { + let n_fill: usize = (i..self.area_number) + .skip(1) // first element + .map(|j| self.area_members[j]) + .sum(); - let p1 = 1.0 - 1.0 / self.cells as f64; - let p1 = 1.0 - p1.powi((self.hash_number * n_fill) as i32); - let p1 = p1.powi(self.area_members[i] as i32); + let p1 = 1.0 - 1.0 / self.cells as f64; + let p1 = 1.0 - p1.powi((self.hash_number * n_fill) as i32); + let p1 = p1.powi(self.area_members[i] as i32); - let p2 = (1.0 - p1).powi(self.area_members[i] as i32); + let p2 = (1.0 - p1).powi(self.area_members[i] as i32); - p3 *= p2; + p3 *= p2; - self.area_prior_isep[i] = p1; - self.area_prior_safep[i] = p2; - }); + self.area_prior_isep[i] = p1; + self.area_prior_safep[i] = p2; + }); self.safeness = p3; } + /// Computes the expected number of cells for each area (expected_cells) - #[allow(clippy::integer_arithmetic)] pub fn set_expected_area_cells(&mut self) { - (1..self.area_number) - .rev() - .for_each(|i| { - let n_fill: usize = (i..self.area_number) - .map(|j| self.area_members[j]) - .sum(); - - let p1 = 1.0 - 1.0 / self.cells as f64; - let p2 = p1.pow((self.hash_number * n_fill) as f64); - self.area_expected_cells[i] = (self.cells as f64 * p1 * p2) as i64; - }) + (1..self.area_number).rev().for_each(|i| { + let n_fill: usize = (i..self.area_number).map(|j| self.area_members[j]).sum(); + + let p1 = 1.0 - 1.0 / self.cells as f64; + let p2 = p1.pow((self.hash_number * n_fill) as f64); + self.area_expected_cells[i] = (self.cells as f64 * p1 * p2) as i64; + }) } -} \ No newline at end of file +} diff --git a/src/tests.rs b/src/tests.rs index 0cebfae..03bff6e 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -7,45 +7,62 @@ use crate::types::HashFunction; #[test] fn test_sbf() -> Result<(), Box> { - let mut sbf = SBF::new(10u8, 2, 5, - HashFunction::MD5, 3)?; - #[cfg(feature = "serde_support")] { + let mut sbf = SBF::new( + 10u8, + 2, + 5, + HashFunction::MD5, + #[cfg(feature = "metrics")] + 3, + )?; + #[cfg(feature = "serde_support")] + { println!("{}", serde_json::to_string(&sbf)?); } assert!(sbf.filter.par_iter().all(|v| *v == 0)); - sbf.insert(b"test".to_vec(), 1).expect("Correct insertion of an area"); - #[cfg(feature = "serde_support")] { + sbf.insert(b"test".to_vec(), 1) + .expect("Correct insertion of an area"); + #[cfg(feature = "serde_support")] + { println!("{}", serde_json::to_string(&sbf)?); } let count = sbf.filter.par_iter().cloned().filter(|v| *v == 1).count(); assert!(2 >= count && count > 0); let filter = sbf.filter.clone(); - sbf.insert(b"test".to_vec(), 1).expect("Correct insertion of an area"); - #[cfg(feature = "serde_support")] { + sbf.insert(b"test".to_vec(), 1) + .expect("Correct insertion of an area"); + #[cfg(feature = "serde_support")] + { println!("{}", serde_json::to_string(&sbf)?); } assert_eq!(filter, sbf.filter); - sbf.insert(b"test1".to_vec(), 2).expect("Correct insertion of an area"); - #[cfg(feature = "serde_support")] { + sbf.insert(b"test1".to_vec(), 2) + .expect("Correct insertion of an area"); + #[cfg(feature = "serde_support")] + { println!("{}", serde_json::to_string(&sbf)?); } let filter = sbf.filter.clone(); - sbf.insert(b"test1".to_vec(), 2).expect("Correct insertion of an area"); - #[cfg(feature = "serde_support")] { + sbf.insert(b"test1".to_vec(), 2) + .expect("Correct insertion of an area"); + #[cfg(feature = "serde_support")] + { println!("{}", serde_json::to_string(&sbf)?); } assert_eq!(filter, sbf.filter); - #[cfg(feature = "metrics")] { + #[cfg(feature = "metrics")] + { sbf.metrics.set_area_fpp(); sbf.metrics.set_prior_area_fpp(); sbf.metrics.set_area_isep(); sbf.metrics.set_prior_area_isep(); - #[cfg(feature = "serde_support")] { + #[cfg(feature = "serde_support")] + { println!("{}", serde_json::to_string(&sbf)?); } println!("AREA MEMBERS: {:?}", sbf.metrics.get_area_members(1)); @@ -53,8 +70,14 @@ fn test_sbf() -> Result<(), Box> { println!("FILTER SPARSITY: {}", sbf.metrics.get_filter_sparsity()); println!("FILTER FPP: {}", sbf.metrics.get_filter_fpp()); - println!("EXPECTED AREA EMERSION 1: {}", sbf.metrics.get_expected_area_emersion(1)); - println!("AREA EMERSION 1: {}", sbf.metrics.get_area_emersion(1).unwrap_or(-1.0)); + println!( + "EXPECTED AREA EMERSION 1: {}", + sbf.metrics.get_expected_area_emersion(1) + ); + println!( + "AREA EMERSION 1: {}", + sbf.metrics.get_area_emersion(1).unwrap_or(-1.0) + ); println!("FILTER PRIOR FPP: {}", sbf.metrics.get_filter_prior_fpp()); }