global: massive columnar rework part 1

This commit is contained in:
nym21
2026-08-10 22:44:28 +02:00
parent 26f07f01a3
commit 4abbc87ce5
171 changed files with 15733 additions and 6546 deletions
Generated
+37 -36
View File
@@ -184,9 +184,9 @@ dependencies = [
[[package]]
name = "async-trait"
version = "0.1.91"
version = "0.1.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec"
checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
dependencies = [
"proc-macro2",
"quote",
@@ -544,6 +544,7 @@ dependencies = [
"brk_traversable",
"brk_types",
"rayon",
"schemars",
"serde",
"vecdb",
]
@@ -963,9 +964,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.4.0"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9"
checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -1158,9 +1159,9 @@ dependencies = [
[[package]]
name = "cookie"
version = "0.18.1"
version = "0.18.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87"
dependencies = [
"percent-encoding",
"time",
@@ -1630,9 +1631,9 @@ dependencies = [
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de"
[[package]]
name = "flate2"
@@ -2321,9 +2322,9 @@ checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07"
[[package]]
name = "js-sys"
version = "0.3.103"
version = "0.3.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a"
dependencies = [
"cfg-if",
"futures-util",
@@ -2796,9 +2797,9 @@ dependencies = [
[[package]]
name = "portable-atomic"
version = "1.14.0"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3"
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
[[package]]
name = "portable-atomic-util"
@@ -3142,9 +3143,9 @@ dependencies = [
[[package]]
name = "rmcp"
version = "3.1.1"
version = "3.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "094c075f6698deef5a657cf4df6b684dff65157d255978b92b552ec22503f17a"
checksum = "c8dddc5b1924b9a59fba420166160ca2c4663a4e01803e52eda33070f56d63c8"
dependencies = [
"async-trait",
"base64 0.23.1",
@@ -3403,9 +3404,9 @@ dependencies = [
[[package]]
name = "serde_qs"
version = "1.1.2"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67d525c8ff68aa99e5818302259bdd02d86d0303710616f39c0f44846ff6d332"
checksum = "475b9310e272827d34113bf8679b6dfb7fa0023ee049764ba003f8f984e8b5d1"
dependencies = [
"axum",
"itoa",
@@ -3711,18 +3712,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.19"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.19"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
"proc-macro2",
"quote",
@@ -4051,11 +4052,11 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
version = "3.3.0"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d"
dependencies = [
"base64 0.22.1",
"base64 0.23.1",
"cookie_store",
"flate2",
"log",
@@ -4071,11 +4072,11 @@ dependencies = [
[[package]]
name = "ureq-proto"
version = "0.6.0"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613"
dependencies = [
"base64 0.22.1",
"base64 0.23.1",
"http",
"httparse",
"log",
@@ -4202,9 +4203,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen"
version = "0.2.126"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70"
dependencies = [
"cfg-if",
"once_cell",
@@ -4215,9 +4216,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.126"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -4225,9 +4226,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.126"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -4238,18 +4239,18 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.126"
version = "0.2.127"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
version = "0.3.103"
version = "0.3.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30"
dependencies = [
"js-sys",
"wasm-bindgen",
+1 -1
View File
@@ -90,7 +90,7 @@ tokio = { version = "1.53.1", features = ["rt-multi-thread"] }
tower-http = { version = "0.7.0", features = ["catch-panic", "compression-br", "compression-gzip", "compression-zstd", "cors", "normalize-path", "timeout", "trace"] }
tower-layer = "0.3"
tracing = { version = "0.1", default-features = false, features = ["std"] }
ureq = { version = "3.3.0", features = ["json"] }
ureq = { version = "3.4.0", features = ["json"] }
vecdb = { path = "crates/vecdb", version = "0.11.2", features = ["derive", "serde_json", "pco", "schemars"] }
vecdb_derive = { path = "crates/vecdb_derive", version = "0.11.2" }
+2240 -547
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -13,6 +13,7 @@ brk_types = { workspace = true }
brk_traversable = { workspace = true }
vecdb = { workspace = true }
rayon = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true }
[package.metadata.cargo-machete]
+224 -168
View File
@@ -6,7 +6,7 @@ use rayon::iter::{IntoParallelIterator, ParallelIterator};
use serde::Serialize;
use vecdb::{ColumnId, VecValue, Version};
use super::{CohortName, Filter, TimeFilter};
use super::{CohortContext, CohortName, Filter, TimeFilter};
// Age boundary constants in hours
pub const HOURS_1H: usize = 1;
@@ -164,6 +164,120 @@ impl ColumnId for AgeRangeId {
}
}
impl AgeRangeId {
#[inline]
pub fn bounds(self) -> &'static Range<usize> {
self.select(&AGE_RANGE_BOUNDS)
}
#[inline]
pub fn filter(self) -> &'static Filter {
self.select(&AGE_RANGE_FILTERS)
}
#[inline]
pub fn name(self) -> &'static CohortName {
self.select(&AGE_RANGE_NAMES)
}
pub fn select<T>(self, values: &AgeRange<T>) -> &T {
match self {
Self::Under1H => &values.under_1h,
Self::From1HTo1D => &values._1h_to_1d,
Self::From1DTo1W => &values._1d_to_1w,
Self::From1WTo1M => &values._1w_to_1m,
Self::From1MTo2M => &values._1m_to_2m,
Self::From2MTo3M => &values._2m_to_3m,
Self::From3MTo4M => &values._3m_to_4m,
Self::From4MTo5M => &values._4m_to_5m,
Self::From5MTo6M => &values._5m_to_6m,
Self::From6MTo9M => &values._6m_to_9m,
Self::From9MTo1Y => &values._9m_to_1y,
Self::From1YTo18M => &values._1y_to_18m,
Self::From18MTo2Y => &values._18m_to_2y,
Self::From2YTo3Y => &values._2y_to_3y,
Self::From3YTo4Y => &values._3y_to_4y,
Self::From4YTo5Y => &values._4y_to_5y,
Self::From5YTo6Y => &values._5y_to_6y,
Self::From6YTo7Y => &values._6y_to_7y,
Self::From7YTo8Y => &values._7y_to_8y,
Self::From8YTo10Y => &values._8y_to_10y,
Self::From10YTo12Y => &values._10y_to_12y,
Self::From12YTo15Y => &values._12y_to_15y,
Self::Over15Y => &values.over_15y,
}
}
pub fn select_mut<T>(self, values: &mut AgeRange<T>) -> &mut T {
match self {
Self::Under1H => &mut values.under_1h,
Self::From1HTo1D => &mut values._1h_to_1d,
Self::From1DTo1W => &mut values._1d_to_1w,
Self::From1WTo1M => &mut values._1w_to_1m,
Self::From1MTo2M => &mut values._1m_to_2m,
Self::From2MTo3M => &mut values._2m_to_3m,
Self::From3MTo4M => &mut values._3m_to_4m,
Self::From4MTo5M => &mut values._4m_to_5m,
Self::From5MTo6M => &mut values._5m_to_6m,
Self::From6MTo9M => &mut values._6m_to_9m,
Self::From9MTo1Y => &mut values._9m_to_1y,
Self::From1YTo18M => &mut values._1y_to_18m,
Self::From18MTo2Y => &mut values._18m_to_2y,
Self::From2YTo3Y => &mut values._2y_to_3y,
Self::From3YTo4Y => &mut values._3y_to_4y,
Self::From4YTo5Y => &mut values._4y_to_5y,
Self::From5YTo6Y => &mut values._5y_to_6y,
Self::From6YTo7Y => &mut values._6y_to_7y,
Self::From7YTo8Y => &mut values._7y_to_8y,
Self::From8YTo10Y => &mut values._8y_to_10y,
Self::From10YTo12Y => &mut values._10y_to_12y,
Self::From12YTo15Y => &mut values._12y_to_15y,
Self::Over15Y => &mut values.over_15y,
}
}
pub fn series<T>(
context: CohortContext,
mut create: impl FnMut(Self, &str) -> T,
) -> AgeRange<T> {
AgeRange::from_fn(|column| {
let name = context.prefixed(column.name().id);
create(column, &name)
})
}
}
impl From<Age> for AgeRangeId {
#[inline]
fn from(age: Age) -> Self {
match age.hours() {
0..HOURS_1H => Self::Under1H,
HOURS_1H..HOURS_1D => Self::From1HTo1D,
HOURS_1D..HOURS_1W => Self::From1DTo1W,
HOURS_1W..HOURS_1M => Self::From1WTo1M,
HOURS_1M..HOURS_2M => Self::From1MTo2M,
HOURS_2M..HOURS_3M => Self::From2MTo3M,
HOURS_3M..HOURS_4M => Self::From3MTo4M,
HOURS_4M..HOURS_5M => Self::From4MTo5M,
HOURS_5M..HOURS_6M => Self::From5MTo6M,
HOURS_6M..HOURS_9M => Self::From6MTo9M,
HOURS_9M..HOURS_1Y => Self::From9MTo1Y,
HOURS_1Y..HOURS_18M => Self::From1YTo18M,
HOURS_18M..HOURS_2Y => Self::From18MTo2Y,
HOURS_2Y..HOURS_3Y => Self::From2YTo3Y,
HOURS_3Y..HOURS_4Y => Self::From3YTo4Y,
HOURS_4Y..HOURS_5Y => Self::From4YTo5Y,
HOURS_5Y..HOURS_6Y => Self::From5YTo6Y,
HOURS_6Y..HOURS_7Y => Self::From6YTo7Y,
HOURS_7Y..HOURS_8Y => Self::From7YTo8Y,
HOURS_8Y..HOURS_10Y => Self::From8YTo10Y,
HOURS_10Y..HOURS_12Y => Self::From10YTo12Y,
HOURS_12Y..HOURS_15Y => Self::From12YTo15Y,
_ => Self::Over15Y,
}
}
}
/// Age boundaries in hours. Defines the cohort ranges:
/// [0, 1h), [1h, 1d), [1d, 1w), [1w, 1m), ..., [15y, ∞)
pub const AGE_BOUNDARIES: [usize; AGE_RANGE_COUNT - 1] = [
@@ -253,12 +367,6 @@ pub const AGE_RANGE_NAMES: AgeRange<CohortName> = AgeRange {
over_15y: CohortName::new("over_15y_old", "15y+", "15+ Years Old"),
};
impl AgeRange<CohortName> {
pub const fn names() -> &'static Self {
&AGE_RANGE_NAMES
}
}
#[derive(Default, Clone, Traversable, Serialize)]
pub struct AgeRange<T> {
pub under_1h: T,
@@ -287,183 +395,86 @@ pub struct AgeRange<T> {
}
impl<T> AgeRange<T> {
pub fn from_fn(mut create: impl FnMut(AgeRangeId) -> T) -> Self {
Self {
under_1h: create(AgeRangeId::Under1H),
_1h_to_1d: create(AgeRangeId::From1HTo1D),
_1d_to_1w: create(AgeRangeId::From1DTo1W),
_1w_to_1m: create(AgeRangeId::From1WTo1M),
_1m_to_2m: create(AgeRangeId::From1MTo2M),
_2m_to_3m: create(AgeRangeId::From2MTo3M),
_3m_to_4m: create(AgeRangeId::From3MTo4M),
_4m_to_5m: create(AgeRangeId::From4MTo5M),
_5m_to_6m: create(AgeRangeId::From5MTo6M),
_6m_to_9m: create(AgeRangeId::From6MTo9M),
_9m_to_1y: create(AgeRangeId::From9MTo1Y),
_1y_to_18m: create(AgeRangeId::From1YTo18M),
_18m_to_2y: create(AgeRangeId::From18MTo2Y),
_2y_to_3y: create(AgeRangeId::From2YTo3Y),
_3y_to_4y: create(AgeRangeId::From3YTo4Y),
_4y_to_5y: create(AgeRangeId::From4YTo5Y),
_5y_to_6y: create(AgeRangeId::From5YTo6Y),
_6y_to_7y: create(AgeRangeId::From6YTo7Y),
_7y_to_8y: create(AgeRangeId::From7YTo8Y),
_8y_to_10y: create(AgeRangeId::From8YTo10Y),
_10y_to_12y: create(AgeRangeId::From10YTo12Y),
_12y_to_15y: create(AgeRangeId::From12YTo15Y),
over_15y: create(AgeRangeId::Over15Y),
}
}
pub fn try_from_fn<E>(mut create: impl FnMut(AgeRangeId) -> Result<T, E>) -> Result<Self, E> {
Ok(Self {
under_1h: create(AgeRangeId::Under1H)?,
_1h_to_1d: create(AgeRangeId::From1HTo1D)?,
_1d_to_1w: create(AgeRangeId::From1DTo1W)?,
_1w_to_1m: create(AgeRangeId::From1WTo1M)?,
_1m_to_2m: create(AgeRangeId::From1MTo2M)?,
_2m_to_3m: create(AgeRangeId::From2MTo3M)?,
_3m_to_4m: create(AgeRangeId::From3MTo4M)?,
_4m_to_5m: create(AgeRangeId::From4MTo5M)?,
_5m_to_6m: create(AgeRangeId::From5MTo6M)?,
_6m_to_9m: create(AgeRangeId::From6MTo9M)?,
_9m_to_1y: create(AgeRangeId::From9MTo1Y)?,
_1y_to_18m: create(AgeRangeId::From1YTo18M)?,
_18m_to_2y: create(AgeRangeId::From18MTo2Y)?,
_2y_to_3y: create(AgeRangeId::From2YTo3Y)?,
_3y_to_4y: create(AgeRangeId::From3YTo4Y)?,
_4y_to_5y: create(AgeRangeId::From4YTo5Y)?,
_5y_to_6y: create(AgeRangeId::From5YTo6Y)?,
_6y_to_7y: create(AgeRangeId::From6YTo7Y)?,
_7y_to_8y: create(AgeRangeId::From7YTo8Y)?,
_8y_to_10y: create(AgeRangeId::From8YTo10Y)?,
_10y_to_12y: create(AgeRangeId::From10YTo12Y)?,
_12y_to_15y: create(AgeRangeId::From12YTo15Y)?,
over_15y: create(AgeRangeId::Over15Y)?,
})
}
/// Get mutable reference by Age. O(1).
#[inline]
pub fn get_mut(&mut self, age: Age) -> &mut T {
match age.hours() {
0..HOURS_1H => &mut self.under_1h,
HOURS_1H..HOURS_1D => &mut self._1h_to_1d,
HOURS_1D..HOURS_1W => &mut self._1d_to_1w,
HOURS_1W..HOURS_1M => &mut self._1w_to_1m,
HOURS_1M..HOURS_2M => &mut self._1m_to_2m,
HOURS_2M..HOURS_3M => &mut self._2m_to_3m,
HOURS_3M..HOURS_4M => &mut self._3m_to_4m,
HOURS_4M..HOURS_5M => &mut self._4m_to_5m,
HOURS_5M..HOURS_6M => &mut self._5m_to_6m,
HOURS_6M..HOURS_9M => &mut self._6m_to_9m,
HOURS_9M..HOURS_1Y => &mut self._9m_to_1y,
HOURS_1Y..HOURS_18M => &mut self._1y_to_18m,
HOURS_18M..HOURS_2Y => &mut self._18m_to_2y,
HOURS_2Y..HOURS_3Y => &mut self._2y_to_3y,
HOURS_3Y..HOURS_4Y => &mut self._3y_to_4y,
HOURS_4Y..HOURS_5Y => &mut self._4y_to_5y,
HOURS_5Y..HOURS_6Y => &mut self._5y_to_6y,
HOURS_6Y..HOURS_7Y => &mut self._6y_to_7y,
HOURS_7Y..HOURS_8Y => &mut self._7y_to_8y,
HOURS_8Y..HOURS_10Y => &mut self._8y_to_10y,
HOURS_10Y..HOURS_12Y => &mut self._10y_to_12y,
HOURS_12Y..HOURS_15Y => &mut self._12y_to_15y,
_ => &mut self.over_15y,
}
AgeRangeId::from(age).select_mut(self)
}
/// Get reference by Age. O(1).
#[inline]
pub fn get(&self, age: Age) -> &T {
match age.hours() {
0..HOURS_1H => &self.under_1h,
HOURS_1H..HOURS_1D => &self._1h_to_1d,
HOURS_1D..HOURS_1W => &self._1d_to_1w,
HOURS_1W..HOURS_1M => &self._1w_to_1m,
HOURS_1M..HOURS_2M => &self._1m_to_2m,
HOURS_2M..HOURS_3M => &self._2m_to_3m,
HOURS_3M..HOURS_4M => &self._3m_to_4m,
HOURS_4M..HOURS_5M => &self._4m_to_5m,
HOURS_5M..HOURS_6M => &self._5m_to_6m,
HOURS_6M..HOURS_9M => &self._6m_to_9m,
HOURS_9M..HOURS_1Y => &self._9m_to_1y,
HOURS_1Y..HOURS_18M => &self._1y_to_18m,
HOURS_18M..HOURS_2Y => &self._18m_to_2y,
HOURS_2Y..HOURS_3Y => &self._2y_to_3y,
HOURS_3Y..HOURS_4Y => &self._3y_to_4y,
HOURS_4Y..HOURS_5Y => &self._4y_to_5y,
HOURS_5Y..HOURS_6Y => &self._5y_to_6y,
HOURS_6Y..HOURS_7Y => &self._6y_to_7y,
HOURS_7Y..HOURS_8Y => &self._7y_to_8y,
HOURS_8Y..HOURS_10Y => &self._8y_to_10y,
HOURS_10Y..HOURS_12Y => &self._10y_to_12y,
HOURS_12Y..HOURS_15Y => &self._12y_to_15y,
_ => &self.over_15y,
}
}
pub fn from_array(arr: [T; AGE_RANGE_COUNT]) -> Self {
let [
a0,
a1,
a2,
a3,
a4,
a5,
a6,
a7,
a8,
a9,
a10,
a11,
a12,
a13,
a14,
a15,
a16,
a17,
a18,
a19,
a20,
a21,
a22,
] = arr;
Self {
under_1h: a0,
_1h_to_1d: a1,
_1d_to_1w: a2,
_1w_to_1m: a3,
_1m_to_2m: a4,
_2m_to_3m: a5,
_3m_to_4m: a6,
_4m_to_5m: a7,
_5m_to_6m: a8,
_6m_to_9m: a9,
_9m_to_1y: a10,
_1y_to_18m: a11,
_18m_to_2y: a12,
_2y_to_3y: a13,
_3y_to_4y: a14,
_4y_to_5y: a15,
_5y_to_6y: a16,
_6y_to_7y: a17,
_7y_to_8y: a18,
_8y_to_10y: a19,
_10y_to_12y: a20,
_12y_to_15y: a21,
over_15y: a22,
}
AgeRangeId::from(age).select(self)
}
pub fn new<F>(mut create: F) -> Self
where
F: FnMut(Filter, &'static str) -> T,
{
let f = AGE_RANGE_FILTERS;
let n = AGE_RANGE_NAMES;
Self {
under_1h: create(f.under_1h.clone(), n.under_1h.id),
_1h_to_1d: create(f._1h_to_1d.clone(), n._1h_to_1d.id),
_1d_to_1w: create(f._1d_to_1w.clone(), n._1d_to_1w.id),
_1w_to_1m: create(f._1w_to_1m.clone(), n._1w_to_1m.id),
_1m_to_2m: create(f._1m_to_2m.clone(), n._1m_to_2m.id),
_2m_to_3m: create(f._2m_to_3m.clone(), n._2m_to_3m.id),
_3m_to_4m: create(f._3m_to_4m.clone(), n._3m_to_4m.id),
_4m_to_5m: create(f._4m_to_5m.clone(), n._4m_to_5m.id),
_5m_to_6m: create(f._5m_to_6m.clone(), n._5m_to_6m.id),
_6m_to_9m: create(f._6m_to_9m.clone(), n._6m_to_9m.id),
_9m_to_1y: create(f._9m_to_1y.clone(), n._9m_to_1y.id),
_1y_to_18m: create(f._1y_to_18m.clone(), n._1y_to_18m.id),
_18m_to_2y: create(f._18m_to_2y.clone(), n._18m_to_2y.id),
_2y_to_3y: create(f._2y_to_3y.clone(), n._2y_to_3y.id),
_3y_to_4y: create(f._3y_to_4y.clone(), n._3y_to_4y.id),
_4y_to_5y: create(f._4y_to_5y.clone(), n._4y_to_5y.id),
_5y_to_6y: create(f._5y_to_6y.clone(), n._5y_to_6y.id),
_6y_to_7y: create(f._6y_to_7y.clone(), n._6y_to_7y.id),
_7y_to_8y: create(f._7y_to_8y.clone(), n._7y_to_8y.id),
_8y_to_10y: create(f._8y_to_10y.clone(), n._8y_to_10y.id),
_10y_to_12y: create(f._10y_to_12y.clone(), n._10y_to_12y.id),
_12y_to_15y: create(f._12y_to_15y.clone(), n._12y_to_15y.id),
over_15y: create(f.over_15y.clone(), n.over_15y.id),
}
Self::from_fn(|id| create(id.filter().clone(), id.name().id))
}
pub fn try_new<F, E>(mut create: F) -> Result<Self, E>
where
F: FnMut(Filter, &'static str) -> Result<T, E>,
{
let f = AGE_RANGE_FILTERS;
let n = AGE_RANGE_NAMES;
Ok(Self {
under_1h: create(f.under_1h.clone(), n.under_1h.id)?,
_1h_to_1d: create(f._1h_to_1d.clone(), n._1h_to_1d.id)?,
_1d_to_1w: create(f._1d_to_1w.clone(), n._1d_to_1w.id)?,
_1w_to_1m: create(f._1w_to_1m.clone(), n._1w_to_1m.id)?,
_1m_to_2m: create(f._1m_to_2m.clone(), n._1m_to_2m.id)?,
_2m_to_3m: create(f._2m_to_3m.clone(), n._2m_to_3m.id)?,
_3m_to_4m: create(f._3m_to_4m.clone(), n._3m_to_4m.id)?,
_4m_to_5m: create(f._4m_to_5m.clone(), n._4m_to_5m.id)?,
_5m_to_6m: create(f._5m_to_6m.clone(), n._5m_to_6m.id)?,
_6m_to_9m: create(f._6m_to_9m.clone(), n._6m_to_9m.id)?,
_9m_to_1y: create(f._9m_to_1y.clone(), n._9m_to_1y.id)?,
_1y_to_18m: create(f._1y_to_18m.clone(), n._1y_to_18m.id)?,
_18m_to_2y: create(f._18m_to_2y.clone(), n._18m_to_2y.id)?,
_2y_to_3y: create(f._2y_to_3y.clone(), n._2y_to_3y.id)?,
_3y_to_4y: create(f._3y_to_4y.clone(), n._3y_to_4y.id)?,
_4y_to_5y: create(f._4y_to_5y.clone(), n._4y_to_5y.id)?,
_5y_to_6y: create(f._5y_to_6y.clone(), n._5y_to_6y.id)?,
_6y_to_7y: create(f._6y_to_7y.clone(), n._6y_to_7y.id)?,
_7y_to_8y: create(f._7y_to_8y.clone(), n._7y_to_8y.id)?,
_8y_to_10y: create(f._8y_to_10y.clone(), n._8y_to_10y.id)?,
_10y_to_12y: create(f._10y_to_12y.clone(), n._10y_to_12y.id)?,
_12y_to_15y: create(f._12y_to_15y.clone(), n._12y_to_15y.id)?,
over_15y: create(f.over_15y.clone(), n.over_15y.id)?,
})
Self::try_from_fn(|id| create(id.filter().clone(), id.name().id))
}
pub fn iter(&self) -> impl Iterator<Item = &T> {
@@ -559,6 +570,8 @@ impl<T> AgeRange<T> {
#[cfg(test)]
mod tests {
use brk_types::Timestamp;
use super::*;
#[test]
@@ -599,6 +612,29 @@ mod tests {
}
}
#[test]
fn column_ids_select_named_fields_and_metadata() {
let mut named = AgeRange::from_fn(|column| column.index());
for &column in &AGE_RANGE_IDS {
assert_eq!(*column.select(&named), column.index());
*column.select_mut(&mut named) += AGE_RANGE_COUNT;
assert_eq!(*column.select(&named), column.index() + AGE_RANGE_COUNT);
assert_eq!(column.bounds(), column.select(&AGE_RANGE_BOUNDS));
assert_eq!(column.filter(), column.select(&AGE_RANGE_FILTERS));
assert_eq!(column.name().id, column.select(&AGE_RANGE_NAMES).id);
}
let series = AgeRangeId::series(CohortContext::Utxo, |column, name| {
(column, name.to_owned())
});
for (((column, name), expected_column), expected_name) in
series.iter().zip(AGE_RANGE_IDS).zip(AGE_RANGE_NAMES.iter())
{
assert_eq!(*column, expected_column);
assert_eq!(name, &CohortContext::Utxo.prefixed(expected_name.id));
}
}
#[test]
fn split_range_names_and_boundaries_match() {
assert_eq!(HOURS_9M, HOURS_6M + HOURS_3M);
@@ -609,15 +645,35 @@ mod tests {
assert_eq!(AGE_RANGE_NAMES._18m_to_2y.id, "18m_to_2y_old");
}
#[test]
fn classifier_matches_every_typed_range_boundary() {
for &id in &AGE_RANGE_IDS {
let bounds = id.bounds();
let at_start = Age::new(
Timestamp::new((bounds.start * 60 * 60) as u32),
Timestamp::ZERO,
);
assert_eq!(AgeRangeId::from(at_start), id);
if bounds.end != usize::MAX {
let before_end = Age::new(
Timestamp::new(((bounds.end - 1) * 60 * 60) as u32),
Timestamp::ZERO,
);
assert_eq!(AgeRangeId::from(before_end), id);
}
}
}
#[test]
fn eighteen_month_classifier_stays_anchored_to_one_year_plus_six_months() {
let age_at_540_days = Age::new(
brk_types::Timestamp::new((HOURS_6M * 3 * 60 * 60) as u32),
brk_types::Timestamp::ZERO,
Timestamp::new((HOURS_6M * 3 * 60 * 60) as u32),
Timestamp::ZERO,
);
let age_at_18m = Age::new(
brk_types::Timestamp::new((HOURS_18M * 60 * 60) as u32),
brk_types::Timestamp::ZERO,
Timestamp::new((HOURS_18M * 60 * 60) as u32),
Timestamp::ZERO,
);
assert_eq!(AGE_RANGE_NAMES.get(age_at_540_days).id, "1y_to_18m_old");
+161 -65
View File
@@ -4,6 +4,7 @@ use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::OutputType;
use rayon::prelude::*;
use vecdb::{ColumnId, VecValue, Version};
use super::Filter;
@@ -16,6 +17,116 @@ pub const P2WSH: &str = "p2wsh";
pub const P2TR: &str = "p2tr";
pub const P2A: &str = "p2a";
pub const ADDR_TYPE_COUNT: usize = 8;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum AddrTypeId {
P2PK65,
P2PK33,
P2PKH,
P2SH,
P2WPKH,
P2WSH,
P2TR,
P2A,
}
pub const ADDR_TYPE_IDS: [AddrTypeId; ADDR_TYPE_COUNT] = [
AddrTypeId::P2PK65,
AddrTypeId::P2PK33,
AddrTypeId::P2PKH,
AddrTypeId::P2SH,
AddrTypeId::P2WPKH,
AddrTypeId::P2WSH,
AddrTypeId::P2TR,
AddrTypeId::P2A,
];
impl ColumnId for AddrTypeId {
type Row<T>
= [T; ADDR_TYPE_COUNT]
where
T: VecValue;
const VERSION: Version = Version::ONE;
const ALL: &'static [Self] = &ADDR_TYPE_IDS;
#[inline]
fn index(self) -> usize {
self as usize
}
#[inline]
fn get<T: VecValue>(self, row: &Self::Row<T>) -> &T {
&row[self.index()]
}
#[inline]
fn get_mut<T: VecValue>(self, row: &mut Self::Row<T>) -> &mut T {
&mut row[self.index()]
}
#[inline]
fn from_fn<T, F>(mut create: F) -> Self::Row<T>
where
T: VecValue,
F: FnMut(Self) -> T,
{
std::array::from_fn(|index| create(ADDR_TYPE_IDS[index]))
}
#[inline]
fn map<T, U, F>(row: Self::Row<T>, create: F) -> Self::Row<U>
where
T: VecValue,
U: VecValue,
F: FnMut(T) -> U,
{
row.map(create)
}
}
impl AddrTypeId {
pub const fn name(self) -> &'static str {
match self {
Self::P2PK65 => P2PK65,
Self::P2PK33 => P2PK33,
Self::P2PKH => P2PKH,
Self::P2SH => P2SH,
Self::P2WPKH => P2WPKH,
Self::P2WSH => P2WSH,
Self::P2TR => P2TR,
Self::P2A => P2A,
}
}
pub const fn output_type(self) -> OutputType {
match self {
Self::P2PK65 => OutputType::P2PK65,
Self::P2PK33 => OutputType::P2PK33,
Self::P2PKH => OutputType::P2PKH,
Self::P2SH => OutputType::P2SH,
Self::P2WPKH => OutputType::P2WPKH,
Self::P2WSH => OutputType::P2WSH,
Self::P2TR => OutputType::P2TR,
Self::P2A => OutputType::P2A,
}
}
pub fn select<T>(self, values: &ByAddrType<T>) -> &T {
values.get_unwrap(self.output_type())
}
pub fn select_mut<T>(self, values: &mut ByAddrType<T>) -> &mut T {
values.get_mut_unwrap(self.output_type())
}
pub fn series<T>(mut create: impl FnMut(Self, &'static str) -> T) -> ByAddrType<T> {
ByAddrType::from_fn(|id| create(id, id.name()))
}
}
#[derive(Default, Clone, Debug, Traversable)]
pub struct ByAddrType<T> {
pub p2pk65: T,
@@ -29,36 +140,44 @@ pub struct ByAddrType<T> {
}
impl<T> ByAddrType<T> {
pub fn from_fn(mut create: impl FnMut(AddrTypeId) -> T) -> Self {
Self {
p2pk65: create(AddrTypeId::P2PK65),
p2pk33: create(AddrTypeId::P2PK33),
p2pkh: create(AddrTypeId::P2PKH),
p2sh: create(AddrTypeId::P2SH),
p2wpkh: create(AddrTypeId::P2WPKH),
p2wsh: create(AddrTypeId::P2WSH),
p2tr: create(AddrTypeId::P2TR),
p2a: create(AddrTypeId::P2A),
}
}
pub fn try_from_fn<E>(mut create: impl FnMut(AddrTypeId) -> Result<T, E>) -> Result<Self, E> {
Ok(Self {
p2pk65: create(AddrTypeId::P2PK65)?,
p2pk33: create(AddrTypeId::P2PK33)?,
p2pkh: create(AddrTypeId::P2PKH)?,
p2sh: create(AddrTypeId::P2SH)?,
p2wpkh: create(AddrTypeId::P2WPKH)?,
p2wsh: create(AddrTypeId::P2WSH)?,
p2tr: create(AddrTypeId::P2TR)?,
p2a: create(AddrTypeId::P2A)?,
})
}
pub fn new<F>(mut create: F) -> Self
where
F: FnMut(Filter) -> T,
{
Self {
p2pk65: create(Filter::Type(OutputType::P2PK65)),
p2pk33: create(Filter::Type(OutputType::P2PK33)),
p2pkh: create(Filter::Type(OutputType::P2PKH)),
p2sh: create(Filter::Type(OutputType::P2SH)),
p2wpkh: create(Filter::Type(OutputType::P2WPKH)),
p2wsh: create(Filter::Type(OutputType::P2WSH)),
p2tr: create(Filter::Type(OutputType::P2TR)),
p2a: create(Filter::Type(OutputType::P2A)),
}
Self::from_fn(|id| create(Filter::Type(id.output_type())))
}
pub fn new_with_name<F>(f: F) -> Result<Self>
where
F: Fn(&'static str) -> Result<T>,
{
Ok(Self {
p2pk65: f(P2PK65)?,
p2pk33: f(P2PK33)?,
p2pkh: f(P2PKH)?,
p2sh: f(P2SH)?,
p2wpkh: f(P2WPKH)?,
p2wsh: f(P2WSH)?,
p2tr: f(P2TR)?,
p2a: f(P2A)?,
})
Self::try_from_fn(|id| f(id.name()))
}
pub fn map_with_name<U>(&self, f: impl Fn(&'static str, &T) -> U) -> ByAddrType<U> {
@@ -78,16 +197,7 @@ impl<T> ByAddrType<T> {
where
F: Fn(usize) -> Result<T>,
{
Ok(Self {
p2pk65: f(0)?,
p2pk33: f(1)?,
p2pkh: f(2)?,
p2sh: f(3)?,
p2wpkh: f(4)?,
p2wsh: f(5)?,
p2tr: f(6)?,
p2a: f(7)?,
})
Self::try_from_fn(|id| f(id as usize))
}
#[inline]
@@ -286,40 +396,26 @@ impl<T> ByAddrType<Option<T>> {
}
}
/// Zip one ByAddrType with a function, producing a new ByAddrType.
pub fn zip_by_addr_type<S, R, F>(source: &ByAddrType<S>, f: F) -> Result<ByAddrType<R>>
where
F: Fn(&'static str, &S) -> Result<R>,
{
Ok(ByAddrType {
p2pk65: f(P2PK65, &source.p2pk65)?,
p2pk33: f(P2PK33, &source.p2pk33)?,
p2pkh: f(P2PKH, &source.p2pkh)?,
p2sh: f(P2SH, &source.p2sh)?,
p2wpkh: f(P2WPKH, &source.p2wpkh)?,
p2wsh: f(P2WSH, &source.p2wsh)?,
p2tr: f(P2TR, &source.p2tr)?,
p2a: f(P2A, &source.p2a)?,
})
}
#[cfg(test)]
mod tests {
use vecdb::ColumnId;
/// Zip two ByAddrTypes with a function, producing a new ByAddrType.
pub fn zip2_by_addr_type<S1, S2, R, F>(
a: &ByAddrType<S1>,
b: &ByAddrType<S2>,
f: F,
) -> Result<ByAddrType<R>>
where
F: Fn(&'static str, &S1, &S2) -> Result<R>,
{
Ok(ByAddrType {
p2pk65: f(P2PK65, &a.p2pk65, &b.p2pk65)?,
p2pk33: f(P2PK33, &a.p2pk33, &b.p2pk33)?,
p2pkh: f(P2PKH, &a.p2pkh, &b.p2pkh)?,
p2sh: f(P2SH, &a.p2sh, &b.p2sh)?,
p2wpkh: f(P2WPKH, &a.p2wpkh, &b.p2wpkh)?,
p2wsh: f(P2WSH, &a.p2wsh, &b.p2wsh)?,
p2tr: f(P2TR, &a.p2tr, &b.p2tr)?,
p2a: f(P2A, &a.p2a, &b.p2a)?,
})
use super::{ADDR_TYPE_IDS, AddrTypeId};
#[test]
fn column_order_matches_named_series() {
assert_eq!(AddrTypeId::ALL, ADDR_TYPE_IDS);
let series = AddrTypeId::series(|column, _| column);
assert!(series.values().copied().eq(ADDR_TYPE_IDS));
}
#[test]
fn row_order_matches_column_indexes() {
let row = AddrTypeId::from_fn(|column| column.index());
for column in ADDR_TYPE_IDS {
assert_eq!(*column.get(&row), column.index());
}
}
}
+141
View File
@@ -3,10 +3,124 @@ use std::ops::{Add, AddAssign};
use brk_traversable::Traversable;
use brk_types::OutputType;
use rayon::prelude::*;
use vecdb::{ColumnId, VecValue, Version};
use super::{Filter, SpendableType, UnspendableType};
pub const OP_RETURN: &str = "op_return";
pub const OUTPUT_TYPE_COUNT: usize = OutputType::COUNT;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum OutputTypeId {
P2PK65,
P2PK33,
P2PKH,
P2MS,
P2SH,
P2WPKH,
P2WSH,
P2TR,
P2A,
Unknown,
Empty,
OpReturn,
}
pub const OUTPUT_TYPE_IDS: [OutputTypeId; OUTPUT_TYPE_COUNT] = [
OutputTypeId::P2PK65,
OutputTypeId::P2PK33,
OutputTypeId::P2PKH,
OutputTypeId::P2MS,
OutputTypeId::P2SH,
OutputTypeId::P2WPKH,
OutputTypeId::P2WSH,
OutputTypeId::P2TR,
OutputTypeId::P2A,
OutputTypeId::Unknown,
OutputTypeId::Empty,
OutputTypeId::OpReturn,
];
impl OutputTypeId {
pub const fn from_output_type(value: OutputType) -> Self {
match value {
OutputType::P2PK65 => Self::P2PK65,
OutputType::P2PK33 => Self::P2PK33,
OutputType::P2PKH => Self::P2PKH,
OutputType::P2MS => Self::P2MS,
OutputType::P2SH => Self::P2SH,
OutputType::P2WPKH => Self::P2WPKH,
OutputType::P2WSH => Self::P2WSH,
OutputType::P2TR => Self::P2TR,
OutputType::P2A => Self::P2A,
OutputType::Unknown => Self::Unknown,
OutputType::Empty => Self::Empty,
OutputType::OpReturn => Self::OpReturn,
}
}
pub const fn output_type(self) -> OutputType {
match self {
Self::P2PK65 => OutputType::P2PK65,
Self::P2PK33 => OutputType::P2PK33,
Self::P2PKH => OutputType::P2PKH,
Self::P2MS => OutputType::P2MS,
Self::P2SH => OutputType::P2SH,
Self::P2WPKH => OutputType::P2WPKH,
Self::P2WSH => OutputType::P2WSH,
Self::P2TR => OutputType::P2TR,
Self::P2A => OutputType::P2A,
Self::Unknown => OutputType::Unknown,
Self::Empty => OutputType::Empty,
Self::OpReturn => OutputType::OpReturn,
}
}
}
impl ColumnId for OutputTypeId {
type Row<T>
= [T; OUTPUT_TYPE_COUNT]
where
T: VecValue;
const VERSION: Version = Version::ONE;
const ALL: &'static [Self] = &OUTPUT_TYPE_IDS;
#[inline]
fn index(self) -> usize {
self as usize
}
#[inline]
fn get<T: VecValue>(self, row: &Self::Row<T>) -> &T {
&row[self as usize]
}
#[inline]
fn get_mut<T: VecValue>(self, row: &mut Self::Row<T>) -> &mut T {
&mut row[self as usize]
}
#[inline]
fn from_fn<T, F>(mut f: F) -> Self::Row<T>
where
T: VecValue,
F: FnMut(Self) -> T,
{
std::array::from_fn(|index| f(OUTPUT_TYPE_IDS[index]))
}
#[inline]
fn map<T, U, F>(row: Self::Row<T>, f: F) -> Self::Row<U>
where
T: VecValue,
U: VecValue,
F: FnMut(T) -> U,
{
row.map(f)
}
}
#[derive(Default, Clone, Debug, Traversable)]
pub struct ByType<T> {
@@ -137,3 +251,30 @@ where
self.unspendable += rhs.unspendable;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn column_ids_match_by_type_order() {
let by_type = ByType::new(|filter, _| {
let Filter::Type(output_type) = filter else {
unreachable!()
};
output_type
});
let output_types: Vec<_> = by_type.iter().copied().collect();
let column_output_types: Vec<_> = OutputTypeId::ALL
.iter()
.map(|column| column.output_type())
.collect();
assert_eq!(column_output_types, output_types);
let row = OutputTypeId::from_fn(|column| column.index());
for column in OutputTypeId::ALL {
assert_eq!(*column.get(&row), column.index());
}
}
}
+2
View File
@@ -19,6 +19,7 @@ mod loss;
mod over_age;
mod over_amount;
mod profit;
mod profitability;
mod profitability_range;
mod spendable_type;
mod state_level;
@@ -49,6 +50,7 @@ pub use loss::*;
pub use over_age::*;
pub use over_amount::*;
pub use profit::*;
pub use profitability::*;
pub use profitability_range::*;
pub use spendable_type::*;
pub use state_level::*;
+479
View File
@@ -0,0 +1,479 @@
use std::{fmt, ops::AddAssign};
use schemars::JsonSchema;
use serde::Serialize;
use vecdb::{ColumnId, Formattable, VecValue, Version};
use crate::{
LOSS_COUNT, Loss, PROFIT_COUNT, PROFITABILITY_RANGE_COUNT, Profit, ProfitabilityRange,
};
pub const PROFITABILITY_COUNT: usize = PROFITABILITY_RANGE_COUNT + PROFIT_COUNT + LOSS_COUNT;
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct ProfitabilityRow<T> {
pub range: [T; PROFITABILITY_RANGE_COUNT],
pub profit: [T; PROFIT_COUNT],
pub loss: [T; LOSS_COUNT],
}
impl<T> ProfitabilityRow<T>
where
T: AddAssign + Copy + Default,
{
pub fn from_ranges(range: [T; PROFITABILITY_RANGE_COUNT]) -> Self {
let (profit_ranges, loss_ranges) = range.split_at(PROFIT_COUNT + 1);
let mut profit = [T::default(); PROFIT_COUNT];
let mut total = profit_ranges[0];
for (threshold, &value) in profit.iter_mut().rev().zip(&profit_ranges[1..]) {
total += value;
*threshold = total;
}
let mut loss = [T::default(); LOSS_COUNT];
let mut total = loss_ranges[loss_ranges.len() - 1];
for (threshold, &value) in loss
.iter_mut()
.rev()
.zip(loss_ranges[..loss_ranges.len() - 1].iter().rev())
{
total += value;
*threshold = total;
}
Self {
range,
profit,
loss,
}
}
}
impl<T: Formattable> Formattable for ProfitabilityRow<T> {
fn write_to(&self, buf: &mut Vec<u8>) {
buf.extend_from_slice(b"{\"range\":");
self.range.write_to(buf);
buf.extend_from_slice(b",\"profit\":");
self.profit.write_to(buf);
buf.extend_from_slice(b",\"loss\":");
self.loss.write_to(buf);
buf.push(b'}');
}
fn fmt_csv(&self, output: &mut String) -> fmt::Result {
let mut json = Vec::new();
self.write_to(&mut json);
let json = std::str::from_utf8(&json).map_err(|_| fmt::Error)?;
output.push('"');
for character in json.chars() {
if character == '"' {
output.push('"');
}
output.push(character);
}
output.push('"');
Ok(())
}
}
/// Every profitability range and aggregate threshold in column storage order.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum ProfitabilityId {
RangeOver1000PctInProfit,
Range500To1000PctInProfit,
Range300To500PctInProfit,
Range200To300PctInProfit,
Range100To200PctInProfit,
Range90To100PctInProfit,
Range80To90PctInProfit,
Range70To80PctInProfit,
Range60To70PctInProfit,
Range50To60PctInProfit,
Range40To50PctInProfit,
Range30To40PctInProfit,
Range20To30PctInProfit,
Range10To20PctInProfit,
Range0To10PctInProfit,
Range0To10PctInLoss,
Range10To20PctInLoss,
Range20To30PctInLoss,
Range30To40PctInLoss,
Range40To50PctInLoss,
Range50To60PctInLoss,
Range60To70PctInLoss,
Range70To80PctInLoss,
Range80To90PctInLoss,
Range90To100PctInLoss,
Profit,
ProfitOver10Pct,
ProfitOver20Pct,
ProfitOver30Pct,
ProfitOver40Pct,
ProfitOver50Pct,
ProfitOver60Pct,
ProfitOver70Pct,
ProfitOver80Pct,
ProfitOver90Pct,
ProfitOver100Pct,
ProfitOver200Pct,
ProfitOver300Pct,
ProfitOver500Pct,
Loss,
LossOver10Pct,
LossOver20Pct,
LossOver30Pct,
LossOver40Pct,
LossOver50Pct,
LossOver60Pct,
LossOver70Pct,
LossOver80Pct,
}
pub const PROFITABILITY_IDS: [ProfitabilityId; PROFITABILITY_COUNT] = [
ProfitabilityId::RangeOver1000PctInProfit,
ProfitabilityId::Range500To1000PctInProfit,
ProfitabilityId::Range300To500PctInProfit,
ProfitabilityId::Range200To300PctInProfit,
ProfitabilityId::Range100To200PctInProfit,
ProfitabilityId::Range90To100PctInProfit,
ProfitabilityId::Range80To90PctInProfit,
ProfitabilityId::Range70To80PctInProfit,
ProfitabilityId::Range60To70PctInProfit,
ProfitabilityId::Range50To60PctInProfit,
ProfitabilityId::Range40To50PctInProfit,
ProfitabilityId::Range30To40PctInProfit,
ProfitabilityId::Range20To30PctInProfit,
ProfitabilityId::Range10To20PctInProfit,
ProfitabilityId::Range0To10PctInProfit,
ProfitabilityId::Range0To10PctInLoss,
ProfitabilityId::Range10To20PctInLoss,
ProfitabilityId::Range20To30PctInLoss,
ProfitabilityId::Range30To40PctInLoss,
ProfitabilityId::Range40To50PctInLoss,
ProfitabilityId::Range50To60PctInLoss,
ProfitabilityId::Range60To70PctInLoss,
ProfitabilityId::Range70To80PctInLoss,
ProfitabilityId::Range80To90PctInLoss,
ProfitabilityId::Range90To100PctInLoss,
ProfitabilityId::Profit,
ProfitabilityId::ProfitOver10Pct,
ProfitabilityId::ProfitOver20Pct,
ProfitabilityId::ProfitOver30Pct,
ProfitabilityId::ProfitOver40Pct,
ProfitabilityId::ProfitOver50Pct,
ProfitabilityId::ProfitOver60Pct,
ProfitabilityId::ProfitOver70Pct,
ProfitabilityId::ProfitOver80Pct,
ProfitabilityId::ProfitOver90Pct,
ProfitabilityId::ProfitOver100Pct,
ProfitabilityId::ProfitOver200Pct,
ProfitabilityId::ProfitOver300Pct,
ProfitabilityId::ProfitOver500Pct,
ProfitabilityId::Loss,
ProfitabilityId::LossOver10Pct,
ProfitabilityId::LossOver20Pct,
ProfitabilityId::LossOver30Pct,
ProfitabilityId::LossOver40Pct,
ProfitabilityId::LossOver50Pct,
ProfitabilityId::LossOver60Pct,
ProfitabilityId::LossOver70Pct,
ProfitabilityId::LossOver80Pct,
];
impl ProfitabilityId {
pub fn range_ids() -> &'static [Self] {
&PROFITABILITY_IDS[..PROFITABILITY_RANGE_COUNT]
}
pub fn profit_ids() -> &'static [Self] {
&PROFITABILITY_IDS[PROFITABILITY_RANGE_COUNT..PROFITABILITY_RANGE_COUNT + PROFIT_COUNT]
}
pub fn loss_ids() -> &'static [Self] {
&PROFITABILITY_IDS[PROFITABILITY_RANGE_COUNT + PROFIT_COUNT..]
}
pub fn range_series<T>(
mut create: impl FnMut(Self, &'static str) -> T,
) -> ProfitabilityRange<T> {
let names = ProfitabilityRange::names();
ProfitabilityRange {
over_1000pct_in_profit: create(
Self::RangeOver1000PctInProfit,
names.over_1000pct_in_profit.id,
),
_500pct_to_1000pct_in_profit: create(
Self::Range500To1000PctInProfit,
names._500pct_to_1000pct_in_profit.id,
),
_300pct_to_500pct_in_profit: create(
Self::Range300To500PctInProfit,
names._300pct_to_500pct_in_profit.id,
),
_200pct_to_300pct_in_profit: create(
Self::Range200To300PctInProfit,
names._200pct_to_300pct_in_profit.id,
),
_100pct_to_200pct_in_profit: create(
Self::Range100To200PctInProfit,
names._100pct_to_200pct_in_profit.id,
),
_90pct_to_100pct_in_profit: create(
Self::Range90To100PctInProfit,
names._90pct_to_100pct_in_profit.id,
),
_80pct_to_90pct_in_profit: create(
Self::Range80To90PctInProfit,
names._80pct_to_90pct_in_profit.id,
),
_70pct_to_80pct_in_profit: create(
Self::Range70To80PctInProfit,
names._70pct_to_80pct_in_profit.id,
),
_60pct_to_70pct_in_profit: create(
Self::Range60To70PctInProfit,
names._60pct_to_70pct_in_profit.id,
),
_50pct_to_60pct_in_profit: create(
Self::Range50To60PctInProfit,
names._50pct_to_60pct_in_profit.id,
),
_40pct_to_50pct_in_profit: create(
Self::Range40To50PctInProfit,
names._40pct_to_50pct_in_profit.id,
),
_30pct_to_40pct_in_profit: create(
Self::Range30To40PctInProfit,
names._30pct_to_40pct_in_profit.id,
),
_20pct_to_30pct_in_profit: create(
Self::Range20To30PctInProfit,
names._20pct_to_30pct_in_profit.id,
),
_10pct_to_20pct_in_profit: create(
Self::Range10To20PctInProfit,
names._10pct_to_20pct_in_profit.id,
),
_0pct_to_10pct_in_profit: create(
Self::Range0To10PctInProfit,
names._0pct_to_10pct_in_profit.id,
),
_0pct_to_10pct_in_loss: create(
Self::Range0To10PctInLoss,
names._0pct_to_10pct_in_loss.id,
),
_10pct_to_20pct_in_loss: create(
Self::Range10To20PctInLoss,
names._10pct_to_20pct_in_loss.id,
),
_20pct_to_30pct_in_loss: create(
Self::Range20To30PctInLoss,
names._20pct_to_30pct_in_loss.id,
),
_30pct_to_40pct_in_loss: create(
Self::Range30To40PctInLoss,
names._30pct_to_40pct_in_loss.id,
),
_40pct_to_50pct_in_loss: create(
Self::Range40To50PctInLoss,
names._40pct_to_50pct_in_loss.id,
),
_50pct_to_60pct_in_loss: create(
Self::Range50To60PctInLoss,
names._50pct_to_60pct_in_loss.id,
),
_60pct_to_70pct_in_loss: create(
Self::Range60To70PctInLoss,
names._60pct_to_70pct_in_loss.id,
),
_70pct_to_80pct_in_loss: create(
Self::Range70To80PctInLoss,
names._70pct_to_80pct_in_loss.id,
),
_80pct_to_90pct_in_loss: create(
Self::Range80To90PctInLoss,
names._80pct_to_90pct_in_loss.id,
),
_90pct_to_100pct_in_loss: create(
Self::Range90To100PctInLoss,
names._90pct_to_100pct_in_loss.id,
),
}
}
pub fn profit_series<T>(mut create: impl FnMut(Self, &'static str) -> T) -> Profit<T> {
let names = Profit::names();
Profit {
all: create(Self::Profit, names.all.id),
_10pct: create(Self::ProfitOver10Pct, names._10pct.id),
_20pct: create(Self::ProfitOver20Pct, names._20pct.id),
_30pct: create(Self::ProfitOver30Pct, names._30pct.id),
_40pct: create(Self::ProfitOver40Pct, names._40pct.id),
_50pct: create(Self::ProfitOver50Pct, names._50pct.id),
_60pct: create(Self::ProfitOver60Pct, names._60pct.id),
_70pct: create(Self::ProfitOver70Pct, names._70pct.id),
_80pct: create(Self::ProfitOver80Pct, names._80pct.id),
_90pct: create(Self::ProfitOver90Pct, names._90pct.id),
_100pct: create(Self::ProfitOver100Pct, names._100pct.id),
_200pct: create(Self::ProfitOver200Pct, names._200pct.id),
_300pct: create(Self::ProfitOver300Pct, names._300pct.id),
_500pct: create(Self::ProfitOver500Pct, names._500pct.id),
}
}
pub fn loss_series<T>(mut create: impl FnMut(Self, &'static str) -> T) -> Loss<T> {
let names = Loss::names();
Loss {
all: create(Self::Loss, names.all.id),
_10pct: create(Self::LossOver10Pct, names._10pct.id),
_20pct: create(Self::LossOver20Pct, names._20pct.id),
_30pct: create(Self::LossOver30Pct, names._30pct.id),
_40pct: create(Self::LossOver40Pct, names._40pct.id),
_50pct: create(Self::LossOver50Pct, names._50pct.id),
_60pct: create(Self::LossOver60Pct, names._60pct.id),
_70pct: create(Self::LossOver70Pct, names._70pct.id),
_80pct: create(Self::LossOver80Pct, names._80pct.id),
}
}
#[inline]
pub const fn is_profit(self) -> bool {
let index = self as usize;
index < PROFIT_COUNT + 1
|| (index >= PROFITABILITY_RANGE_COUNT
&& index < PROFITABILITY_RANGE_COUNT + PROFIT_COUNT)
}
}
impl ColumnId for ProfitabilityId {
type Row<T>
= ProfitabilityRow<T>
where
T: VecValue;
const VERSION: Version = Version::ONE;
const ALL: &'static [Self] = &PROFITABILITY_IDS;
#[inline]
fn index(self) -> usize {
self as usize
}
#[inline]
fn get<T: VecValue>(self, row: &Self::Row<T>) -> &T {
let index = self as usize;
if index < PROFITABILITY_RANGE_COUNT {
&row.range[index]
} else if index < PROFITABILITY_RANGE_COUNT + PROFIT_COUNT {
&row.profit[index - PROFITABILITY_RANGE_COUNT]
} else {
&row.loss[index - PROFITABILITY_RANGE_COUNT - PROFIT_COUNT]
}
}
#[inline]
fn get_mut<T: VecValue>(self, row: &mut Self::Row<T>) -> &mut T {
let index = self as usize;
if index < PROFITABILITY_RANGE_COUNT {
&mut row.range[index]
} else if index < PROFITABILITY_RANGE_COUNT + PROFIT_COUNT {
&mut row.profit[index - PROFITABILITY_RANGE_COUNT]
} else {
&mut row.loss[index - PROFITABILITY_RANGE_COUNT - PROFIT_COUNT]
}
}
#[inline]
fn from_fn<T, F>(mut f: F) -> Self::Row<T>
where
T: VecValue,
F: FnMut(Self) -> T,
{
ProfitabilityRow {
range: std::array::from_fn(|index| f(PROFITABILITY_IDS[index])),
profit: std::array::from_fn(|index| {
f(PROFITABILITY_IDS[PROFITABILITY_RANGE_COUNT + index])
}),
loss: std::array::from_fn(|index| {
f(PROFITABILITY_IDS[PROFITABILITY_RANGE_COUNT + PROFIT_COUNT + index])
}),
}
}
#[inline]
fn map<T, U, F>(row: Self::Row<T>, mut f: F) -> Self::Row<U>
where
T: VecValue,
U: VecValue,
F: FnMut(T) -> U,
{
ProfitabilityRow {
range: row.range.map(&mut f),
profit: row.profit.map(&mut f),
loss: row.loss.map(f),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn profitability_ids_cover_the_schema_in_storage_order() {
assert_eq!(
ProfitabilityId::range_ids().len(),
PROFITABILITY_RANGE_COUNT
);
assert_eq!(ProfitabilityId::profit_ids().len(), PROFIT_COUNT);
assert_eq!(ProfitabilityId::loss_ids().len(), LOSS_COUNT);
for (index, id) in PROFITABILITY_IDS.into_iter().enumerate() {
assert_eq!(id.index(), index);
}
assert!(
ProfitabilityId::range_series(|id, _| id)
.iter()
.copied()
.eq(ProfitabilityId::range_ids().iter().copied())
);
assert!(
ProfitabilityId::profit_series(|id, _| id)
.iter()
.copied()
.eq(ProfitabilityId::profit_ids().iter().copied())
);
assert!(
ProfitabilityId::loss_series(|id, _| id)
.iter()
.copied()
.eq(ProfitabilityId::loss_ids().iter().copied())
);
}
#[test]
fn rows_expand_ranges_into_profit_prefixes_and_loss_suffixes() {
let ranges = std::array::from_fn(|index| index + 1);
let row = ProfitabilityRow::from_ranges(ranges);
assert_eq!(
row.profit[0],
ranges[..PROFIT_COUNT + 1].iter().copied().sum::<usize>()
);
assert_eq!(row.profit[PROFIT_COUNT - 1], ranges[0] + ranges[1]);
assert_eq!(
row.loss[0],
ranges[PROFIT_COUNT + 1..].iter().copied().sum::<usize>()
);
assert_eq!(
row.loss[LOSS_COUNT - 1],
ranges[PROFITABILITY_RANGE_COUNT - 2..]
.iter()
.copied()
.sum::<usize>()
);
}
}
+137
View File
@@ -4,9 +4,121 @@ use brk_traversable::Traversable;
use brk_types::OutputType;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use serde::Serialize;
use vecdb::{ColumnId, VecValue, Version};
use super::{CohortName, Filter};
pub const SPENDABLE_TYPE_COUNT: usize = 11;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum SpendableTypeId {
P2PK65,
P2PK33,
P2PKH,
P2MS,
P2SH,
P2WPKH,
P2WSH,
P2TR,
P2A,
Unknown,
Empty,
}
pub const SPENDABLE_TYPE_IDS: [SpendableTypeId; SPENDABLE_TYPE_COUNT] = [
SpendableTypeId::P2PK65,
SpendableTypeId::P2PK33,
SpendableTypeId::P2PKH,
SpendableTypeId::P2MS,
SpendableTypeId::P2SH,
SpendableTypeId::P2WPKH,
SpendableTypeId::P2WSH,
SpendableTypeId::P2TR,
SpendableTypeId::P2A,
SpendableTypeId::Unknown,
SpendableTypeId::Empty,
];
impl SpendableTypeId {
pub const fn from_output_type(value: OutputType) -> Option<Self> {
match value {
OutputType::P2PK65 => Some(Self::P2PK65),
OutputType::P2PK33 => Some(Self::P2PK33),
OutputType::P2PKH => Some(Self::P2PKH),
OutputType::P2MS => Some(Self::P2MS),
OutputType::P2SH => Some(Self::P2SH),
OutputType::P2WPKH => Some(Self::P2WPKH),
OutputType::P2WSH => Some(Self::P2WSH),
OutputType::P2TR => Some(Self::P2TR),
OutputType::P2A => Some(Self::P2A),
OutputType::Unknown => Some(Self::Unknown),
OutputType::Empty => Some(Self::Empty),
OutputType::OpReturn => None,
}
}
pub const fn output_type(self) -> OutputType {
match self {
Self::P2PK65 => OutputType::P2PK65,
Self::P2PK33 => OutputType::P2PK33,
Self::P2PKH => OutputType::P2PKH,
Self::P2MS => OutputType::P2MS,
Self::P2SH => OutputType::P2SH,
Self::P2WPKH => OutputType::P2WPKH,
Self::P2WSH => OutputType::P2WSH,
Self::P2TR => OutputType::P2TR,
Self::P2A => OutputType::P2A,
Self::Unknown => OutputType::Unknown,
Self::Empty => OutputType::Empty,
}
}
}
impl ColumnId for SpendableTypeId {
type Row<T>
= [T; SPENDABLE_TYPE_COUNT]
where
T: VecValue;
const VERSION: Version = Version::ONE;
const ALL: &'static [Self] = &SPENDABLE_TYPE_IDS;
#[inline]
fn index(self) -> usize {
self as usize
}
#[inline]
fn get<T: VecValue>(self, row: &Self::Row<T>) -> &T {
&row[self as usize]
}
#[inline]
fn get_mut<T: VecValue>(self, row: &mut Self::Row<T>) -> &mut T {
&mut row[self as usize]
}
#[inline]
fn from_fn<T, F>(mut f: F) -> Self::Row<T>
where
T: VecValue,
F: FnMut(Self) -> T,
{
std::array::from_fn(|index| f(SPENDABLE_TYPE_IDS[index]))
}
#[inline]
fn map<T, U, F>(row: Self::Row<T>, f: F) -> Self::Row<U>
where
T: VecValue,
U: VecValue,
F: FnMut(T) -> U,
{
row.map(f)
}
}
/// Spendable type values
pub const SPENDABLE_TYPE_VALUES: SpendableType<OutputType> = SpendableType {
p2pk65: OutputType::P2PK65,
@@ -279,3 +391,28 @@ where
self.empty += rhs.empty;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn column_ids_match_spendable_type_order() {
let output_types: Vec<_> = SPENDABLE_TYPE_VALUES.iter().copied().collect();
let column_output_types: Vec<_> = SpendableTypeId::ALL
.iter()
.map(|column| column.output_type())
.collect();
assert_eq!(column_output_types, output_types);
assert_eq!(
SpendableTypeId::from_output_type(OutputType::OpReturn),
None
);
let row = SpendableTypeId::from_fn(|column| column.index());
for column in SpendableTypeId::ALL {
assert_eq!(*column.get(&row), column.index());
}
}
}
@@ -1,29 +1,22 @@
//! Address activity tracking - per-block counts of address behaviors.
//!
//! Tracks global and per-address-type activity metrics:
//!
//! | Metric | Description |
//! |--------|-------------|
//! | `receiving` | Unique addresses that received this block |
//! | `sending` | Unique addresses that sent this block |
//! | `reactivated` | Addresses that were empty and now have funds |
//! | `bidirectional` | Addresses that both sent AND received in same block |
//! | `active` | Distinct addresses involved (sent received) |
//! Per-block address activity, split by address type.
use brk_cohort::ByAddrType;
use brk_cohort::{AddrTypeId, ByAddrType};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::Version;
use brk_types::{StoredU32, StoredU64, Version};
use derive_more::{Deref, DerefMut};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Database, Rw, StorageMode};
use vecdb::{AnyStoredVec, AnyVec, ColumnId, Database, ReadOnlyClone, Rw, StorageMode};
use crate::{
indexes,
internal::{CachedWindowStartVec, CountPerBlockRollingAverage, Windows},
internal::{
CachedWindowStartVec, ColumnarPerBlockCumulativeRolling, LazyPerBlockCumulativeAverage,
StoredU64ToStoredU32, Windows, WithAddrTypes,
},
};
/// Per-block activity counts - reset each block.
/// Per-block activity counts, reset after every block.
#[derive(Debug, Default, Clone)]
pub struct BlockActivityCounts {
pub reactivated: u32,
@@ -33,217 +26,69 @@ pub struct BlockActivityCounts {
}
impl BlockActivityCounts {
/// Reset all counts to zero.
#[inline]
pub(crate) fn reset(&mut self) {
*self = Self::default();
}
#[inline(always)]
fn active(&self) -> u32 {
debug_assert!(self.bidirectional <= self.sending.min(self.receiving));
self.sending + self.receiving - self.bidirectional
}
}
/// Per-address-type activity counts - aggregated during block processing.
/// Activity counts accumulated during block processing for each address type.
#[derive(Debug, Default, Deref, DerefMut)]
pub struct AddrTypeToActivityCounts(pub ByAddrType<BlockActivityCounts>);
impl AddrTypeToActivityCounts {
/// Reset all per-type counts.
pub(crate) fn reset(&mut self) {
self.0.values_mut().for_each(|v| v.reset());
self.0.values_mut().for_each(BlockActivityCounts::reset);
}
/// Sum all types to get totals.
pub(crate) fn totals(&self) -> BlockActivityCounts {
let mut total = BlockActivityCounts::default();
for counts in self.0.values() {
total.reactivated += counts.reactivated;
total.sending += counts.sending;
total.receiving += counts.receiving;
total.bidirectional += counts.bidirectional;
}
total
pub(crate) fn active(&self) -> u32 {
self.0.values().map(BlockActivityCounts::active).sum()
}
}
/// Activity count vectors for a single category (e.g., one address type or "all").
#[derive(Traversable)]
pub struct ActivityCountVecs<M: StorageMode = Rw> {
pub reactivated: CountPerBlockRollingAverage<M>,
pub sending: CountPerBlockRollingAverage<M>,
pub receiving: CountPerBlockRollingAverage<M>,
pub bidirectional: CountPerBlockRollingAverage<M>,
/// Distinct addresses involved in this block (sent received),
/// computed at push time as `sending + receiving - bidirectional`
/// via inclusion-exclusion. For per-type instances this is
/// per-type. For the `all` aggregate it's the cross-type total.
pub active: CountPerBlockRollingAverage<M>,
}
impl ActivityCountVecs {
/// `prefix` is prepended to each field's disk name. Use `""` for the
/// "all" aggregate and `"{type}_"` for per-address-type instances.
/// Field names are suffixed with `_addrs` so the final disk series
/// are e.g. `active_addrs`, `p2tr_bidirectional_addrs`.
pub(crate) fn forced_import(
db: &Database,
prefix: &str,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &Windows<&CachedWindowStartVec>,
) -> Result<Self> {
Ok(Self {
reactivated: CountPerBlockRollingAverage::forced_import(
db,
&format!("{prefix}reactivated_addrs"),
version,
indexes,
cached_starts,
)?,
sending: CountPerBlockRollingAverage::forced_import(
db,
&format!("{prefix}sending_addrs"),
version,
indexes,
cached_starts,
)?,
receiving: CountPerBlockRollingAverage::forced_import(
db,
&format!("{prefix}receiving_addrs"),
version,
indexes,
cached_starts,
)?,
bidirectional: CountPerBlockRollingAverage::forced_import(
db,
&format!("{prefix}bidirectional_addrs"),
version,
indexes,
cached_starts,
)?,
active: CountPerBlockRollingAverage::forced_import(
db,
&format!("{prefix}active_addrs"),
version,
indexes,
cached_starts,
)?,
#[inline(always)]
fn row(
&self,
value: impl Fn(&BlockActivityCounts) -> u32,
) -> <AddrTypeId as ColumnId>::Row<StoredU64> {
AddrTypeId::from_fn(|column| {
StoredU64::from(StoredU32::from(value(column.select(&self.0))))
})
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.reactivated
.min_stateful_len()
.min(self.sending.min_stateful_len())
.min(self.receiving.min_stateful_len())
.min(self.bidirectional.min_stateful_len())
.min(self.active.min_stateful_len())
}
pub(crate) fn par_iter_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
[
self.reactivated.stored_mut(),
self.sending.stored_mut(),
self.receiving.stored_mut(),
self.bidirectional.stored_mut(),
self.active.stored_mut(),
]
.into_par_iter()
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
self.reactivated.reset()?;
self.sending.reset()?;
self.receiving.reset()?;
self.bidirectional.reset()?;
self.active.reset()?;
Ok(())
}
#[inline(always)]
pub(crate) fn push_height(&mut self, counts: &BlockActivityCounts) {
self.reactivated.push_block(counts.reactivated.into());
self.sending.push_block(counts.sending.into());
self.receiving.push_block(counts.receiving.into());
self.bidirectional.push_block(counts.bidirectional.into());
let active = counts.sending + counts.receiving - counts.bidirectional;
self.active.push_block(active.into());
}
}
/// Per-address-type activity count vecs.
#[derive(Clone, Traversable)]
pub struct ActivityCountVecs {
pub reactivated: LazyPerBlockCumulativeAverage<StoredU32, StoredU64, StoredU64ToStoredU32>,
pub sending: LazyPerBlockCumulativeAverage<StoredU32, StoredU64, StoredU64ToStoredU32>,
pub receiving: LazyPerBlockCumulativeAverage<StoredU32, StoredU64, StoredU64ToStoredU32>,
pub bidirectional: LazyPerBlockCumulativeAverage<StoredU32, StoredU64, StoredU64ToStoredU32>,
pub active: LazyPerBlockCumulativeAverage<StoredU32, StoredU64, StoredU64ToStoredU32>,
}
/// Five metric-first cumulative matrices with cohort-first public views.
#[derive(Deref, DerefMut, Traversable)]
pub struct AddrTypeToActivityCountVecs<M: StorageMode = Rw>(ByAddrType<ActivityCountVecs<M>>);
impl From<ByAddrType<ActivityCountVecs>> for AddrTypeToActivityCountVecs {
#[inline]
fn from(value: ByAddrType<ActivityCountVecs>) -> Self {
Self(value)
}
}
impl AddrTypeToActivityCountVecs {
pub(crate) fn forced_import(
db: &Database,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &Windows<&CachedWindowStartVec>,
) -> Result<Self> {
Ok(Self::from(ByAddrType::<ActivityCountVecs>::new_with_name(
|type_name| {
ActivityCountVecs::forced_import(
db,
&format!("{type_name}_"),
version,
indexes,
cached_starts,
)
},
)?))
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.0
.values()
.map(|v| v.min_stateful_len())
.min()
.unwrap_or(0)
}
pub(crate) fn par_iter_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
let mut vecs: Vec<&mut dyn AnyStoredVec> = Vec::new();
for type_vecs in self.0.values_mut() {
vecs.push(type_vecs.reactivated.stored_mut());
vecs.push(type_vecs.sending.stored_mut());
vecs.push(type_vecs.receiving.stored_mut());
vecs.push(type_vecs.bidirectional.stored_mut());
vecs.push(type_vecs.active.stored_mut());
}
vecs.into_par_iter()
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
for v in self.0.values_mut() {
v.reset_height()?;
}
Ok(())
}
#[inline(always)]
pub(crate) fn push_height(&mut self, counts: &AddrTypeToActivityCounts) {
for (vecs, c) in self.0.values_mut().zip(counts.0.values()) {
vecs.push_height(c);
}
}
}
/// Storage for activity metrics (global + per type).
#[derive(Traversable)]
pub struct AddrActivityVecs<M: StorageMode = Rw> {
pub all: ActivityCountVecs<M>,
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub by_addr_type: AddrTypeToActivityCountVecs<M>,
pub series: WithAddrTypes<ActivityCountVecs>,
#[traversable(hidden)]
cumulative_reactivated: ColumnarPerBlockCumulativeRolling<StoredU64, AddrTypeId, (), M>,
#[traversable(hidden)]
cumulative_sending: ColumnarPerBlockCumulativeRolling<StoredU64, AddrTypeId, (), M>,
#[traversable(hidden)]
cumulative_receiving: ColumnarPerBlockCumulativeRolling<StoredU64, AddrTypeId, (), M>,
#[traversable(hidden)]
cumulative_bidirectional: ColumnarPerBlockCumulativeRolling<StoredU64, AddrTypeId, (), M>,
#[traversable(hidden)]
cumulative_active: ColumnarPerBlockCumulativeRolling<StoredU64, AddrTypeId, (), M>,
}
impl AddrActivityVecs {
@@ -253,41 +98,174 @@ impl AddrActivityVecs {
indexes: &indexes::Vecs,
cached_starts: &Windows<&CachedWindowStartVec>,
) -> Result<Self> {
let cumulative_version = version + Version::TWO;
let cumulative_reactivated = ColumnarPerBlockCumulativeRolling::forced_import(
db,
"reactivated_addrs_by_type_cumulative",
cumulative_version,
|_| (),
)?;
let cumulative_sending = ColumnarPerBlockCumulativeRolling::forced_import(
db,
"sending_addrs_by_type_cumulative",
cumulative_version,
|_| (),
)?;
let cumulative_receiving = ColumnarPerBlockCumulativeRolling::forced_import(
db,
"receiving_addrs_by_type_cumulative",
cumulative_version,
|_| (),
)?;
let cumulative_bidirectional = ColumnarPerBlockCumulativeRolling::forced_import(
db,
"bidirectional_addrs_by_type_cumulative",
cumulative_version,
|_| (),
)?;
let cumulative_active = ColumnarPerBlockCumulativeRolling::forced_import(
db,
"active_addrs_by_type_cumulative",
cumulative_version,
|_| (),
)?;
let reactivated = WithAddrTypes::from_columnar_cumulative_average_source(
"reactivated_addrs",
version,
&cumulative_reactivated.cumulative.read_only_clone(),
indexes,
cached_starts,
);
let sending = WithAddrTypes::from_columnar_cumulative_average_source(
"sending_addrs",
version,
&cumulative_sending.cumulative.read_only_clone(),
indexes,
cached_starts,
);
let receiving = WithAddrTypes::from_columnar_cumulative_average_source(
"receiving_addrs",
version,
&cumulative_receiving.cumulative.read_only_clone(),
indexes,
cached_starts,
);
let bidirectional = WithAddrTypes::from_columnar_cumulative_average_source(
"bidirectional_addrs",
version,
&cumulative_bidirectional.cumulative.read_only_clone(),
indexes,
cached_starts,
);
let active = WithAddrTypes::from_columnar_cumulative_average_source(
"active_addrs",
version,
&cumulative_active.cumulative.read_only_clone(),
indexes,
cached_starts,
);
let by_addr_type = AddrTypeId::series(|column, _| ActivityCountVecs {
reactivated: column.select(&reactivated.by_addr_type).clone(),
sending: column.select(&sending.by_addr_type).clone(),
receiving: column.select(&receiving.by_addr_type).clone(),
bidirectional: column.select(&bidirectional.by_addr_type).clone(),
active: column.select(&active.by_addr_type).clone(),
});
let series = WithAddrTypes {
all: ActivityCountVecs {
reactivated: reactivated.all,
sending: sending.all,
receiving: receiving.all,
bidirectional: bidirectional.all,
active: active.all,
},
by_addr_type,
};
Ok(Self {
all: ActivityCountVecs::forced_import(db, "", version, indexes, cached_starts)?,
by_addr_type: AddrTypeToActivityCountVecs::forced_import(
db,
version,
indexes,
cached_starts,
)?,
series,
cumulative_reactivated,
cumulative_sending,
cumulative_receiving,
cumulative_bidirectional,
cumulative_active,
})
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.all
.min_stateful_len()
.min(self.by_addr_type.min_stateful_len())
[
self.cumulative_reactivated.cumulative.len(),
self.cumulative_sending.cumulative.len(),
self.cumulative_receiving.cumulative.len(),
self.cumulative_bidirectional.cumulative.len(),
self.cumulative_active.cumulative.len(),
]
.into_iter()
.min()
.unwrap_or_default()
}
pub(crate) fn par_iter_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
self.all
.par_iter_height_mut()
.chain(self.by_addr_type.par_iter_height_mut())
[
self.cumulative_reactivated.stored_mut(),
self.cumulative_sending.stored_mut(),
self.cumulative_receiving.stored_mut(),
self.cumulative_bidirectional.stored_mut(),
self.cumulative_active.stored_mut(),
]
.into_par_iter()
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
self.all.reset_height()?;
self.by_addr_type.reset_height()?;
self.cumulative_reactivated.reset()?;
self.cumulative_sending.reset()?;
self.cumulative_receiving.reset()?;
self.cumulative_bidirectional.reset()?;
self.cumulative_active.reset()?;
Ok(())
}
#[inline(always)]
pub(crate) fn push_height(&mut self, counts: &AddrTypeToActivityCounts) {
let totals = counts.totals();
self.all.push_height(&totals);
self.by_addr_type.push_height(counts);
self.cumulative_reactivated
.push_block(counts.row(|counts| counts.reactivated));
self.cumulative_sending
.push_block(counts.row(|counts| counts.sending));
self.cumulative_receiving
.push_block(counts.row(|counts| counts.receiving));
self.cumulative_bidirectional
.push_block(counts.row(|counts| counts.bidirectional));
self.cumulative_active
.push_block(counts.row(BlockActivityCounts::active));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn active_count_sums_distinct_addresses_across_types() {
let counts = AddrTypeToActivityCounts(ByAddrType {
p2pkh: BlockActivityCounts {
sending: 5,
receiving: 4,
bidirectional: 2,
..BlockActivityCounts::default()
},
p2tr: BlockActivityCounts {
sending: 3,
receiving: 2,
bidirectional: 1,
..BlockActivityCounts::default()
},
..ByAddrType::default()
});
assert_eq!(counts.active(), 11);
}
}
@@ -1,12 +1,14 @@
use brk_cohort::AddrTypeId;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{StoredU64, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{Database, Rw, StorageMode};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, AnyVec, Database, Rw, StorageMode, WritableVec};
use crate::{
indexes,
internal::{PerBlock, WithAddrTypes},
internal::{ColumnarPerBlock, LazyColumnPerBlock, LazyPerBlock, WithAddrTypes},
};
use super::AddrTypeToAddrCount;
@@ -16,7 +18,13 @@ use super::AddrTypeToAddrCount;
/// funded/total pairs used by exposed, reused, and respent.
#[derive(Deref, DerefMut, Traversable)]
pub struct AddrCountsVecs<M: StorageMode = Rw>(
#[traversable(flatten)] pub WithAddrTypes<PerBlock<StoredU64, M>>,
#[traversable(flatten)]
pub ColumnarPerBlock<
StoredU64,
AddrTypeId,
WithAddrTypes<LazyColumnPerBlock<StoredU64, AddrTypeId>, LazyPerBlock<StoredU64>>,
M,
>,
);
impl AddrCountsVecs {
@@ -26,13 +34,31 @@ impl AddrCountsVecs {
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self(WithAddrTypes::<PerBlock<StoredU64>>::forced_import(
db, name, version, indexes,
Ok(Self(ColumnarPerBlock::forced_import(
db,
&format!("{name}_by_type"),
version,
|source| WithAddrTypes::from_columnar_source(name, version, source, indexes),
)?))
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.height.len()
}
pub(crate) fn par_iter_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
rayon::iter::once(&mut self.height as &mut dyn AnyStoredVec)
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
self.height.reset()?;
Ok(())
}
#[inline(always)]
pub(crate) fn push_counts(&mut self, counts: &AddrTypeToAddrCount) {
self.push_height(counts.sum(), counts.values().copied());
self.push(counts.row());
}
}
@@ -1,9 +1,8 @@
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::Version;
use rayon::prelude::*;
use vecdb::{AnyStoredVec, Database, Exit, Rw, StorageMode};
use vecdb::{AnyStoredVec, Database, Rw, StorageMode};
use crate::indexes;
@@ -70,10 +69,4 @@ impl AddrCountFundedTotalVecs {
self.funded.push_counts(funded);
self.total.push_counts(total);
}
pub(crate) fn compute_rest(&mut self, starting_lengths: &Lengths, exit: &Exit) -> Result<()> {
self.funded.compute_rest(starting_lengths, exit)?;
self.total.compute_rest(starting_lengths, exit)?;
Ok(())
}
}
@@ -1,7 +1,7 @@
use brk_cohort::ByAddrType;
use brk_types::Height;
use brk_cohort::{AddrTypeId, ByAddrType};
use brk_types::{Height, StoredU64};
use derive_more::{Deref, DerefMut};
use vecdb::ReadableVec;
use vecdb::{ColumnId, ReadableVec};
use super::AddrCountsVecs;
@@ -12,9 +12,8 @@ use super::AddrCountsVecs;
pub struct AddrTypeToAddrCount(ByAddrType<u64>);
impl AddrTypeToAddrCount {
#[inline]
pub(crate) fn sum(&self) -> u64 {
self.0.values().sum()
pub(crate) fn row(&self) -> <AddrTypeId as ColumnId>::Row<StoredU64> {
AddrTypeId::from_fn(|id| StoredU64::from(*id.select(&self.0)))
}
}
@@ -88,12 +88,21 @@ impl ExposedAddrVecs {
.min(self.supply.min_stateful_len())
}
pub(crate) fn par_iter_stateful_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
self.count
.par_iter_height_mut()
.chain(self.supply.par_iter_height_mut())
}
pub(crate) fn par_iter_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
self.count
.par_iter_height_mut()
.chain(self.supply.par_iter_height_mut())
.chain(rayon::iter::once(self.supply_share.stored_mut()))
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
@@ -115,7 +124,6 @@ impl ExposedAddrVecs {
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
exit: &Exit,
) -> Result<()> {
self.count.compute_rest(starting_lengths, exit)?;
self.supply_share.compute_rest(
starting_lengths.height,
&self.supply,
@@ -23,7 +23,7 @@ impl NewAddrCountVecs {
cached_starts: &Windows<&CachedWindowStartVec>,
) -> Self {
Self(WithAddrTypes {
all: LazyPerBlockCumulativeRolling::from_source(
all: LazyPerBlockCumulativeRolling::from_lazy_source(
"new_addr_count",
version,
&total.all,
@@ -31,7 +31,7 @@ impl NewAddrCountVecs {
indexes,
),
by_addr_type: total.by_addr_type.map_with_name(|name, total| {
LazyPerBlockCumulativeRolling::from_source(
LazyPerBlockCumulativeRolling::from_column_source(
&format!("{name}_new_addr_count"),
version,
total,
@@ -1,5 +1,7 @@
use brk_cohort::ByAddrType;
use brk_cohort::{AddrTypeId, ByAddrType};
use brk_types::StoredU64;
use derive_more::{Deref, DerefMut};
use vecdb::ColumnId;
/// Per-block running counter of address-reuse events, per address type. Shared
/// across reused (receive-based) and respent (spend-based) flavors, and
@@ -18,6 +20,11 @@ impl AddrTypeToAddrEventCount {
self.0.values().sum()
}
#[inline]
pub(crate) fn row(&self) -> <AddrTypeId as ColumnId>::Row<StoredU64> {
AddrTypeId::from_fn(|column| StoredU64::from(*column.select(&self.0)))
}
#[inline]
pub(crate) fn reset(&mut self) {
for v in self.0.values_mut() {
@@ -1,4 +1,4 @@
use brk_cohort::{ByAddrType, zip2_by_addr_type};
use brk_cohort::{AddrTypeId, ByAddrType};
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
@@ -9,9 +9,10 @@ use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec};
use crate::{
indexes, inputs,
internal::{
CachedBlockCountReader, CachedWindowStartVec, CountPerBlockRollingAverage,
LazyPercentCumulativeRolling, PerBlockCumulativeRolling, PerBlockRollingAverage, RatioU64,
Windows, WithAddrTypes,
CachedBlockCountReader, CachedWindowStartVec, ColumnarPerBlockCumulativeRolling,
CountPerBlockRollingAverage, LazyColumnPerBlockCumulativeRolling,
LazyPerBlockCumulativeRolling, LazyPercentCumulativeRolling, PerBlockRollingAverage,
RatioU64, Windows, WithAddrTypes,
},
outputs,
};
@@ -78,33 +79,45 @@ impl AddrEventShares {
indexes: &indexes::Vecs,
cached_starts: &Windows<&CachedWindowStartVec>,
all: LazyPercentCumulativeRolling<PartsPerMillion32>,
numerators: &ByAddrType<PerBlockCumulativeRolling<StoredU64>>,
numerators: &ByAddrType<LazyColumnPerBlockCumulativeRolling<StoredU64, AddrTypeId>>,
denominators: &ByAddrType<CachedBlockCountReader>,
) -> Result<Self> {
let by_addr_type = zip2_by_addr_type(
numerators,
denominators,
|type_name, numerator, denominator| {
Ok(LazyPercentCumulativeRolling::from_cached_block_count(
&format!("{type_name}_{name}"),
version,
&numerator.cumulative.height,
denominator.clone(),
cached_starts,
indexes,
))
},
)?;
Ok(Self { all, by_addr_type })
) -> Self {
let by_addr_type = AddrTypeId::series(|column, type_name| {
LazyPercentCumulativeRolling::from_cached_block_count(
&format!("{type_name}_{name}"),
version,
&column.select(numerators).cumulative.height,
column.select(denominators).clone(),
cached_starts,
indexes,
)
});
Self { all, by_addr_type }
}
}
#[derive(Traversable)]
pub struct AddrEventsVecs<M: StorageMode = Rw> {
pub output_to_reused_addr_count: WithAddrTypes<PerBlockCumulativeRolling<StoredU64, M>>,
pub output_to_reused_addr_count: ColumnarPerBlockCumulativeRolling<
StoredU64,
AddrTypeId,
WithAddrTypes<
LazyColumnPerBlockCumulativeRolling<StoredU64, AddrTypeId>,
LazyPerBlockCumulativeRolling<StoredU64>,
>,
M,
>,
pub output_to_reused_addr_share: AddrEventShares,
pub spendable_output_to_reused_addr_share: LazyPercentCumulativeRolling<PartsPerMillion32>,
pub input_from_reused_addr_count: WithAddrTypes<PerBlockCumulativeRolling<StoredU64, M>>,
pub input_from_reused_addr_count: ColumnarPerBlockCumulativeRolling<
StoredU64,
AddrTypeId,
WithAddrTypes<
LazyColumnPerBlockCumulativeRolling<StoredU64, AddrTypeId>,
LazyPerBlockCumulativeRolling<StoredU64>,
>,
M,
>,
pub input_from_reused_addr_share: AddrEventShares,
pub active_reused_addr_count: CountPerBlockRollingAverage<M>,
pub active_reused_addr_share: PerBlockRollingAverage<StoredF32, StoredF32, M>,
@@ -121,12 +134,19 @@ impl AddrEventsVecs {
inputs_by_type: &inputs::ByTypeVecs,
) -> Result<Self> {
let import_count = |name: &str| {
WithAddrTypes::<PerBlockCumulativeRolling<StoredU64>>::forced_import(
ColumnarPerBlockCumulativeRolling::forced_import(
db,
name,
&format!("{name}_by_type_cumulative"),
version,
indexes,
cached_starts,
|source| {
WithAddrTypes::from_columnar_cumulative_source(
name,
version,
source,
indexes,
cached_starts,
)
},
)
};
@@ -147,7 +167,7 @@ impl AddrEventsVecs {
),
&output_to_reused_addr_count.by_addr_type,
&output_denominators,
)?;
);
let spendable_share_name = format!("spendable_output_to_{name}_addr_share");
let spendable_output_to_reused_addr_share =
LazyPercentCumulativeRolling::from_cumulative_ratio::<
@@ -179,7 +199,7 @@ impl AddrEventsVecs {
),
&input_from_reused_addr_count.by_addr_type,
&input_denominators,
)?;
);
let active_reused_addr_count = CountPerBlockRollingAverage::forced_import(
db,
@@ -209,8 +229,9 @@ impl AddrEventsVecs {
pub(crate) fn min_stateful_len(&self) -> usize {
self.output_to_reused_addr_count
.min_stateful_len()
.min(self.input_from_reused_addr_count.min_stateful_len())
.cumulative
.len()
.min(self.input_from_reused_addr_count.cumulative.len())
.min(self.active_reused_addr_count.min_stateful_len())
.min(self.active_reused_addr_share.block.len())
}
@@ -218,9 +239,10 @@ impl AddrEventsVecs {
pub(crate) fn par_iter_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
self.output_to_reused_addr_count
.par_iter_height_mut()
.chain(self.input_from_reused_addr_count.par_iter_height_mut())
rayon::iter::once(self.output_to_reused_addr_count.stored_mut())
.chain(rayon::iter::once(
self.input_from_reused_addr_count.stored_mut(),
))
.chain([
self.active_reused_addr_count.stored_mut(),
&mut self.active_reused_addr_share.block as &mut dyn AnyStoredVec,
@@ -228,8 +250,8 @@ impl AddrEventsVecs {
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
self.output_to_reused_addr_count.reset_height()?;
self.input_from_reused_addr_count.reset_height()?;
self.output_to_reused_addr_count.reset()?;
self.input_from_reused_addr_count.reset()?;
self.active_reused_addr_count.reset()?;
self.active_reused_addr_share.block.reset()?;
Ok(())
@@ -243,10 +265,8 @@ impl AddrEventsVecs {
active_addr_count: u32,
active_reused_addr_count: u32,
) {
self.output_to_reused_addr_count
.push_height(uses.sum(), uses.values().copied());
self.input_from_reused_addr_count
.push_height(spends.sum(), spends.values().copied());
self.output_to_reused_addr_count.push_block(uses.row());
self.input_from_reused_addr_count.push_block(spends.row());
self.active_reused_addr_count
.push_block(StoredU32::from(active_reused_addr_count));
// Stored as a percentage in [0, 100] to match the rest of the
@@ -97,6 +97,15 @@ impl ReusedAddrVecs {
.min(self.supply.min_stateful_len())
}
pub(crate) fn par_iter_stateful_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
self.count
.par_iter_height_mut()
.chain(self.events.par_iter_height_mut())
.chain(self.supply.par_iter_height_mut())
}
pub(crate) fn par_iter_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
@@ -104,6 +113,7 @@ impl ReusedAddrVecs {
.par_iter_height_mut()
.chain(self.events.par_iter_height_mut())
.chain(self.supply.par_iter_height_mut())
.chain(rayon::iter::once(self.supply_share.stored_mut()))
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
@@ -136,7 +146,6 @@ impl ReusedAddrVecs {
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
exit: &Exit,
) -> Result<()> {
self.count.compute_rest(starting_lengths, exit)?;
self.events.compute_rest(starting_lengths, exit)?;
self.supply_share.compute_rest(
starting_lengths.height,
@@ -1,13 +1,16 @@
use brk_cohort::ByAddrType;
use brk_cohort::{AddrTypeId, ByAddrType};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, PartsPerMillion32, Sats, Version};
use vecdb::{Database, Exit, ReadableVec, Rw, StorageMode, WritableVec};
use vecdb::{
AnyStoredVec, BinaryTransform, Database, Exit, ReadOnlyClone, ReadableVec, Rw, StorageMode,
WritableVec,
};
use crate::{
distribution::metrics::AllSupplyCache,
indexes,
internal::{LazyPercentPerBlock, PercentPerBlock, RatioSats},
internal::{ColumnarPerBlock, LazyColumnPercentPerBlock, LazyPercentPerBlock, RatioSats},
};
use super::vecs::AddrSupplyVecs;
@@ -20,7 +23,9 @@ use super::vecs::AddrSupplyVecs;
pub struct AddrSupplyShareVecs<M: StorageMode = Rw> {
pub all: LazyPercentPerBlock<PartsPerMillion32>,
#[traversable(flatten)]
pub by_addr_type: ByAddrType<PercentPerBlock<PartsPerMillion32, M>>,
pub by_addr_type: ByAddrType<LazyColumnPercentPerBlock<PartsPerMillion32, AddrTypeId>>,
#[traversable(hidden)]
ppm: ColumnarPerBlock<PartsPerMillion32, AddrTypeId, (), M>,
}
impl AddrSupplyShareVecs {
@@ -40,20 +45,35 @@ impl AddrSupplyShareVecs {
all_supply.cached_boxed_clone(),
indexes,
);
let by_addr_type = ByAddrType::new_with_name(|type_name| {
PercentPerBlock::forced_import(db, &format!("{type_name}_{name}"), version, indexes)
})?;
let ppm =
ColumnarPerBlock::forced_import(db, &format!("{name}_ppm_by_type"), version, |_| ())?;
let source = ppm.height.read_only_clone();
let by_addr_type = AddrTypeId::series(|column, type_name| {
LazyColumnPercentPerBlock::new(
&format!("{type_name}_{name}"),
version,
&source,
column,
indexes,
)
});
Ok(Self { all, by_addr_type })
Ok(Self {
all,
by_addr_type,
ppm,
})
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
for share in self.by_addr_type.values_mut() {
share.ppm.height.reset()?;
}
self.ppm.height.reset()?;
Ok(())
}
pub(crate) fn stored_mut(&mut self) -> &mut dyn AnyStoredVec {
self.ppm.stored_mut()
}
pub(crate) fn compute_rest(
&mut self,
max_from: Height,
@@ -61,18 +81,12 @@ impl AddrSupplyShareVecs {
type_supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
exit: &Exit,
) -> Result<()> {
for ((_, share), ((_, cat), (_, denom))) in self
.by_addr_type
.iter_mut()
.zip(supply.by_addr_type.iter().zip(type_supply_sats.iter()))
{
share.compute_binary::<Sats, Sats, RatioSats<PartsPerMillion32>>(
max_from,
&cat.sats.height,
*denom,
exit,
)?;
}
Ok(())
self.ppm.compute_matrix_columns2(
max_from,
&supply.height,
|column| *column.select(type_supply_sats),
|_, category, total| RatioSats::<PartsPerMillion32>::apply(category, total),
exit,
)
}
}
@@ -1,7 +1,7 @@
use brk_cohort::ByAddrType;
use brk_cohort::{AddrTypeId, ByAddrType};
use brk_types::{Height, OutputType, Sats};
use derive_more::{Deref, DerefMut};
use vecdb::ReadableVec;
use vecdb::{ColumnId, ReadableVec};
use super::vecs::AddrSupplyVecs;
@@ -12,8 +12,8 @@ pub struct AddrTypeToSupply(ByAddrType<Sats>);
impl AddrTypeToSupply {
#[inline]
pub(crate) fn sum(&self) -> Sats {
self.0.values().copied().sum()
pub(crate) fn row(&self) -> <AddrTypeId as ColumnId>::Row<Sats> {
AddrTypeId::from_fn(|column| *column.select(&self.0))
}
/// Apply a signed `after - before` delta to the slot for `output_type`.
@@ -1,12 +1,16 @@
use brk_cohort::AddrTypeId;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Height, Version};
use brk_types::{Cents, Height, Sats, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{CachedBoxedVec, Database, Rw, StorageMode};
use rayon::prelude::*;
use vecdb::{AnyStoredVec, AnyVec, CachedBoxedVec, Database, Rw, StorageMode, WritableVec};
use crate::{
indexes,
internal::{SpotValuePerBlock, WithAddrTypes},
internal::{
ColumnarPerBlock, LazyColumnSpotValuePerBlock, LazySpotValuePerBlock, WithAddrTypes,
},
};
use super::AddrTypeToSupply;
@@ -17,7 +21,13 @@ use super::AddrTypeToSupply;
/// sats × spot price.
#[derive(Deref, DerefMut, Traversable)]
pub struct AddrSupplyVecs<M: StorageMode = Rw>(
#[traversable(flatten)] pub WithAddrTypes<SpotValuePerBlock<M>>,
#[traversable(flatten)]
pub ColumnarPerBlock<
Sats,
AddrTypeId,
WithAddrTypes<LazyColumnSpotValuePerBlock<AddrTypeId>, LazySpotValuePerBlock>,
M,
>,
);
impl AddrSupplyVecs {
@@ -28,17 +38,36 @@ impl AddrSupplyVecs {
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
Ok(Self(WithAddrTypes::<SpotValuePerBlock>::forced_import(
let name = format!("{name}_addr_supply");
Ok(Self(ColumnarPerBlock::forced_import(
db,
&format!("{name}_addr_supply"),
&format!("{name}_sats_by_type"),
version,
indexes,
spot_price,
|source| {
WithAddrTypes::from_columnar_spot_value_source(
&name, version, source, indexes, spot_price,
)
},
)?))
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.height.len()
}
pub(crate) fn par_iter_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
rayon::iter::once(self.stored_mut())
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
self.height.reset()?;
Ok(())
}
#[inline(always)]
pub(crate) fn push_supply(&mut self, supply: &AddrTypeToSupply) {
self.push_height(supply.sum(), supply.values().copied());
self.push(supply.row());
}
}
@@ -1,21 +1,17 @@
use brk_cohort::AddrTypeId;
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, StoredU64, Version};
use brk_types::{Height, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{Database, Exit, Rw, StorageMode};
use vecdb::{ColumnId, Database, Exit, ReadOnlyClone, Rw, StorageMode};
use crate::{
indexes,
internal::{PerBlock, WithAddrTypes},
};
use crate::indexes;
use super::AddrCountsVecs;
/// Total address count (global + per-type) with all derived indexes.
#[derive(Deref, DerefMut, Traversable)]
pub struct TotalAddrCountVecs<M: StorageMode = Rw>(
#[traversable(flatten)] pub WithAddrTypes<PerBlock<StoredU64, M>>,
);
pub struct TotalAddrCountVecs<M: StorageMode = Rw>(#[traversable(flatten)] pub AddrCountsVecs<M>);
impl TotalAddrCountVecs {
pub(crate) fn forced_import(
@@ -23,7 +19,7 @@ impl TotalAddrCountVecs {
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self(WithAddrTypes::<PerBlock<StoredU64>>::forced_import(
Ok(Self(AddrCountsVecs::forced_import(
db,
"total_addr_count",
version,
@@ -39,24 +35,21 @@ impl TotalAddrCountVecs {
empty_addr_count: &AddrCountsVecs,
exit: &Exit,
) -> Result<()> {
self.all.height.compute_add(
let addr_count = addr_count.height.read_only_clone();
let empty_addr_count = empty_addr_count.height.read_only_clone();
self.height.compute_transform2(
max_from,
&addr_count.all.height,
&empty_addr_count.all.height,
&addr_count,
&empty_addr_count,
|(height, addr_count, empty_addr_count, ..)| {
let total = AddrTypeId::from_fn(|column| {
*column.get(&addr_count) + *column.get(&empty_addr_count)
});
(height, total)
},
exit,
)?;
for ((_, total), ((_, addr), (_, empty))) in self.by_addr_type.iter_mut().zip(
addr_count
.by_addr_type
.iter()
.zip(empty_addr_count.by_addr_type.iter()),
) {
total
.height
.compute_add(max_from, &addr.height, &empty.height, exit)?;
}
Ok(())
}
}
@@ -1,7 +1,11 @@
use std::collections::BTreeMap;
use brk_cohort::{
AGE_RANGE_COUNT, Filter, PROFITABILITY_RANGE_COUNT, compute_profitability_boundaries,
AGE_RANGE_COUNT, AgeRangeId, Filter, PROFITABILITY_RANGE_COUNT,
compute_profitability_boundaries,
};
use brk_types::{Cents, CentsCompact, PartsPerMillion32, Sats};
use vecdb::ColumnId;
use crate::{
distribution::state::PendingDelta,
@@ -123,18 +127,14 @@ impl CostBasisFenwick {
}
/// Pre-compute `is_sth` lookup from the STH filter and age-range filters.
pub(super) fn compute_is_sth<'a>(
&mut self,
sth_filter: &Filter,
age_range_filters: impl Iterator<Item = &'a Filter>,
) {
for (i, f) in age_range_filters.enumerate() {
self.is_sth[i] = sth_filter.includes(f);
pub(super) fn compute_is_sth(&mut self, sth_filter: &Filter) {
for id in AgeRangeId::ALL {
self.is_sth[id.index()] = sth_filter.includes(id.filter());
}
}
pub(super) fn is_sth_at(&self, age_range_idx: usize) -> bool {
self.is_sth[age_range_idx]
pub(super) fn is_sth(&self, id: AgeRangeId) -> bool {
self.is_sth[id.index()]
}
/// Apply a net delta from a pending map entry.
@@ -158,7 +158,7 @@ impl CostBasisFenwick {
/// Bulk-initialize from age-range maps.
pub(super) fn bulk_init<'a>(
&mut self,
maps: impl Iterator<Item = (&'a std::collections::BTreeMap<CentsCompact, Sats>, bool)>,
maps: impl Iterator<Item = (&'a BTreeMap<CentsCompact, Sats>, bool)>,
) {
self.tree.reset();
self.totals = CostBasisNode::default();
@@ -1,8 +1,8 @@
use std::path::Path;
use brk_cohort::{
AGE_RANGE_COUNT, AgeRange, AmountRange, ByEntry, ByEpoch, Class, CohortContext, Filter,
Filtered, OverAge, OverAmount, SpendableType, Term, UnderAge, UnderAmount,
AGE_RANGE_COUNT, AgeRange, AgeRangeId, AmountRange, ByEntry, ByEpoch, Class, CohortContext,
Filter, OverAge, OverAmount, SpendableType, Term, UnderAge, UnderAmount,
};
use brk_error::Result;
use brk_indexer::Lengths;
@@ -10,7 +10,8 @@ use brk_traversable::Traversable;
use brk_types::{Cents, CentsSquaredSats, Height, Sats, Version};
use rayon::prelude::*;
use vecdb::{
AnyStoredVec, AnyVec, CachedBoxedVec, Database, Exit, ReadOnlyClone, Rw, StorageMode,
AnyStoredVec, AnyVec, BinaryTransform, CachedBoxedVec, ColumnId, ColumnarVec, Database,
EagerVec, Exit, ImportableVec, PcoVec, ReadOnlyClone, ReadableVec, Rw, StorageMode,
WritableVec,
};
@@ -18,15 +19,17 @@ use crate::{
distribution::{
AllChainCache, DynCohortVecs,
metrics::{
AllCohortMetrics, AllSupplyCache, BasicCohortMetrics, CohortMetricsBase,
CoreCohortMetrics, ExtendedAdjustedCohortMetrics, ExtendedCohortMetrics, ImportConfig,
MinimalCohortMetrics, ProfitabilityMetrics, RealizedFullAccum, SupplyCore,
TypeCohortMetrics,
AgeRangeSupplySources, AllCohortMetrics, AllSupplyCache, BasicCohortMetrics,
CohortMetricsBase, CoreCohortMetrics, ExtendedAdjustedCohortMetrics,
ExtendedCohortMetrics, ImportConfig, MinimalCohortMetrics, ProfitabilityMetrics,
RealizedFullAccum, TypeCohortMetrics,
},
state::UTXOCohortState,
},
indexes,
internal::{CachedWindowStartVec, ValuePerBlockCumulativeRolling, Windows},
internal::{
CachedWindowStartVec, LazyColumnValuePerBlockCumulativeRolling, SatsToCents, Windows,
},
price,
};
@@ -52,7 +55,12 @@ pub struct UTXOCohorts<M: StorageMode = Rw> {
#[traversable(rename = "type")]
pub type_: SpendableType<UTXOCohortVecs<TypeCohortMetrics<M>>>,
pub profitability: ProfitabilityMetrics<M>,
pub matured: AgeRange<ValuePerBlockCumulativeRolling<M>>,
#[traversable(skip)]
age_range_supply: AgeRangeSupplySources<M>,
pub matured: AgeRange<LazyColumnValuePerBlockCumulativeRolling<AgeRangeId>>,
pub cumulative_matured_sats: M::Stored<EagerVec<ColumnarVec<PcoVec<Height, Sats>, AgeRangeId>>>,
pub cumulative_matured_cents:
M::Stored<EagerVec<ColumnarVec<PcoVec<Height, Cents>, AgeRangeId>>>,
#[traversable(skip)]
all_supply_cache: AllSupplyCache,
#[traversable(skip)]
@@ -69,6 +77,10 @@ pub(crate) struct UTXOCohortsTransientState {
pub(super) tick_tock_cached_positions: [usize; AGE_RANGE_COUNT - 1],
}
const MATURED_VERSION: Version = Version::new(4);
const AGE_RANGE_SUPPLY_VERSION: Version = Version::ONE;
const WRITE_INTERVAL: usize = 10_000;
impl UTXOCohorts<Rw> {
/// Separate cohorts currently total 74:
/// 23 age + 5 epoch + 18 class + 2 entry + 15 amount + 11 spendable type.
@@ -85,8 +97,9 @@ impl UTXOCohorts<Rw> {
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let v = version + VERSION;
let prefix = CohortContext::Utxo.prefix();
// Phase 1: Import "all" supply first so it can be referenced by all cohorts' relative metrics.
// Phase 1: Import the age-range supply source and its cached all-column view.
let all_full_name = CohortContext::Utxo.full_name(&Filter::All, "");
let all_cfg = ImportConfig {
db,
@@ -97,33 +110,31 @@ impl UTXOCohorts<Rw> {
cached_starts,
spot_price,
};
let all_supply = SupplyCore::forced_import_all(&all_cfg)?;
let all_supply_cache = AllSupplyCache::new(&all_supply.total.sats.height);
let age_range_supply =
AgeRangeSupplySources::forced_import(db, prefix, v + AGE_RANGE_SUPPLY_VERSION)?;
let (all_supply, all_supply_cache) = age_range_supply.all(&all_cfg);
let all_chain_cache = AllChainCache::new(&all_supply_cache, spot_price);
// Phase 2: Import separate (stateful) cohorts.
// Helper for separate cohorts with BasicCohortMetrics + full state
let basic_separate =
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<BasicCohortMetrics>> {
let full_name = CohortContext::Utxo.full_name(&f, name);
let cfg = ImportConfig {
db,
filter: &f,
full_name: &full_name,
version: v,
indexes,
cached_starts,
spot_price,
};
let state = Some(Box::new(UTXOCohortState::new(states_path, &full_name)));
Ok(UTXOCohortVecs::new(
state,
BasicCohortMetrics::forced_import(&cfg, &all_supply_cache)?,
))
let age_range = AgeRange::try_from_fn(|id| {
let filter = id.filter().clone();
let full_name = CohortContext::Utxo.full_name(&filter, id.name().id);
let cfg = ImportConfig {
db,
filter: &filter,
full_name: &full_name,
version: v,
indexes,
cached_starts,
spot_price,
};
let age_range = AgeRange::try_new(&basic_separate)?;
let state = Some(Box::new(UTXOCohortState::new(states_path, &full_name)));
let supply = age_range_supply.column(&cfg, id, &all_supply_cache);
Ok::<_, brk_error::Error>(UTXOCohortVecs::new(
state,
BasicCohortMetrics::forced_import(&cfg, supply)?,
))
})?;
let core_separate =
|f: Filter, name: &'static str| -> Result<UTXOCohortVecs<CoreCohortMetrics>> {
@@ -221,7 +232,7 @@ impl UTXOCohorts<Rw> {
None,
ExtendedAdjustedCohortMetrics::forced_import(
&cfg,
&all_supply_cache,
age_range_supply.filtered(&cfg, &f, &all_supply_cache),
&all_chain_cache,
)?,
)
@@ -242,7 +253,11 @@ impl UTXOCohorts<Rw> {
};
UTXOCohortVecs::new(
None,
ExtendedCohortMetrics::forced_import(&cfg, &all_supply_cache, &all_chain_cache)?,
ExtendedCohortMetrics::forced_import(
&cfg,
age_range_supply.filtered(&cfg, &f, &all_supply_cache),
&all_chain_cache,
)?,
)
};
@@ -259,9 +274,10 @@ impl UTXOCohorts<Rw> {
cached_starts,
spot_price,
};
let supply = age_range_supply.filtered(&cfg, &f, &all_supply_cache);
Ok(UTXOCohortVecs::new(
None,
CoreCohortMetrics::forced_import(&cfg, &all_supply_cache)?,
CoreCohortMetrics::forced_import_with_supply(&cfg, supply)?,
))
};
@@ -292,18 +308,32 @@ impl UTXOCohorts<Rw> {
let under_amount = UnderAmount::try_new(&minimal_no_state)?;
let over_amount = OverAmount::try_new(&minimal_no_state)?;
let prefix = CohortContext::Utxo.prefix();
let matured = AgeRange::try_new(&|_f: Filter,
name: &'static str|
-> Result<ValuePerBlockCumulativeRolling> {
ValuePerBlockCumulativeRolling::forced_import(
let matured_version = v + MATURED_VERSION;
let matured_sats =
EagerVec::<ColumnarVec<PcoVec<Height, Sats>, AgeRangeId>>::forced_import(
db,
&format!("{prefix}_{name}_matured_supply"),
v,
&format!("{prefix}_age_range_matured_supply_cumulative_sats"),
matured_version,
)?;
let matured_cents =
EagerVec::<ColumnarVec<PcoVec<Height, Cents>, AgeRangeId>>::forced_import(
db,
&format!("{prefix}_age_range_matured_supply_cumulative_cents"),
matured_version,
)?;
let matured_sats_ref = matured_sats.read_only_clone();
let matured_cents_ref = matured_cents.read_only_clone();
let matured = AgeRangeId::series(CohortContext::Utxo, |column, name| {
LazyColumnValuePerBlockCumulativeRolling::new(
&format!("{name}_matured_supply"),
matured_version,
&matured_sats_ref,
&matured_cents_ref,
column,
indexes,
cached_starts,
)
})?;
});
Ok(Self {
all,
@@ -320,7 +350,10 @@ impl UTXOCohorts<Rw> {
under_amount,
over_amount,
profitability,
age_range_supply,
matured,
cumulative_matured_sats: matured_sats,
cumulative_matured_cents: matured_cents,
all_supply_cache,
caches: UTXOCohortsTransientState::default(),
})
@@ -348,20 +381,18 @@ impl UTXOCohorts<Rw> {
age_range,
..
} = self;
caches
.fenwick
.compute_is_sth(&sth.metrics.filter, age_range.iter().map(|v| v.filter()));
caches.fenwick.compute_is_sth(&sth.metrics.filter);
let maps: Vec<_> = age_range
let maps: Vec<_> = AgeRangeId::ALL
.iter()
.enumerate()
.filter_map(|(i, sub)| {
.filter_map(|&id| {
let sub = id.select(age_range);
let state = sub.state.as_ref()?;
let map = state.cost_basis_map();
if map.is_empty() {
return None;
}
Some((map, caches.fenwick.is_sth_at(i)))
Some((map, caches.fenwick.is_sth(id)))
})
.collect();
caches.fenwick.bulk_init(maps.into_iter());
@@ -377,9 +408,10 @@ impl UTXOCohorts<Rw> {
let Self {
caches, age_range, ..
} = self;
for (i, sub) in age_range.iter().enumerate() {
for &id in AgeRangeId::ALL {
let sub = id.select(age_range);
if let Some(state) = sub.state.as_ref() {
let is_sth = caches.fenwick.is_sth_at(i);
let is_sth = caches.fenwick.is_sth(id);
state.for_each_cost_basis_pending(|&price, delta| {
caches.fenwick.apply_delta(price, delta, is_sth);
});
@@ -390,9 +422,49 @@ impl UTXOCohorts<Rw> {
/// Push maturation sats to the matured vecs for the given height.
#[inline(always)]
pub(crate) fn push_maturation(&mut self, matured: &AgeRange<Sats>) {
for (v, &sats) in self.matured.iter_mut().zip(matured.iter()) {
v.push_block_sats(sats);
let mut cumulative = self
.cumulative_matured_sats
.collect_last()
.unwrap_or_default();
for column in AgeRangeId::ALL {
*column.get_mut(&mut cumulative) += *column.select(matured);
}
self.cumulative_matured_sats.push(cumulative);
}
fn compute_matured_cents(
&mut self,
starting_height: Height,
prices: &price::Vecs,
exit: &Exit,
) -> Result<()> {
let cumulative_sats = self.cumulative_matured_sats.read_only_clone();
let mut previous_sats = None;
self.cumulative_matured_cents.compute_transform2_batched(
starting_height,
&cumulative_sats,
&prices.spot.cents.height,
WRITE_INTERVAL,
|(height, current_sats, price, target)| {
let prior_sats = previous_sats.replace(current_sats).unwrap_or_else(|| {
height
.decremented()
.and_then(|height| cumulative_sats.collect_one(height))
.unwrap_or_default()
});
let mut current_cents = target.collect_last().unwrap_or_default();
for column in AgeRangeId::ALL {
let block_sats = *column.get(&current_sats) - *column.get(&prior_sats);
*column.get_mut(&mut current_cents) += SatsToCents::apply(block_sats, price);
}
(height, current_cents)
},
exit,
)?;
Ok(())
}
pub(crate) fn par_iter_separate_mut(
@@ -574,10 +646,8 @@ impl UTXOCohorts<Rw> {
.try_for_each(|v| v.compute_rest_part1(prices, starting_lengths, exit))?;
}
// Compute matured cumulative + cents from sats × price
self.matured
.par_iter_mut()
.try_for_each(|v| v.compute_rest(starting_lengths.height, prices, exit))?;
// Compute matured cumulative cents from the cumulative sats matrix × price.
self.compute_matured_cents(starting_lengths.height, prices, exit)?;
// Compute profitability supply cents and realized price
self.profitability.compute(prices, starting_lengths, exit)?;
@@ -751,11 +821,9 @@ impl UTXOCohorts<Rw> {
vecs.extend(v.metrics.collect_all_vecs_mut());
}
vecs.extend(self.profitability.collect_all_vecs_mut());
for v in self.matured.iter_mut() {
let inner = &mut v.inner;
vecs.push(&mut inner.cumulative.sats.height);
vecs.push(&mut inner.cumulative.cents.height);
}
vecs.extend(self.age_range_supply.stored_vecs_mut());
vecs.push(&mut self.cumulative_matured_sats);
vecs.push(&mut self.cumulative_matured_cents);
vecs.into_par_iter()
}
@@ -768,13 +836,12 @@ impl UTXOCohorts<Rw> {
pub(crate) fn min_stateful_len(&self) -> Height {
self.iter_separate()
.map(|v| Height::from(v.min_stateful_len()))
.chain(
self.matured
.iter()
.map(|v| Height::from(v.cumulative.sats.height.len())),
)
.chain(std::iter::once(Height::from(
self.cumulative_matured_sats.len(),
)))
.min()
.unwrap_or_default()
.min(Height::from(self.age_range_supply.len()))
.min(Height::from(self.profitability.min_stateful_len()))
.min(Height::from(self.all.min_stateful_len()))
.min(Height::from(self.sth.min_stateful_len()))
@@ -828,6 +895,7 @@ impl UTXOCohorts<Rw> {
sth,
lth,
age_range,
age_range_supply,
..
} = self;
@@ -841,11 +909,19 @@ impl UTXOCohorts<Rw> {
let mut sth_ccap = (0u128, 0u128);
let mut lth_ccap = (0u128, 0u128);
for ar in age_range.iter_mut() {
let mut total_supply = AgeRangeId::from_fn(|_| Sats::ZERO);
let mut supply_in_profit = AgeRangeId::from_fn(|_| Sats::ZERO);
let mut supply_in_loss = AgeRangeId::from_fn(|_| Sats::ZERO);
for &id in AgeRangeId::ALL {
let ar = id.select_mut(age_range);
if let Some(state) = ar.state.as_mut() {
all_acc.add(&state.realized);
let u = state.compute_unrealized_state(height_price);
*id.get_mut(&mut total_supply) = state.supply.value;
*id.get_mut(&mut supply_in_profit) = u.supply_in_profit;
*id.get_mut(&mut supply_in_loss) = u.supply_in_loss;
all_ccap.0 += u.capitalized_cap_in_profit_raw;
all_ccap.1 += u.capitalized_cap_in_loss_raw;
@@ -861,6 +937,8 @@ impl UTXOCohorts<Rw> {
}
}
age_range_supply.push(total_supply, supply_in_profit, supply_in_loss);
let all_capitalized_price = all.metrics.realized.push_accum(&all_acc);
sth.metrics.realized.push_accum(&sth_acc);
lth.metrics.realized.push_accum(&lth_acc);
@@ -1,11 +1,16 @@
use std::{cmp::Reverse, collections::BinaryHeap, path::Path};
use std::{
cmp::Reverse,
collections::{BTreeMap, BinaryHeap},
path::Path,
};
use brk_cohort::{
AGE_RANGE_NAMES, CohortContext, Filtered, PROFITABILITY_RANGE_COUNT, TERM_NAMES, UTXO_ALL_NAME,
AgeRangeId, CohortContext, Filtered, PROFITABILITY_RANGE_COUNT, TERM_NAMES, UTXO_ALL_NAME,
};
use brk_error::Result;
use brk_types::{Cents, CentsCompact, Date, Dollars, PartsPerMillion32, Sats, UrpdRaw};
use rayon::prelude::*;
use vecdb::ColumnId;
use crate::distribution::metrics::{CostBasis, ProfitabilityMetrics};
@@ -45,20 +50,16 @@ impl UTXOCohorts {
/// consumers can avoid writing and immediately rereading the current day.
pub(crate) fn age_range_urpd_entries(
&self,
) -> impl Iterator<Item = (usize, bool, CentsCompact, Sats)> + '_ {
let sth_filter = &self.sth.metrics.filter;
self.age_range
.iter()
.enumerate()
.flat_map(move |(age, cohort)| {
let is_sth = sth_filter.includes(cohort.filter());
cohort.state.iter().flat_map(move |state| {
state
.cost_basis_map()
.iter()
.map(move |(&price, &sats)| (age, is_sth, rounded_urpd_price(price), sats))
})
) -> impl Iterator<Item = (AgeRangeId, CentsCompact, Sats)> + '_ {
AgeRangeId::ALL.iter().copied().flat_map(move |id| {
let cohort = id.select(&self.age_range);
cohort.state.iter().flat_map(move |state| {
state
.cost_basis_map()
.iter()
.map(move |(&price, &sats)| (id, rounded_urpd_price(price), sats))
})
})
}
/// Push all Fenwick-derived per-block results: percentiles, density, profitability.
@@ -82,12 +83,12 @@ impl UTXOCohorts {
fn write_disk_distributions(&mut self, date: Date, states_path: &Path) -> Result<()> {
let sth_filter = self.sth.metrics.filter.clone();
self.age_range
AgeRangeId::ALL
.iter()
.zip(AGE_RANGE_NAMES.iter())
.map(|&id| (id, id.select(&self.age_range)))
.collect::<Vec<_>>()
.into_par_iter()
.try_for_each(|(sub, name)| -> Result<()> {
.try_for_each(|(id, sub)| -> Result<()> {
let Some(state) = sub.state.as_ref() else {
return Ok(());
};
@@ -102,7 +103,7 @@ impl UTXOCohorts {
merged.push((rounded, sats));
}
}
let full = CohortContext::Utxo.prefixed(name.id);
let full = CohortContext::Utxo.prefixed(id.name().id);
UrpdRaw::write(states_path, &full, date, merged.into_iter())
})?;
@@ -173,15 +174,12 @@ fn push_profitability(
buckets: &[ProfitabilityRangeResult; PROFITABILITY_RANGE_COUNT],
metrics: &mut ProfitabilityMetrics,
) {
for (i, bucket) in metrics.range.as_array_mut().into_iter().enumerate() {
let r = &buckets[i];
bucket.push(
Sats::from(r.all_sats),
Sats::from(r.sth_sats),
raw_usd_to_dollars(r.all_usd),
raw_usd_to_dollars(r.sth_usd),
);
}
metrics.push_ranges(
(*buckets).map(|range| Sats::from(range.all_sats)),
(*buckets).map(|range| Sats::from(range.sth_sats)),
(*buckets).map(|range| raw_usd_to_dollars(range.all_usd)),
(*buckets).map(|range| raw_usd_to_dollars(range.sth_usd)),
);
}
// ---------------------------------------------------------------------------
@@ -243,7 +241,7 @@ impl MergeTarget {
/// K-way merge via BinaryHeap over BTreeMap iterators.
/// Only builds merged distribution for disk writes.
fn merge_k_way(
maps: &[(&std::collections::BTreeMap<CentsCompact, Sats>, bool)],
maps: &[(&BTreeMap<CentsCompact, Sats>, bool)],
targets: &mut AllSthLth<MergeTarget>,
) {
let mut iters: Vec<_> = maps
@@ -1,4 +1,4 @@
use brk_cohort::{AGE_BOUNDARIES, AGE_RANGE_COUNT, AgeRange};
use brk_cohort::{AGE_BOUNDARIES, AGE_RANGE_IDS, AgeRange};
use brk_types::{CostBasisSnapshot, ONE_HOUR_IN_SEC, Sats, Timestamp};
use vecdb::{Rw, unlikely};
@@ -35,17 +35,18 @@ impl UTXOCohorts<Rw> {
return AgeRange::default();
}
let mut matured = [Sats::ZERO; AGE_RANGE_COUNT];
// Get age_range cohort states (indexed 0..AGE_RANGE_COUNT)
// Cohort i covers hours [BOUNDARIES[i-1], BOUNDARIES[i])
// Cohort 0 covers [0, 1) hours
// The final cohort covers [15*365*24, infinity) hours
let mut age_cohorts: Vec<_> = self.age_range.iter_mut().map(|v| &mut v.state).collect();
let cached = &mut self.caches.tick_tock_cached_positions;
let mut matured = AgeRange::default();
let Self {
age_range, caches, ..
} = self;
let cached = &mut caches.tick_tock_cached_positions;
// For each boundary (in hours), find blocks that just crossed it
for (boundary_idx, &boundary_hours) in AGE_BOUNDARIES.iter().enumerate() {
for (boundary_idx, (&boundary_hours, adjacent)) in AGE_BOUNDARIES
.iter()
.zip(AGE_RANGE_IDS.windows(2))
.enumerate()
{
let boundary_seconds = (boundary_hours as u32) * ONE_HOUR_IN_SEC;
// Blocks crossing boundary B have timestamps in (prev - B*HOUR, curr - B*HOUR]
@@ -81,18 +82,21 @@ impl UTXOCohorts<Rw> {
.map_or(chain_state.len(), |pos| start_idx + pos);
// Move supply from younger cohort to older cohort
let younger = adjacent[0];
let older = adjacent[1];
let matured_sats = younger.select_mut(&mut matured);
for block_state in &chain_state[start_idx..end_idx] {
let snapshot = CostBasisSnapshot::from_utxo(block_state.price, &block_state.supply);
if let Some(state) = age_cohorts[boundary_idx].as_mut() {
if let Some(state) = younger.select_mut(age_range).state.as_mut() {
state.decrement_snapshot(&snapshot);
}
if let Some(state) = age_cohorts[boundary_idx + 1].as_mut() {
if let Some(state) = older.select_mut(age_range).state.as_mut() {
state.increment_snapshot(&snapshot);
}
matured[boundary_idx] += block_state.supply.value;
*matured_sats += block_state.supply.value;
}
}
AgeRange::from_array(matured)
matured
}
}
@@ -434,9 +434,7 @@ pub(crate) fn process_blocks(
// Update Fenwick tree from pending deltas (must happen before push_cohort_states drains pending)
vecs.utxo_cohorts.update_fenwick_from_pending();
let activity_totals = state.activity.totals();
let active_addr_count =
activity_totals.sending + activity_totals.receiving - activity_totals.bidirectional;
let active_addr_count = state.activity.active();
vecs.addrs.push_height(&state, active_addr_count);
let is_last_of_day = is_last_of_day[offset];
@@ -5,7 +5,7 @@ use brk_types::{StoredF32, StoredF64, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{AnyStoredVec, Exit, Rw, StorageMode};
use crate::internal::{Identity, LazyPerBlock, PerBlock, Windows};
use crate::internal::{ColumnarRollingWindows, Identity, LazyPerBlock};
use crate::{
distribution::{
@@ -26,7 +26,7 @@ pub struct ActivityFull<M: StorageMode = Rw> {
pub coinyears_destroyed: LazyPerBlock<StoredF64, StoredF64>,
pub dormancy: Windows<PerBlock<StoredF32, M>>,
pub dormancy: ColumnarRollingWindows<StoredF32, M>,
}
impl ActivityFull {
@@ -41,14 +41,12 @@ impl ActivityFull {
cfg.indexes,
);
let dormancy = Windows::try_from_fn(|suffix| {
PerBlock::forced_import(
cfg.db,
&cfg.name(&format!("dormancy_{suffix}")),
cfg.version + v1,
cfg.indexes,
)
})?;
let dormancy = ColumnarRollingWindows::forced_import(
cfg.db,
&cfg.name("dormancy"),
cfg.version + v1,
cfg.indexes,
)?;
Ok(Self {
inner,
@@ -71,9 +69,7 @@ impl ActivityFull {
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs = self.inner.collect_vecs_mut();
for d in self.dormancy.as_mut_array() {
vecs.push(&mut d.height);
}
vecs.push(self.dormancy.stored_mut());
vecs
}
@@ -96,28 +92,25 @@ impl ActivityFull {
self.inner
.compute_rest_part1(prices, starting_lengths, exit)?;
for ((dormancy, cdd_sum), tv_sum) in self
.dormancy
.as_mut_array()
.into_iter()
.zip(self.inner.coindays_destroyed.sum.as_array())
.zip(self.inner.minimal.transfer_volume.sum.0.as_array())
{
dormancy.height.compute_transform2(
starting_lengths.height,
&cdd_sum.height,
&tv_sum.btc.height,
|(i, rolling_cdd, rolling_btc, ..)| {
let btc = f64::from(rolling_btc);
if btc == 0.0 {
(i, StoredF32::from(0.0f32))
} else {
(i, StoredF32::from((f64::from(rolling_cdd) / btc) as f32))
}
},
exit,
)?;
}
let Self {
inner, dormancy, ..
} = self;
let cdd_sums = &inner.coindays_destroyed.sum;
let transfer_volume_sums = &inner.minimal.transfer_volume.sum.0;
dormancy.compute_columns2(
starting_lengths.height,
|window| &window.select(cdd_sums).height,
|window| &window.select(transfer_volume_sums).btc.height,
|_, rolling_cdd, rolling_btc| {
let btc = f64::from(rolling_btc);
if btc == 0.0 {
StoredF32::from(0.0f32)
} else {
StoredF32::from((f64::from(rolling_cdd) / btc) as f32)
}
},
exit,
)?;
Ok(())
}
@@ -6,8 +6,8 @@ use vecdb::{AnyStoredVec, Exit, Rw, StorageMode};
use crate::{
distribution::metrics::{
ActivityCore, AllSupplyCache, CohortMetricsBase, ImportConfig, OutputsBase, RealizedCore,
SupplyCore, UnrealizedCore,
ActivityCore, CohortMetricsBase, ImportConfig, OutputsBase, RealizedCore, SupplyCore,
UnrealizedCore,
},
price,
};
@@ -43,8 +43,7 @@ impl CohortMetricsBase for BasicCohortMetrics {
}
impl BasicCohortMetrics {
pub(crate) fn forced_import(cfg: &ImportConfig, all_supply: &AllSupplyCache) -> Result<Self> {
let supply = SupplyCore::forced_import(cfg, all_supply)?;
pub(crate) fn forced_import(cfg: &ImportConfig, supply: SupplyCore) -> Result<Self> {
let realized = RealizedCore::forced_import(cfg)?;
let unrealized = UnrealizedCore::forced_import(cfg, &realized.price.ppm)?;
@@ -26,12 +26,20 @@ pub struct CoreCohortMetrics<M: StorageMode = Rw> {
impl CoreCohortMetrics {
pub(crate) fn forced_import(cfg: &ImportConfig, all_supply: &AllSupplyCache) -> Result<Self> {
let supply = SupplyCore::forced_import(cfg, all_supply)?;
Self::forced_import_with_supply(cfg, supply)
}
pub(crate) fn forced_import_with_supply(
cfg: &ImportConfig,
supply: SupplyCore,
) -> Result<Self> {
let realized = RealizedCore::forced_import(cfg)?;
let unrealized = UnrealizedCore::forced_import(cfg, &realized.price.ppm)?;
Ok(Self {
filter: cfg.filter.clone(),
supply: Box::new(SupplyCore::forced_import(cfg, all_supply)?),
supply: Box::new(supply),
outputs: Box::new(OutputsBase::forced_import(cfg)?),
activity: Box::new(ActivityCore::forced_import(cfg)?),
realized: Box::new(realized),
@@ -9,8 +9,8 @@ use vecdb::{Exit, Rw, StorageMode};
use crate::{
distribution::AllChainCache,
distribution::metrics::{
ActivityFull, AllSupplyCache, CohortMetricsBase, CostBasis, ImportConfig, OutputsBase,
RealizedFull, RelativeWithExtended, SupplyCore, UnrealizedFull,
ActivityFull, CohortMetricsBase, CostBasis, ImportConfig, OutputsBase, RealizedFull,
RelativeWithExtended, SupplyCore, UnrealizedFull,
},
price,
};
@@ -69,10 +69,9 @@ impl CohortMetricsBase for ExtendedCohortMetrics {
impl ExtendedCohortMetrics {
pub(crate) fn forced_import(
cfg: &ImportConfig,
all_supply: &AllSupplyCache,
supply: SupplyCore,
all_chain: &AllChainCache,
) -> Result<Self> {
let supply = SupplyCore::forced_import(cfg, all_supply)?;
let realized = RealizedFull::forced_import(cfg, all_chain)?;
let unrealized = UnrealizedFull::forced_import(cfg, &realized.price.ppm)?;
@@ -8,7 +8,7 @@ use vecdb::{AnyStoredVec, Exit, ReadableVec, Rw, StorageMode};
use crate::{
distribution::AllChainCache,
distribution::metrics::{
ActivityFull, AdjustedSopr, AllSupplyCache, CohortMetricsBase, ImportConfig, RealizedFull,
ActivityFull, AdjustedSopr, CohortMetricsBase, ImportConfig, RealizedFull, SupplyCore,
UnrealizedFull,
},
price,
@@ -52,10 +52,10 @@ impl CohortMetricsBase for ExtendedAdjustedCohortMetrics {
impl ExtendedAdjustedCohortMetrics {
pub(crate) fn forced_import(
cfg: &ImportConfig,
all_supply: &AllSupplyCache,
supply: SupplyCore,
all_chain: &AllChainCache,
) -> Result<Self> {
let inner = ExtendedCohortMetrics::forced_import(cfg, all_supply, all_chain)?;
let inner = ExtendedCohortMetrics::forced_import(cfg, supply, all_chain)?;
let asopr = AdjustedSopr::forced_import(cfg)?;
Ok(Self {
inner,
@@ -9,11 +9,12 @@ use vecdb::{BytesVec, BytesVecValue, CachedBoxedVec, Database, ImportableVec};
use crate::{
indexes,
internal::{
CachedWindowStartVec, FiatPerBlock, FiatPerBlockCumulativeWithSums, FiatType, NumericValue,
PerBlock, PerBlockCumulativeRolling, PercentPerBlock, PercentRollingWindows, Price,
CachedWindowStartVec, ColumnarPercentRollingWindows, ColumnarRollingWindows,
ColumnarRollingWindowsFrom1w, FiatPerBlock, FiatPerBlockCumulativeWithSums, FiatType,
NumericValue, PerBlock, PerBlockCumulativeRolling, PercentPerBlock, Price,
PriceWithRatioPerBlock, RatioPerBlock, RollingWindow24hPerBlock, RollingWindows,
RollingWindowsFrom1w, SpotValuePerBlock, ValuePerBlock, ValuePerBlockCumulative,
ValuePerBlockCumulativeRolling, Windows,
SpotValuePerBlock, ValuePerBlock, ValuePerBlockCumulative, ValuePerBlockCumulativeRolling,
Windows,
},
};
@@ -43,7 +44,7 @@ impl_config_import!(
PercentPerBlock<PartsPerMillion32>,
PercentPerBlock<PartsPerMillionSigned32>,
PercentPerBlock<PartsPerMillionSigned64>,
PercentRollingWindows<PartsPerMillion32>,
ColumnarPercentRollingWindows<PartsPerMillion32>,
Price<PerBlock<Cents>>,
);
@@ -96,6 +97,11 @@ impl<T: NumericValue + JsonSchema> ConfigImport for RollingWindows<T> {
Self::forced_import(cfg.db, &cfg.name(suffix), cfg.version + offset, cfg.indexes)
}
}
impl<T: NumericValue + JsonSchema> ConfigImport for ColumnarRollingWindows<T> {
fn config_import(cfg: &ImportConfig, suffix: &str, offset: Version) -> Result<Self> {
Self::forced_import(cfg.db, &cfg.name(suffix), cfg.version + offset, cfg.indexes)
}
}
impl<T: NumericValue + JsonSchema> ConfigImport for RollingWindow24hPerBlock<T> {
fn config_import(cfg: &ImportConfig, suffix: &str, offset: Version) -> Result<Self> {
Self::forced_import(cfg.db, &cfg.name(suffix), cfg.version + offset, cfg.indexes)
@@ -123,7 +129,7 @@ impl<C: FiatType> ConfigImport for FiatPerBlockCumulativeWithSums<C> {
)
}
}
impl<T: NumericValue + JsonSchema> ConfigImport for RollingWindowsFrom1w<T> {
impl<T: NumericValue + JsonSchema> ConfigImport for ColumnarRollingWindowsFrom1w<T> {
fn config_import(cfg: &ImportConfig, suffix: &str, offset: Version) -> Result<Self> {
Self::forced_import(cfg.db, &cfg.name(suffix), cfg.version + offset, cfg.indexes)
}
@@ -89,6 +89,8 @@ impl CostBasis {
.len()
.min(self.max.cents.height.len())
.min(self.supply_density.ppm.height.len())
.min(self.per_coin.prices.height.len())
.min(self.per_dollar.prices.height.len())
}
#[inline(always)]
@@ -130,18 +132,8 @@ impl CostBasis {
&mut self.max.cents.height,
&mut self.supply_density.ppm.height,
];
vecs.extend(
self.per_coin
.vecs
.iter_mut()
.map(|v| &mut v.cents.height as &mut dyn AnyStoredVec),
);
vecs.extend(
self.per_dollar
.vecs
.iter_mut()
.map(|v| &mut v.cents.height as &mut dyn AnyStoredVec),
);
vecs.push(self.per_coin.prices.stored_mut());
vecs.push(self.per_dollar.prices.stored_mut());
vecs
}
@@ -1,256 +1,159 @@
use brk_cohort::{Loss, Profit, ProfitabilityRange};
use brk_cohort::{
Loss, PROFITABILITY_RANGE_COUNT, Profit, ProfitabilityId, ProfitabilityRange, ProfitabilityRow,
};
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Bitcoin, Cents, Dollars, Height, PartsPerMillionSigned32, Sats, Version};
use vecdb::{AnyStoredVec, AnyVec, CachedBoxedVec, Database, Exit, Rw, StorageMode, WritableVec};
use vecdb::{
AnyStoredVec, AnyVec, CachedBoxedVec, ColumnId, ColumnarVec, Database, EagerVec, Exit,
ImportableVec, PcoVec, ReadOnlyClone, ReadOnlyColumnarVec, Rw, StorageMode, WritableVec,
};
use crate::{
indexes,
internal::{
CachedWindowStartVec, PerBlock, RatioPerBlock, SpotValuePerBlock,
SpotValuePerBlockWithDeltas, Windows,
CachedWindowStartVec, LazyColumnPerBlock, LazyColumnRatioPerBlock,
LazyColumnSpotValuePerBlock, LazyColumnSpotValuePerBlockWithDeltas, Windows,
},
price,
};
#[derive(Traversable)]
#[derive(Clone, Traversable)]
pub struct WithSth<All, Sth = All> {
pub all: All,
pub sth: Sth,
}
#[derive(Traversable)]
pub struct ProfitabilityBucket<M: StorageMode = Rw> {
pub supply: WithSth<SpotValuePerBlockWithDeltas<M>, SpotValuePerBlock<M>>,
pub realized_cap: WithSth<PerBlock<Dollars, M>>,
pub unrealized_pnl: WithSth<PerBlock<Dollars, M>>,
pub nupl: RatioPerBlock<PartsPerMillionSigned32, M>,
#[derive(Clone, Traversable)]
pub struct ProfitabilityBucket {
pub supply: WithSth<
LazyColumnSpotValuePerBlockWithDeltas<ProfitabilityId>,
LazyColumnSpotValuePerBlock<ProfitabilityId>,
>,
pub realized_cap: WithSth<
LazyColumnPerBlock<Dollars, ProfitabilityId>,
LazyColumnPerBlock<Dollars, ProfitabilityId>,
>,
pub unrealized_pnl: WithSth<
LazyColumnPerBlock<Dollars, ProfitabilityId>,
LazyColumnPerBlock<Dollars, ProfitabilityId>,
>,
pub nupl: LazyColumnRatioPerBlock<PartsPerMillionSigned32, ProfitabilityId>,
}
impl<M: StorageMode> ProfitabilityBucket<M> {
fn min_len(&self) -> usize {
self.supply
.all
.sats
.height
.len()
.min(self.realized_cap.all.height.len())
}
struct ProfitabilitySources {
all_supply_sats: ReadOnlyColumnarVec<PcoVec<Height, Sats>, ProfitabilityId>,
sth_supply_sats: ReadOnlyColumnarVec<PcoVec<Height, Sats>, ProfitabilityId>,
all_realized_cap: ReadOnlyColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>,
sth_realized_cap: ReadOnlyColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>,
all_unrealized_pnl: ReadOnlyColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>,
sth_unrealized_pnl: ReadOnlyColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>,
nupl: ReadOnlyColumnarVec<PcoVec<Height, PartsPerMillionSigned32>, ProfitabilityId>,
}
impl ProfitabilityBucket {
fn forced_import(
db: &Database,
fn new(
name: &str,
version: Version,
column: ProfitabilityId,
sources: &ProfitabilitySources,
indexes: &indexes::Vecs,
cached_starts: &Windows<&CachedWindowStartVec>,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
Ok(Self {
) -> Self {
Self {
supply: WithSth {
all: SpotValuePerBlockWithDeltas::forced_import(
db,
all: LazyColumnSpotValuePerBlockWithDeltas::new(
&format!("{name}_supply"),
version,
&sources.all_supply_sats,
column,
indexes,
cached_starts,
spot_price,
)?,
sth: SpotValuePerBlock::forced_import(
db,
),
sth: LazyColumnSpotValuePerBlock::new(
&format!("{name}_sth_supply"),
version,
&sources.sth_supply_sats,
column,
indexes,
spot_price,
)?,
),
},
realized_cap: WithSth {
all: PerBlock::forced_import(
db,
all: LazyColumnPerBlock::new(
&format!("{name}_realized_cap"),
version,
&sources.all_realized_cap,
column,
indexes,
)?,
sth: PerBlock::forced_import(
db,
),
sth: LazyColumnPerBlock::new(
&format!("{name}_sth_realized_cap"),
version,
&sources.sth_realized_cap,
column,
indexes,
)?,
),
},
unrealized_pnl: WithSth {
all: PerBlock::forced_import(
db,
all: LazyColumnPerBlock::new(
&format!("{name}_unrealized_pnl"),
version,
&sources.all_unrealized_pnl,
column,
indexes,
)?,
sth: PerBlock::forced_import(
db,
),
sth: LazyColumnPerBlock::new(
&format!("{name}_sth_unrealized_pnl"),
version,
&sources.sth_unrealized_pnl,
column,
indexes,
)?,
),
},
nupl: RatioPerBlock::forced_import_ppm(
db,
nupl: LazyColumnRatioPerBlock::new(
&format!("{name}_nupl"),
version + Version::ONE,
version + Version::new(4),
&sources.nupl,
column,
indexes,
)?,
})
}
#[inline(always)]
pub(crate) fn push(
&mut self,
supply: Sats,
sth_supply: Sats,
realized_cap: Dollars,
sth_realized_cap: Dollars,
) {
self.supply.all.sats.height.push(supply);
self.supply.sth.sats.height.push(sth_supply);
self.realized_cap.all.height.push(realized_cap);
self.realized_cap.sth.height.push(sth_realized_cap);
}
pub(crate) fn compute(
&mut self,
prices: &price::Vecs,
starting_lengths: &Lengths,
is_profit: bool,
exit: &Exit,
) -> Result<()> {
let max_from = starting_lengths.height;
self.unrealized_pnl.all.height.compute_transform3(
max_from,
&prices.spot.cents.height,
&self.realized_cap.all.height,
&self.supply.all.sats.height,
|(i, spot, cap, supply, ..)| {
let mv = f64::from(Dollars::from(spot)) * f64::from(Bitcoin::from(supply));
let rc = f64::from(cap);
let pnl = if is_profit { mv - rc } else { rc - mv }.max(0.0);
(i, Dollars::from(pnl))
},
exit,
)?;
self.unrealized_pnl.sth.height.compute_transform3(
max_from,
&prices.spot.cents.height,
&self.realized_cap.sth.height,
&self.supply.sth.sats.height,
|(i, spot, cap, supply, ..)| {
let mv = f64::from(Dollars::from(spot)) * f64::from(Bitcoin::from(supply));
let rc = f64::from(cap);
let pnl = if is_profit { mv - rc } else { rc - mv }.max(0.0);
(i, Dollars::from(pnl))
},
exit,
)?;
self.nupl.ppm.height.compute_transform3(
max_from,
&prices.spot.cents.height,
&self.realized_cap.all.height,
&self.supply.all.sats.height,
|(i, spot, cap_dollars, supply_sats, ..)| {
let p = spot.as_u128();
let supply = supply_sats.as_u128();
if p == 0 || supply == 0 {
(i, PartsPerMillionSigned32::ZERO)
} else {
let rp = Cents::from(cap_dollars).as_u128() * Sats::ONE_BTC_U128 / supply;
let ratio = (p as f64 - rp as f64) / p as f64;
(i, PartsPerMillionSigned32::from(ratio))
}
},
exit,
)?;
Ok(())
}
pub(crate) fn compute_from_ranges(
&mut self,
prices: &price::Vecs,
starting_lengths: &Lengths,
is_profit: bool,
sources: &[&ProfitabilityBucket],
exit: &Exit,
) -> Result<()> {
let max_from = starting_lengths.height;
self.supply.all.sats.height.compute_sum_of_others(
max_from,
&sources
.iter()
.map(|s| &s.supply.all.sats.height)
.collect::<Vec<_>>(),
exit,
)?;
self.supply.sth.sats.height.compute_sum_of_others(
max_from,
&sources
.iter()
.map(|s| &s.supply.sth.sats.height)
.collect::<Vec<_>>(),
exit,
)?;
self.realized_cap.all.height.compute_sum_of_others(
max_from,
&sources
.iter()
.map(|s| &s.realized_cap.all.height)
.collect::<Vec<_>>(),
exit,
)?;
self.realized_cap.sth.height.compute_sum_of_others(
max_from,
&sources
.iter()
.map(|s| &s.realized_cap.sth.height)
.collect::<Vec<_>>(),
exit,
)?;
self.compute(prices, starting_lengths, is_profit, exit)
}
pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
vec![
&mut self.supply.all.inner.sats.height as &mut dyn AnyStoredVec,
&mut self.supply.sth.sats.height,
&mut self.realized_cap.all.height,
&mut self.realized_cap.sth.height,
&mut self.unrealized_pnl.all.height,
&mut self.unrealized_pnl.sth.height,
&mut self.nupl.ppm.height,
]
),
}
}
}
/// All profitability metrics: 25 ranges + 14 profit thresholds + 9 loss thresholds.
#[derive(Traversable)]
pub struct ProfitabilityMetrics<M: StorageMode = Rw> {
pub range: ProfitabilityRange<ProfitabilityBucket<M>>,
pub profit: Profit<ProfitabilityBucket<M>>,
pub loss: Loss<ProfitabilityBucket<M>>,
pub range: ProfitabilityRange<ProfitabilityBucket>,
pub profit: Profit<ProfitabilityBucket>,
pub loss: Loss<ProfitabilityBucket>,
pub all_supply_sats: M::Stored<EagerVec<ColumnarVec<PcoVec<Height, Sats>, ProfitabilityId>>>,
pub sth_supply_sats: M::Stored<EagerVec<ColumnarVec<PcoVec<Height, Sats>, ProfitabilityId>>>,
pub all_realized_cap:
M::Stored<EagerVec<ColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>>>,
pub sth_realized_cap:
M::Stored<EagerVec<ColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>>>,
pub all_unrealized_pnl:
M::Stored<EagerVec<ColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>>>,
pub sth_unrealized_pnl:
M::Stored<EagerVec<ColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>>>,
pub nupl:
M::Stored<EagerVec<ColumnarVec<PcoVec<Height, PartsPerMillionSigned32>, ProfitabilityId>>>,
}
impl<M: StorageMode> ProfitabilityMetrics<M> {
pub fn iter(&self) -> impl Iterator<Item = &ProfitabilityBucket<M>> {
pub fn iter(&self) -> impl Iterator<Item = &ProfitabilityBucket> {
self.range
.iter()
.chain(self.profit.iter())
.chain(self.loss.iter())
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut ProfitabilityBucket<M>> {
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut ProfitabilityBucket> {
self.range
.iter_mut()
.chain(self.profit.iter_mut())
@@ -258,7 +161,7 @@ impl<M: StorageMode> ProfitabilityMetrics<M> {
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.range.iter().map(|b| b.min_len()).min().unwrap_or(0)
self.all_supply_sats.len().min(self.all_realized_cap.len())
}
}
@@ -270,75 +173,274 @@ impl ProfitabilityMetrics {
cached_starts: &Windows<&CachedWindowStartVec>,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let range = ProfitabilityRange::try_new(|name| {
ProfitabilityBucket::forced_import(
let source_version = version + Version::TWO;
let all_supply_sats =
EagerVec::<ColumnarVec<PcoVec<Height, Sats>, ProfitabilityId>>::forced_import(
db,
"profitability_all_supply_sats",
source_version,
)?;
let sth_supply_sats =
EagerVec::<ColumnarVec<PcoVec<Height, Sats>, ProfitabilityId>>::forced_import(
db,
"profitability_sth_supply_sats",
source_version,
)?;
let all_realized_cap =
EagerVec::<ColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>>::forced_import(
db,
"profitability_all_realized_cap",
source_version,
)?;
let sth_realized_cap =
EagerVec::<ColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>>::forced_import(
db,
"profitability_sth_realized_cap",
source_version,
)?;
let all_unrealized_pnl =
EagerVec::<ColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>>::forced_import(
db,
"profitability_all_unrealized_pnl",
source_version,
)?;
let sth_unrealized_pnl =
EagerVec::<ColumnarVec<PcoVec<Height, Dollars>, ProfitabilityId>>::forced_import(
db,
"profitability_sth_unrealized_pnl",
source_version,
)?;
let nupl = EagerVec::<ColumnarVec<
PcoVec<Height, PartsPerMillionSigned32>,
ProfitabilityId,
>>::forced_import(
db,
"profitability_nupl_ppm",
source_version + Version::new(4),
)?;
let sources = ProfitabilitySources {
all_supply_sats: all_supply_sats.read_only_clone(),
sth_supply_sats: sth_supply_sats.read_only_clone(),
all_realized_cap: all_realized_cap.read_only_clone(),
sth_realized_cap: sth_realized_cap.read_only_clone(),
all_unrealized_pnl: all_unrealized_pnl.read_only_clone(),
sth_unrealized_pnl: sth_unrealized_pnl.read_only_clone(),
nupl: nupl.read_only_clone(),
};
let range = ProfitabilityId::range_series(|column, name| {
ProfitabilityBucket::new(
name,
version,
column,
&sources,
indexes,
cached_starts,
spot_price,
)
})?;
});
let aggregate_version = version + Version::TWO;
let profit = Profit::try_new(|name| {
ProfitabilityBucket::forced_import(
db,
let profit = ProfitabilityId::profit_series(|column, name| {
ProfitabilityBucket::new(
name,
aggregate_version,
column,
&sources,
indexes,
cached_starts,
spot_price,
)
})?;
});
let loss = Loss::try_new(|name| {
ProfitabilityBucket::forced_import(
db,
let loss = ProfitabilityId::loss_series(|column, name| {
ProfitabilityBucket::new(
name,
aggregate_version,
column,
&sources,
indexes,
cached_starts,
spot_price,
)
})?;
});
Ok(Self {
range,
profit,
loss,
all_supply_sats,
sth_supply_sats,
all_realized_cap,
sth_realized_cap,
all_unrealized_pnl,
sth_unrealized_pnl,
nupl,
})
}
#[inline(always)]
pub(crate) fn push_ranges(
&mut self,
all_supply_sats: [Sats; PROFITABILITY_RANGE_COUNT],
sth_supply_sats: [Sats; PROFITABILITY_RANGE_COUNT],
all_realized_cap: [Dollars; PROFITABILITY_RANGE_COUNT],
sth_realized_cap: [Dollars; PROFITABILITY_RANGE_COUNT],
) {
self.all_supply_sats
.push(ProfitabilityRow::from_ranges(all_supply_sats));
self.sth_supply_sats
.push(ProfitabilityRow::from_ranges(sth_supply_sats));
self.all_realized_cap
.push(ProfitabilityRow::from_ranges(all_realized_cap));
self.sth_realized_cap
.push(ProfitabilityRow::from_ranges(sth_realized_cap));
}
pub(crate) fn compute(
&mut self,
prices: &price::Vecs,
starting_lengths: &Lengths,
exit: &Exit,
) -> Result<()> {
for (is_profit, bucket) in self.range.iter_mut_with_is_profit() {
bucket.compute(prices, starting_lengths, is_profit, exit)?;
}
let Self {
all_supply_sats,
sth_supply_sats,
all_realized_cap,
sth_realized_cap,
all_unrealized_pnl,
sth_unrealized_pnl,
nupl,
..
} = self;
let max_from = starting_lengths.height;
let range_arr = self.range.as_array();
for (threshold, sources) in self.profit.iter_mut_with_growing_prefix(&range_arr) {
threshold.compute_from_ranges(prices, starting_lengths, true, sources, exit)?;
}
for (threshold, sources) in self.loss.iter_mut_with_growing_suffix(&range_arr) {
threshold.compute_from_ranges(prices, starting_lengths, false, sources, exit)?;
}
all_unrealized_pnl.compute_transform3(
max_from,
&prices.spot.cents.height,
all_realized_cap,
all_supply_sats,
|(height, spot, cap, supply, ..)| (height, unrealized_pnl_row(spot, cap, supply)),
exit,
)?;
sth_unrealized_pnl.compute_transform3(
max_from,
&prices.spot.cents.height,
sth_realized_cap,
sth_supply_sats,
|(height, spot, cap, supply, ..)| (height, unrealized_pnl_row(spot, cap, supply)),
exit,
)?;
nupl.compute_transform3(
max_from,
&prices.spot.cents.height,
all_realized_cap,
all_supply_sats,
|(height, spot, cap, supply, ..)| (height, nupl_row(spot, cap, supply)),
exit,
)?;
Ok(())
}
pub(crate) fn collect_all_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs = Vec::new();
for bucket in self.iter_mut() {
vecs.extend(bucket.collect_all_vecs_mut());
}
vecs
pub(crate) fn collect_all_vecs_mut(&mut self) -> [&mut dyn AnyStoredVec; 7] {
[
&mut self.all_supply_sats as &mut dyn AnyStoredVec,
&mut self.sth_supply_sats,
&mut self.all_realized_cap,
&mut self.sth_realized_cap,
&mut self.all_unrealized_pnl,
&mut self.sth_unrealized_pnl,
&mut self.nupl,
]
}
}
fn unrealized_pnl_row(
spot: Cents,
cap: ProfitabilityRow<Dollars>,
supply: ProfitabilityRow<Sats>,
) -> ProfitabilityRow<Dollars> {
ProfitabilityId::from_fn(|column| {
let market_value =
f64::from(Dollars::from(spot)) * f64::from(Bitcoin::from(*column.get(&supply)));
let realized_cap = f64::from(*column.get(&cap));
let pnl = if column.is_profit() {
market_value - realized_cap
} else {
realized_cap - market_value
}
.max(0.0);
Dollars::from(pnl)
})
}
fn nupl_row(
spot: Cents,
cap: ProfitabilityRow<Dollars>,
supply: ProfitabilityRow<Sats>,
) -> ProfitabilityRow<PartsPerMillionSigned32> {
ProfitabilityId::from_fn(|column| {
let spot = spot.as_u128();
let supply = column.get(&supply).as_u128();
if spot == 0 || supply == 0 {
PartsPerMillionSigned32::ZERO
} else {
let realized_price =
Cents::from(*column.get(&cap)).as_u128() * Sats::ONE_BTC_U128 / supply;
PartsPerMillionSigned32::from((spot as f64 - realized_price as f64) / spot as f64)
}
})
}
#[cfg(test)]
mod tests {
use super::*;
use brk_cohort::PROFIT_COUNT;
#[test]
fn expanded_profitability_thresholds_match_prefix_and_suffix_sums() {
let ranges = std::array::from_fn(|index| Sats::from(index as u64 + 1));
let row = ProfitabilityRow::from_ranges(ranges);
let sum = |values: &[Sats]| {
values
.iter()
.copied()
.fold(Sats::default(), |total, value| total + value)
};
for (threshold, &column) in ProfitabilityId::profit_ids().iter().enumerate() {
assert_eq!(
*column.get(&row),
sum(&ranges[..PROFIT_COUNT + 1 - threshold])
);
}
for (threshold, &column) in ProfitabilityId::loss_ids().iter().enumerate() {
assert_eq!(
*column.get(&row),
sum(&ranges[PROFIT_COUNT + 1 + threshold..])
);
}
}
#[test]
fn derived_rows_preserve_profit_and_loss_polarity() {
let supply = ProfitabilityId::from_fn(|_| Sats::ONE_BTC);
let cap = ProfitabilityId::from_fn(|column| {
Dollars::from(if column.is_profit() { 1.0 } else { 3.0 })
});
let spot = Cents::from(200_u64);
let pnl = unrealized_pnl_row(spot, cap.clone(), supply.clone());
let nupl = nupl_row(spot, cap, supply);
for column in ProfitabilityId::ALL {
assert_eq!(*column.get(&pnl), Dollars::from(1.0));
assert_eq!(
*column.get(&nupl),
PartsPerMillionSigned32::from(if column.is_profit() { 0.5 } else { -0.5 })
);
}
}
}
@@ -2,16 +2,16 @@ use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Cents, Height, StoredF64, Version};
use vecdb::{Exit, ReadableVec, Rw, StorageMode};
use vecdb::{BinaryTransform, Exit, ReadableVec, Rw, StorageMode};
use crate::{
distribution::metrics::ImportConfig,
internal::{PerBlockCumulativeRolling, RatioCents64, RollingWindows},
internal::{ColumnarRollingWindows, PerBlockCumulativeRolling, RatioCents64},
};
#[derive(Traversable)]
pub struct AdjustedSopr<M: StorageMode = Rw> {
pub ratio: RollingWindows<StoredF64, M>,
pub ratio: ColumnarRollingWindows<StoredF64, M>,
pub transfer_volume: PerBlockCumulativeRolling<Cents, M>,
pub value_destroyed: PerBlockCumulativeRolling<Cents, M>,
}
@@ -48,20 +48,15 @@ impl AdjustedSopr {
exit,
)?;
for ((sopr, tv), vd) in self
.ratio
.as_mut_array()
.into_iter()
.zip(self.transfer_volume.sum.as_array())
.zip(self.value_destroyed.sum.as_array())
{
sopr.compute_binary::<Cents, Cents, RatioCents64>(
starting_lengths.height,
&tv.height,
&vd.height,
exit,
)?;
}
self.ratio.compute_columns2(
starting_lengths.height,
|window| &window.select(&self.transfer_volume.sum).height,
|window| &window.select(&self.value_destroyed.sum).height,
|_, transfer_volume, value_destroyed| {
RatioCents64::apply(transfer_volume, value_destroyed)
},
exit,
)?;
Ok(())
}
@@ -6,15 +6,18 @@ use brk_types::{
PartsPerMillion64, PartsPerMillionSigned64, StoredF64, Version,
};
use derive_more::{Deref, DerefMut};
use vecdb::{AnyStoredVec, AnyVec, BytesVec, Exit, ReadableVec, Rw, StorageMode, WritableVec};
use vecdb::{
AnyStoredVec, AnyVec, BinaryTransform, BytesVec, Exit, ReadableVec, Rw, StorageMode,
WritableVec,
};
use crate::{
distribution::AllChainCache,
distribution::state::{CohortState, CostBasisData, RealizedState, WithCapital},
internal::{
ColumnarPercentRollingWindows, ColumnarRollingWindows, ColumnarRollingWindowsFrom1w,
FiatPerBlockCumulativeWithSums, LazyPercentPerBlock, PercentPerBlock,
PercentRollingWindows, PriceWithRatioPerBlock, RatioCents, RatioCents64,
RatioCentsSignedCents, RollingWindows, RollingWindowsFrom1w,
PriceWithRatioPerBlock, RatioCents, RatioCents64, RatioCentsSignedCents,
ValuePerBlockCumulativeRolling,
},
price,
@@ -35,7 +38,7 @@ pub struct RealizedNetPnl<M: StorageMode = Rw> {
#[derive(Traversable)]
pub struct RealizedSopr<M: StorageMode = Rw> {
#[traversable(rename = "ratio")]
pub ratio_extended: RollingWindowsFrom1w<StoredF64, M>,
pub ratio_extended: ColumnarRollingWindowsFrom1w<StoredF64, M>,
}
#[derive(Traversable)]
@@ -61,13 +64,13 @@ pub struct RealizedFull<M: StorageMode = Rw> {
#[traversable(wrap = "cap", rename = "to_own_mcap")]
pub cap_to_own_mcap: LazyPercentPerBlock<PartsPerMillion32>,
pub gross_pnl: FiatPerBlockCumulativeWithSums<Cents, M>,
pub sell_side_risk_ratio: PercentRollingWindows<PartsPerMillion32, M>,
pub sell_side_risk_ratio: ColumnarPercentRollingWindows<PartsPerMillion32, M>,
pub net_pnl: RealizedNetPnl<M>,
pub sopr: RealizedSopr<M>,
pub peak_regret: RealizedPeakRegret<M>,
pub capitalized: RealizedCapitalized<M>,
pub profit_to_loss_ratio: RollingWindows<StoredF64, M>,
pub profit_to_loss_ratio: ColumnarRollingWindows<StoredF64, M>,
#[traversable(hidden)]
cap_raw: M::Stored<BytesVec<Height, CentsSats>>,
@@ -246,21 +249,22 @@ impl RealizedFull {
)?;
// SOPR ratios from lazy rolling sums (1w, 1m, 1y)
for ((sopr, vc), vd) in self
.sopr
.ratio_extended
.as_mut_array()
.into_iter()
.zip(activity_transfer_volume.sum.0.as_array()[1..].iter())
.zip(self.core.sopr.value_destroyed.sum.as_array()[1..].iter())
{
sopr.compute_binary::<Cents, Cents, RatioCents64>(
starting_lengths.height,
&vc.cents.height,
&vd.height,
exit,
)?;
}
self.sopr.ratio_extended.compute_columns2(
starting_lengths.height,
|window| {
&window
.select_full(&activity_transfer_volume.sum.0)
.cents
.height
},
|window| {
&window
.select_full(&self.core.sopr.value_destroyed.sum)
.height
},
|_, value_created, value_destroyed| RatioCents64::apply(value_created, value_destroyed),
exit,
)?;
// Gross PnL
self.gross_pnl.compute_from_cumulative_pair(
@@ -281,35 +285,24 @@ impl RealizedFull {
exit,
)?;
// Sell-side risk ratios
for (ssrr, rv) in self
.sell_side_risk_ratio
.as_mut_array()
.into_iter()
.zip(self.gross_pnl.sum.as_array())
{
ssrr.compute_binary::<Cents, Cents, RatioCents<PartsPerMillion32>>(
starting_lengths.height,
&rv.cents.height,
&self.core.minimal.cap.cents.height,
exit,
)?;
}
self.sell_side_risk_ratio.compute_columns2(
starting_lengths.height,
|window| &window.select(&self.gross_pnl.sum).cents.height,
|_| &self.core.minimal.cap.cents.height,
|_, realized_value, realized_cap| {
RatioCents::<PartsPerMillion32>::apply(realized_value, realized_cap)
},
exit,
)?;
// Realized profit to loss ratios
for ((ratio, profit), loss) in self
.profit_to_loss_ratio
.as_mut_array()
.into_iter()
.zip(self.core.minimal.profit.sum.as_array())
.zip(self.core.minimal.loss.sum.as_array())
{
ratio.compute_binary::<Cents, Cents, RatioCents64>(
starting_lengths.height,
&profit.cents.height,
&loss.cents.height,
exit,
)?;
}
self.profit_to_loss_ratio.compute_columns2(
starting_lengths.height,
|window| &window.select(&self.core.minimal.profit.sum).cents.height,
|window| &window.select(&self.core.minimal.loss.sum).cents.height,
|_, profit, loss| RatioCents64::apply(profit, loss),
exit,
)?;
Ok(())
}
@@ -0,0 +1,176 @@
use brk_cohort::{AGE_RANGE_IDS, AgeRangeId, Filter};
use brk_error::Result;
use brk_types::{Height, Sats, Version};
use vecdb::{
AnyStoredVec, AnyVec, ColumnId, ColumnarVec, Database, EagerVec, ImportableVec, PcoVec,
ReadOnlyClone, ReadableColumnarVec, Rw, StorageMode, WritableVec,
};
use crate::{
distribution::metrics::{AllSupplyCache, ImportConfig, SupplyCore},
internal::LazySpotValuePerBlock,
};
type Source<M> = M::Stored<EagerVec<ColumnarVec<PcoVec<Height, Sats>, AgeRangeId>>>;
pub(crate) struct AgeRangeSupplySources<M: StorageMode = Rw> {
total: Source<M>,
in_profit: Source<M>,
in_loss: Source<M>,
}
impl AgeRangeSupplySources {
pub(crate) fn forced_import(
db: &Database,
prefix: &str,
version: Version,
) -> Result<Self> {
Ok(Self {
total: EagerVec::forced_import(
db,
&format!("{prefix}_age_range_supply_sats"),
version,
)?,
in_profit: EagerVec::forced_import(
db,
&format!("{prefix}_age_range_supply_in_profit_sats"),
version,
)?,
in_loss: EagerVec::forced_import(
db,
&format!("{prefix}_age_range_supply_in_loss_sats"),
version,
)?,
})
}
pub(crate) fn all(&self, cfg: &ImportConfig) -> (SupplyCore, AllSupplyCache) {
let total_name = cfg.name("supply");
let total_source = self.total.read_only_clone().sum_columns(
&format!("{total_name}_sats"),
cfg.version,
AGE_RANGE_IDS,
);
let all_supply = AllSupplyCache::new(total_source);
let total = LazySpotValuePerBlock::from_sats_source(
&total_name,
cfg.version,
all_supply.readable_boxed_clone(),
cfg.indexes,
cfg.spot_price,
);
let in_profit =
Self::summed_spot(&self.in_profit, cfg, "supply_in_profit", AGE_RANGE_IDS);
let in_loss = Self::summed_spot(&self.in_loss, cfg, "supply_in_loss", AGE_RANGE_IDS);
(
SupplyCore::from_lazy_all(cfg, total, in_profit, in_loss),
all_supply,
)
}
pub(crate) fn column(
&self,
cfg: &ImportConfig,
column: AgeRangeId,
all_supply: &AllSupplyCache,
) -> SupplyCore {
SupplyCore::from_lazy(
cfg,
Self::column_spot(&self.total, cfg, "supply", column),
Self::column_spot(&self.in_profit, cfg, "supply_in_profit", column),
Self::column_spot(&self.in_loss, cfg, "supply_in_loss", column),
all_supply,
)
}
pub(crate) fn filtered(
&self,
cfg: &ImportConfig,
filter: &Filter,
all_supply: &AllSupplyCache,
) -> SupplyCore {
let columns: Vec<_> = AGE_RANGE_IDS
.into_iter()
.filter(|column| filter.includes(column.filter()))
.collect();
SupplyCore::from_lazy(
cfg,
Self::summed_spot(&self.total, cfg, "supply", columns.iter().copied()),
Self::summed_spot(
&self.in_profit,
cfg,
"supply_in_profit",
columns.iter().copied(),
),
Self::summed_spot(&self.in_loss, cfg, "supply_in_loss", columns),
all_supply,
)
}
fn column_spot(
source: &EagerVec<ColumnarVec<PcoVec<Height, Sats>, AgeRangeId>>,
cfg: &ImportConfig,
suffix: &str,
column: AgeRangeId,
) -> LazySpotValuePerBlock {
let name = cfg.name(suffix);
let source = source.read_only_clone().column(
&format!("{name}_sats"),
cfg.version,
column,
);
LazySpotValuePerBlock::from_sats_source(
&name,
cfg.version,
source,
cfg.indexes,
cfg.spot_price,
)
}
fn summed_spot(
source: &EagerVec<ColumnarVec<PcoVec<Height, Sats>, AgeRangeId>>,
cfg: &ImportConfig,
suffix: &str,
columns: impl IntoIterator<Item = AgeRangeId>,
) -> LazySpotValuePerBlock {
let name = cfg.name(suffix);
let source = source.read_only_clone().sum_columns(
&format!("{name}_sats"),
cfg.version,
columns,
);
LazySpotValuePerBlock::from_sats_source(
&name,
cfg.version,
source,
cfg.indexes,
cfg.spot_price,
)
}
#[inline(always)]
pub(crate) fn push(
&mut self,
total: <AgeRangeId as ColumnId>::Row<Sats>,
in_profit: <AgeRangeId as ColumnId>::Row<Sats>,
in_loss: <AgeRangeId as ColumnId>::Row<Sats>,
) {
self.total.push(total);
self.in_profit.push(in_profit);
self.in_loss.push(in_loss);
}
pub(crate) fn stored_vecs_mut(&mut self) -> [&mut dyn AnyStoredVec; 3] {
[&mut self.total, &mut self.in_profit, &mut self.in_loss]
}
pub(crate) fn len(&self) -> usize {
self.total
.len()
.min(self.in_profit.len())
.min(self.in_loss.len())
}
}
@@ -1,38 +1,44 @@
use brk_cohort::ByAddrType;
use brk_cohort::{AddrTypeId, ByAddrType};
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Height, Sats, StoredU64, Version};
use derive_more::{Deref, DerefMut};
use rayon::prelude::*;
use vecdb::{
AnyStoredVec, CachedBoxedVec, Database, Exit, ReadableVec, Rw, StorageMode, WritableVec,
AnyStoredVec, CachedBoxedVec, Database, Exit, ReadOnlyClone, ReadableVec, Rw, StorageMode,
WritableVec,
};
use crate::{
distribution::AllChainCache,
indexes,
internal::{LazySpotValuePerBlock, SpotValuePerBlock},
internal::{
ColumnarPerBlock, LazyColumnSpotValuePerBlock, LazySpotValuePerBlock, WithAddrTypes,
},
};
/// Average amount held per UTXO and per funded address.
///
/// `utxo = supply / utxo_count`, `addr = supply / funded_addr_count`.
#[derive(Traversable)]
pub struct AvgAmountMetrics<M: StorageMode = Rw> {
pub utxo: SpotValuePerBlock<M>,
pub addr: SpotValuePerBlock<M>,
}
#[derive(Clone, Traversable)]
pub struct LazyAvgAmountMetrics {
pub utxo: LazySpotValuePerBlock,
pub addr: LazySpotValuePerBlock,
pub struct AvgAmountMetrics<V> {
pub utxo: V,
pub addr: V,
}
#[derive(Traversable)]
#[derive(Deref, DerefMut, Traversable)]
pub struct AvgAmountVecs<M: StorageMode = Rw> {
pub all: LazyAvgAmountMetrics,
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub by_addr_type: ByAddrType<AvgAmountMetrics<M>>,
pub series: WithAddrTypes<
AvgAmountMetrics<LazyColumnSpotValuePerBlock<AddrTypeId>>,
AvgAmountMetrics<LazySpotValuePerBlock>,
>,
#[traversable(hidden)]
utxo_source: ColumnarPerBlock<Sats, AddrTypeId, (), M>,
#[traversable(hidden)]
addr_source: ColumnarPerBlock<Sats, AddrTypeId, (), M>,
}
impl AvgAmountVecs {
@@ -57,7 +63,7 @@ impl AvgAmountVecs {
funded_addr_count,
|_, count, supply| supply / count,
);
let all = LazyAvgAmountMetrics {
let all = AvgAmountMetrics {
utxo: LazySpotValuePerBlock::from_sats_source(
"avg_utxo_amount",
version,
@@ -73,92 +79,74 @@ impl AvgAmountVecs {
spot_price,
),
};
let by_addr_type = ByAddrType::new_with_name(|type_name| {
AvgAmountMetrics::forced_import(db, type_name, version, indexes, spot_price)
})?;
Ok(Self { all, by_addr_type })
let utxo_source =
ColumnarPerBlock::forced_import(db, "avg_utxo_amount_sats_by_type", version, |_| ())?;
let addr_source =
ColumnarPerBlock::forced_import(db, "avg_addr_amount_sats_by_type", version, |_| ())?;
let utxo = utxo_source.height.read_only_clone();
let addr = addr_source.height.read_only_clone();
let by_addr_type = AddrTypeId::series(|column, type_name| AvgAmountMetrics {
utxo: LazyColumnSpotValuePerBlock::new(
&format!("{type_name}_avg_utxo_amount"),
version,
&utxo,
column,
indexes,
spot_price,
),
addr: LazyColumnSpotValuePerBlock::new(
&format!("{type_name}_avg_addr_amount"),
version,
&addr,
column,
indexes,
spot_price,
),
});
Ok(Self {
series: WithAddrTypes { all, by_addr_type },
utxo_source,
addr_source,
})
}
pub(crate) fn par_iter_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
self.by_addr_type
.par_values_mut()
.flat_map_iter(AvgAmountMetrics::collect_vecs_mut)
rayon::iter::once(self.utxo_source.stored_mut())
.chain(rayon::iter::once(self.addr_source.stored_mut()))
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
for metrics in self.by_addr_type.values_mut() {
metrics.reset_height()?;
}
Ok(())
}
}
impl AvgAmountMetrics {
pub(crate) fn forced_import(
db: &Database,
prefix: &str,
version: Version,
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let name = |suffix: &str| {
if prefix.is_empty() {
suffix.to_string()
} else {
format!("{prefix}_{suffix}")
}
};
Ok(Self {
utxo: SpotValuePerBlock::forced_import(
db,
&name("avg_utxo_amount"),
version,
indexes,
spot_price,
)?,
addr: SpotValuePerBlock::forced_import(
db,
&name("avg_addr_amount"),
version,
indexes,
spot_price,
)?,
})
}
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
vec![
&mut self.utxo.sats.height as &mut dyn AnyStoredVec,
&mut self.addr.sats.height,
]
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
self.utxo.sats.height.reset()?;
self.addr.sats.height.reset()?;
self.utxo_source.height.reset()?;
self.addr_source.height.reset()?;
Ok(())
}
pub(crate) fn compute(
&mut self,
supply_sats: &impl ReadableVec<Height, Sats>,
utxo_count: &impl ReadableVec<Height, StoredU64>,
funded_addr_count: &impl ReadableVec<Height, StoredU64>,
supply_sats: &ByAddrType<&impl ReadableVec<Height, Sats>>,
utxo_count: &ByAddrType<&impl ReadableVec<Height, StoredU64>>,
funded_addr_count: &ByAddrType<&impl ReadableVec<Height, StoredU64>>,
max_from: Height,
exit: &Exit,
) -> Result<()> {
self.utxo
.sats
.height
.compute_divide(max_from, supply_sats, utxo_count, exit)?;
self.addr
.sats
.height
.compute_divide(max_from, supply_sats, funded_addr_count, exit)?;
self.utxo_source.compute_columns2(
max_from,
|column| *column.select(supply_sats),
|column| *column.select(utxo_count),
|_, supply, count| supply / count,
exit,
)?;
self.addr_source.compute_columns2(
max_from,
|column| *column.select(supply_sats),
|column| *column.select(funded_addr_count),
|_, supply, count| supply / count,
exit,
)?;
Ok(())
}
@@ -3,14 +3,15 @@ use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Height, PartsPerMillion32, PartsPerMillionSigned64, Sats, SatsSigned, Version};
use vecdb::{
AnyStoredVec, AnyVec, BinaryTransform, Exit, ReadableCloneableVec, Rw, StorageMode, WritableVec,
AnyStoredVec, AnyVec, BinaryTransform, EagerVec, Exit, ImportableVec, PcoVec, ReadOnlyClone,
ReadableCloneableVec, Rw, StorageMode, WritableVec,
};
use crate::distribution::state::{CohortState, CostBasisOps, RealizedOps};
use crate::internal::{
LazyIndexedVec, LazyPercentPerBlock, LazyRollingDeltasAmountFromHeight, RatioSats,
SpotValuePerBlock,
LazyIndexedVec, LazyPercentPerBlock, LazyRollingDeltasAmountFromHeight, LazySpotValuePerBlock,
RatioSats,
};
use crate::distribution::metrics::{AllSupplyCache, ImportConfig};
@@ -18,15 +19,49 @@ use crate::distribution::metrics::{AllSupplyCache, ImportConfig};
/// Base supply metrics: total supply + dominance (share of circulating).
#[derive(Traversable)]
pub struct SupplyBase<M: StorageMode = Rw> {
pub total: SpotValuePerBlock<M>,
pub total: LazySpotValuePerBlock,
pub delta: LazyRollingDeltasAmountFromHeight<Sats, SatsSigned, PartsPerMillionSigned64>,
#[traversable(rename = "dominance")]
pub dominance: LazyPercentPerBlock<PartsPerMillion32>,
#[traversable(hidden)]
source: Option<M::Stored<EagerVec<PcoVec<Height, Sats>>>>,
}
impl SupplyBase {
pub(crate) fn forced_import(cfg: &ImportConfig, all_supply: &AllSupplyCache) -> Result<Self> {
let supply: SpotValuePerBlock = cfg.import("supply", Version::ZERO)?;
let name = cfg.name("supply");
let sats_source =
EagerVec::forced_import(cfg.db, &format!("{name}_sats"), cfg.version)?;
let supply = LazySpotValuePerBlock::from_sats_source(
&name,
cfg.version,
sats_source.read_only_clone(),
cfg.indexes,
cfg.spot_price,
);
let name = cfg.name("supply_dominance");
let dominance_source = LazyIndexedVec::new(
&format!("{name}_ppm_source"),
cfg.version,
supply.sats.height.read_only_boxed_clone(),
all_supply.cached_boxed_clone(),
|_, supply, all_supply| RatioSats::<PartsPerMillion32>::apply(supply, all_supply),
);
let dominance = LazyPercentPerBlock::from_height_source(
&name,
cfg.version,
dominance_source,
cfg.indexes,
);
Ok(Self::new(cfg, supply, dominance, Some(sats_source)))
}
pub(crate) fn from_lazy(
cfg: &ImportConfig,
supply: LazySpotValuePerBlock,
all_supply: &AllSupplyCache,
) -> Self {
let name = cfg.name("supply_dominance");
let source = LazyIndexedVec::new(
&format!("{name}_ppm_source"),
@@ -38,11 +73,10 @@ impl SupplyBase {
let dominance =
LazyPercentPerBlock::from_height_source(&name, cfg.version, source, cfg.indexes);
Ok(Self::new(cfg, supply, dominance))
Self::new(cfg, supply, dominance, None)
}
pub(crate) fn forced_import_all(cfg: &ImportConfig) -> Result<Self> {
let supply: SpotValuePerBlock = cfg.import("supply", Version::ZERO)?;
pub(crate) fn from_lazy_all(cfg: &ImportConfig, supply: LazySpotValuePerBlock) -> Self {
let dominance = LazyPercentPerBlock::from_indexed_source(
&cfg.name("supply_dominance"),
cfg.version,
@@ -51,7 +85,7 @@ impl SupplyBase {
cfg.indexes,
);
Ok(Self::new(cfg, supply, dominance))
Self::new(cfg, supply, dominance, None)
}
fn all_dominance(_height: Height, supply: Sats) -> PartsPerMillion32 {
@@ -60,8 +94,9 @@ impl SupplyBase {
fn new(
cfg: &ImportConfig,
supply: SpotValuePerBlock,
supply: LazySpotValuePerBlock,
dominance: LazyPercentPerBlock<PartsPerMillion32>,
source: Option<EagerVec<PcoVec<Height, Sats>>>,
) -> Self {
let delta = LazyRollingDeltasAmountFromHeight::new(
&cfg.name("supply_delta"),
@@ -75,6 +110,7 @@ impl SupplyBase {
total: supply,
delta,
dominance,
source,
}
}
@@ -84,11 +120,16 @@ impl SupplyBase {
#[inline(always)]
pub(crate) fn push_state(&mut self, state: &CohortState<impl RealizedOps, impl CostBasisOps>) {
self.total.sats.height.push(state.supply.value);
if let Some(source) = self.source.as_mut() {
source.push(state.supply.value);
}
}
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
vec![&mut self.total.sats.height as &mut dyn AnyStoredVec]
self.source
.iter_mut()
.map(|source| source as &mut dyn AnyStoredVec)
.collect()
}
pub(crate) fn compute_from_stateful(
@@ -97,14 +138,16 @@ impl SupplyBase {
others: &[&Self],
exit: &Exit,
) -> Result<()> {
self.total.sats.height.compute_sum_of_others(
starting_lengths.height,
&others
.iter()
.map(|v| &v.total.sats.height)
.collect::<Vec<_>>(),
exit,
)?;
if let Some(source) = self.source.as_mut() {
source.compute_sum_of_others(
starting_lengths.height,
&others
.iter()
.map(|v| &v.total.sats.height)
.collect::<Vec<_>>(),
exit,
)?;
}
Ok(())
}
}
@@ -1,7 +1,7 @@
use brk_types::{Height, Sats};
use vecdb::{
CachedBoxedVec, CachedReadableVec, CachedVec, EagerVec, PcoVec, PcodecStrategy, ReadOnlyClone,
ReadOnlyCompressedVec,
CachedBoxedVec, CachedReadableVec, CachedVec, ReadableBoxedVec, ReadableCloneableVec,
ReadableVec, TypedVec,
};
/// Pinned in-memory snapshot of the all-cohort supply.
@@ -10,20 +10,35 @@ use vecdb::{
/// global cache budget because evicting it would make each lazy read hit disk.
#[derive(Clone)]
pub(crate) struct AllSupplyCache {
cache: CachedVec<ReadOnlyCompressedVec<Height, Sats, PcodecStrategy<Sats>>>,
cache: CachedBoxedVec<Height, Sats>,
source: ReadableBoxedVec<Height, Sats>,
}
impl AllSupplyCache {
pub(crate) fn new(source: &EagerVec<PcoVec<Height, Sats>>) -> Self {
Self {
cache: CachedVec::wrap(source.read_only_clone()),
}
pub(crate) fn new<V>(source: V) -> Self
where
V: TypedVec<I = Height, T = Sats>
+ ReadableVec<Height, Sats>
+ Clone
+ Send
+ Sync
+ 'static,
{
let cache = CachedVec::wrap(source);
let source = ReadableCloneableVec::read_only_boxed_clone(&cache);
let cache = cache.cached_boxed_clone();
Self { cache, source }
}
pub(crate) fn cached_boxed_clone(&self) -> CachedBoxedVec<Height, Sats> {
self.cache.cached_boxed_clone()
}
pub(crate) fn readable_boxed_clone(&self) -> ReadableBoxedVec<Height, Sats> {
self.source.read_only_boxed_clone()
}
pub(crate) fn clear(&self) {
self.cache.clear();
}
@@ -32,7 +47,7 @@ impl AllSupplyCache {
#[cfg(test)]
mod tests {
use brk_types::Version;
use vecdb::{AnyStoredVec, Database, ImportableVec, WritableVec};
use vecdb::{AnyStoredVec, Database, EagerVec, ImportableVec, PcoVec, ReadOnlyClone, WritableVec};
use super::*;
@@ -54,7 +69,7 @@ mod tests {
source.push(Sats::new(20));
source.write().unwrap();
let cache = AllSupplyCache::new(&source);
let cache = AllSupplyCache::new(source.read_only_clone());
let reader = cache.cached_boxed_clone();
assert_eq!(&*reader.cached(), &[Sats::new(10), Sats::new(20)]);
@@ -1,21 +1,25 @@
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::Version;
use brk_types::{Height, Sats, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{AnyStoredVec, AnyVec, Exit, Rw, StorageMode, WritableVec};
use vecdb::{
AnyStoredVec, AnyVec, EagerVec, Exit, ImportableVec, PcoVec, ReadOnlyClone, Rw, StorageMode,
WritableVec,
};
use crate::distribution::state::UnrealizedState;
use crate::internal::{
HalveCents, HalveDollars, HalveSats, HalveSatsToBitcoin, LazyValuePerBlock, SpotValuePerBlock,
HalveCents, HalveDollars, HalveSats, HalveSatsToBitcoin, LazySpotValuePerBlock,
LazyValuePerBlock,
};
use crate::distribution::metrics::{AllSupplyCache, ImportConfig};
use super::SupplyBase;
/// Core supply metrics: total + halved + in_profit/in_loss (4 stored vecs).
/// Core supply metrics: total + halved + in_profit/in_loss.
#[derive(Deref, DerefMut, Traversable)]
pub struct SupplyCore<M: StorageMode = Rw> {
#[deref]
@@ -24,22 +28,86 @@ pub struct SupplyCore<M: StorageMode = Rw> {
pub base: SupplyBase<M>,
pub half: LazyValuePerBlock,
pub in_profit: SpotValuePerBlock<M>,
pub in_loss: SpotValuePerBlock<M>,
pub in_profit: LazySpotValuePerBlock,
pub in_loss: LazySpotValuePerBlock,
#[traversable(hidden)]
in_profit_source: Option<M::Stored<EagerVec<PcoVec<Height, Sats>>>>,
#[traversable(hidden)]
in_loss_source: Option<M::Stored<EagerVec<PcoVec<Height, Sats>>>>,
}
impl SupplyCore {
pub(crate) fn forced_import(cfg: &ImportConfig, all_supply: &AllSupplyCache) -> Result<Self> {
Self::forced_import_with_base(cfg, SupplyBase::forced_import(cfg, all_supply)?)
let base = SupplyBase::forced_import(cfg, all_supply)?;
let (in_profit, in_profit_source) = Self::import_spot(cfg, "supply_in_profit")?;
let (in_loss, in_loss_source) = Self::import_spot(cfg, "supply_in_loss")?;
Ok(Self::new(
cfg,
base,
in_profit,
in_loss,
Some(in_profit_source),
Some(in_loss_source),
))
}
pub(crate) fn forced_import_all(cfg: &ImportConfig) -> Result<Self> {
Self::forced_import_with_base(cfg, SupplyBase::forced_import_all(cfg)?)
pub(crate) fn from_lazy(
cfg: &ImportConfig,
total: LazySpotValuePerBlock,
in_profit: LazySpotValuePerBlock,
in_loss: LazySpotValuePerBlock,
all_supply: &AllSupplyCache,
) -> Self {
Self::new(
cfg,
SupplyBase::from_lazy(cfg, total, all_supply),
in_profit,
in_loss,
None,
None,
)
}
fn forced_import_with_base(cfg: &ImportConfig, base: SupplyBase) -> Result<Self> {
let v0 = Version::ZERO;
pub(crate) fn from_lazy_all(
cfg: &ImportConfig,
total: LazySpotValuePerBlock,
in_profit: LazySpotValuePerBlock,
in_loss: LazySpotValuePerBlock,
) -> Self {
Self::new(
cfg,
SupplyBase::from_lazy_all(cfg, total),
in_profit,
in_loss,
None,
None,
)
}
fn import_spot(
cfg: &ImportConfig,
suffix: &str,
) -> Result<(LazySpotValuePerBlock, EagerVec<PcoVec<Height, Sats>>)> {
let name = cfg.name(suffix);
let source = EagerVec::forced_import(cfg.db, &format!("{name}_sats"), cfg.version)?;
let series = LazySpotValuePerBlock::from_sats_source(
&name,
cfg.version,
source.read_only_clone(),
cfg.indexes,
cfg.spot_price,
);
Ok((series, source))
}
fn new(
cfg: &ImportConfig,
base: SupplyBase,
in_profit: LazySpotValuePerBlock,
in_loss: LazySpotValuePerBlock,
in_profit_source: Option<EagerVec<PcoVec<Height, Sats>>>,
in_loss_source: Option<EagerVec<PcoVec<Height, Sats>>>,
) -> Self {
let half = LazyValuePerBlock::from_spot_block_source::<
HalveSats,
HalveSatsToBitcoin,
@@ -47,12 +115,14 @@ impl SupplyCore {
HalveDollars,
>(&cfg.name("supply_half"), &base.total, cfg.version);
Ok(Self {
Self {
base,
half,
in_profit: cfg.import("supply_in_profit", v0)?,
in_loss: cfg.import("supply_in_loss", v0)?,
})
in_profit,
in_loss,
in_profit_source,
in_loss_source,
}
}
pub(crate) fn min_len(&self) -> usize {
@@ -64,14 +134,26 @@ impl SupplyCore {
#[inline(always)]
pub(crate) fn push_profitability(&mut self, state: &UnrealizedState) {
self.in_profit.sats.height.push(state.supply_in_profit);
self.in_loss.sats.height.push(state.supply_in_loss);
if let Some(source) = self.in_profit_source.as_mut() {
source.push(state.supply_in_profit);
}
if let Some(source) = self.in_loss_source.as_mut() {
source.push(state.supply_in_loss);
}
}
pub(crate) fn collect_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs = self.base.collect_vecs_mut();
vecs.push(&mut self.in_profit.sats.height as &mut dyn AnyStoredVec);
vecs.push(&mut self.in_loss.sats.height);
vecs.extend(
self.in_profit_source
.iter_mut()
.map(|source| source as &mut dyn AnyStoredVec),
);
vecs.extend(
self.in_loss_source
.iter_mut()
.map(|source| source as &mut dyn AnyStoredVec),
);
vecs
}
@@ -88,8 +170,26 @@ impl SupplyCore {
let base_refs: Vec<&SupplyBase> = others.iter().map(|o| &o.base).collect();
self.base
.compute_from_stateful(starting_lengths, &base_refs, exit)?;
sum_others!(self, starting_lengths, others, exit; in_profit.sats.height);
sum_others!(self, starting_lengths, others, exit; in_loss.sats.height);
if let Some(source) = self.in_profit_source.as_mut() {
source.compute_sum_of_others(
starting_lengths.height,
&others
.iter()
.map(|other| &other.in_profit.sats.height)
.collect::<Vec<_>>(),
exit,
)?;
}
if let Some(source) = self.in_loss_source.as_mut() {
source.compute_sum_of_others(
starting_lengths.height,
&others
.iter()
.map(|other| &other.in_loss.sats.height)
.collect::<Vec<_>>(),
exit,
)?;
}
Ok(())
}
}
@@ -1,8 +1,10 @@
mod age_range;
mod avg_amount;
mod base;
mod cache;
mod core;
pub(crate) use age_range::AgeRangeSupplySources;
pub use self::core::SupplyCore;
pub use avg_amount::AvgAmountVecs;
pub use base::SupplyBase;
+25 -29
View File
@@ -1,6 +1,6 @@
use std::path::{Path, PathBuf};
use brk_cohort::{ByAddrType, EntryPrice, Filter};
use brk_cohort::{AddrTypeId, EntryPrice};
use brk_error::Result;
use brk_indexer::Indexer;
use brk_traversable::Traversable;
@@ -65,6 +65,7 @@ impl AddrMetricsVecs {
self.funded.reset_height()?;
self.empty.reset_height()?;
self.activity.reset_height()?;
self.total.reset_height()?;
self.reused.reset_height()?;
self.respent.reset_height()?;
self.exposed.reset_height()?;
@@ -91,9 +92,9 @@ impl AddrMetricsVecs {
.par_iter_height_mut()
.chain(self.empty.par_iter_height_mut())
.chain(self.activity.par_iter_height_mut())
.chain(self.reused.par_iter_height_mut())
.chain(self.respent.par_iter_height_mut())
.chain(self.exposed.par_iter_height_mut())
.chain(self.reused.par_iter_stateful_height_mut())
.chain(self.respent.par_iter_stateful_height_mut())
.chain(self.exposed.par_iter_stateful_height_mut())
}
/// All height-indexed vecs including derived (`avg_amount`). Used for
@@ -105,6 +106,7 @@ impl AddrMetricsVecs {
.par_iter_height_mut()
.chain(self.empty.par_iter_height_mut())
.chain(self.activity.par_iter_height_mut())
.chain(self.total.par_iter_height_mut())
.chain(self.reused.par_iter_height_mut())
.chain(self.respent.par_iter_height_mut())
.chain(self.exposed.par_iter_height_mut())
@@ -660,15 +662,17 @@ impl Vecs {
r2?;
}
// 6b. Compute address count sum (by addr_type -> all)
self.addrs.funded.compute_rest(&starting_lengths, exit)?;
self.addrs.empty.compute_rest(&starting_lengths, exit)?;
// 6b. Compute address metrics derived from stored per-type sources.
let t = &self.utxo_cohorts.type_;
let type_supply_sats = ByAddrType::new(|filter| {
let Filter::Type(ot) = filter else {
unreachable!()
};
&t.get(ot).metrics.supply.total.sats.height
let type_supply_sats = AddrTypeId::series(|column, _| {
&t.get(column.output_type()).metrics.supply.total.sats.height
});
let type_utxo_counts = AddrTypeId::series(|column, _| {
&t.get(column.output_type())
.metrics
.outputs
.unspent_count
.height
});
self.addrs
.reused
@@ -680,23 +684,15 @@ impl Vecs {
.exposed
.compute_rest(&starting_lengths, &type_supply_sats, exit)?;
// Average amount (supply / utxo_count, supply / funded_addr_count) for `all` and per addr type.
for ((ot, avg), (_, funded)) in self
.addrs
.avg_amount
.by_addr_type
.iter_mut()
.zip(self.addrs.funded.by_addr_type.iter())
{
let type_m = &t.get(ot).metrics;
avg.compute(
&type_m.supply.total.sats.height,
&type_m.outputs.unspent_count.height,
&funded.height,
starting_lengths.height,
exit,
)?;
}
let type_funded_addr_counts =
AddrTypeId::series(|column, _| &column.select(&self.addrs.funded.by_addr_type).height);
self.addrs.avg_amount.compute(
&type_supply_sats,
&type_utxo_counts,
&type_funded_addr_counts,
starting_lengths.height,
exit,
)?;
// 6c. Compute total_addr_count = addr_count + empty_addr_count
self.addrs.total.compute(
@@ -1,18 +1,18 @@
use brk_error::Result;
use brk_indexer::{Indexer, Lengths};
use brk_types::{Bitcoin, Cents, Height, Sats, StoredF64, Timestamp, Version};
use vecdb::{AnyStoredVec, Exit, ReadableVec, WritableVec};
use vecdb::{AnyStoredVec, ColumnId, Exit, ReadableVec, WritableVec};
use brk_cohort::{AGE_RANGE_FILTERS, ByTerm, TERM_FILTERS};
use brk_cohort::{AgeRange, AgeRangeId, ByTerm, TERM_FILTERS};
use super::super::cointime;
use super::{
AGE_COHORT_COUNT, AgeBand, AggregateVecs, CohortVecs, HORIZON_COUNT, Horizons,
MINIMUM_DURATION_DAYS, Vecs, age_bounds_days, horizon_mobility, mobility,
AGE_COHORT_COUNT, AgeBand, AggregateVecs, Horizons, MINIMUM_DURATION_DAYS, Vecs,
age_bounds_days, horizon_mobility, mobility,
};
use crate::{
distribution,
frameworks::{WeightedCohortContribution, WeightedCohortState, WeightedRatio, realized_price},
frameworks::{WeightedCohortContribution, WeightedCohortState, WeightedRatio},
indexes,
internal::db_utils::validate_any_computed_version_or_reset,
price,
@@ -50,8 +50,8 @@ impl AggregateState {
loss_supply: Sats,
total_cap: Cents,
mobility: StoredF64,
horizon_mobilities: &Horizons<[f64; AGE_COHORT_COUNT]>,
age: usize,
horizon_mobilities: &Horizons<AgeRange<f64>>,
age: AgeRangeId,
) -> WeightedCohortContribution {
let contribution = self
.weighted
@@ -63,7 +63,7 @@ impl AggregateState {
.iter_mut()
.zip(horizon_mobilities.iter())
{
ratio.add(loss, total, weights[age]);
ratio.add(loss, total, *age.select(weights));
}
contribution
}
@@ -92,42 +92,46 @@ impl Vecs {
exit: &Exit,
) -> Result<()> {
let starting_lengths = indexer.safe_lengths();
let source_cohorts: Vec<_> = distribution.utxo_cohorts.age_range.iter().collect();
let transfer_volumes: Vec<_> = source_cohorts
.iter()
.map(|cohort| {
&cohort
.metrics
.activity
.transfer_volume
.cumulative
.sats
.height
})
.collect();
let supplies: Vec<_> = source_cohorts
.iter()
.map(|cohort| &cohort.metrics.supply.total.sats.height)
.collect();
let loss_supplies: Vec<_> = source_cohorts
.iter()
.map(|cohort| &cohort.metrics.supply.in_loss.sats.height)
.collect();
let realized_caps: Vec<_> = source_cohorts
.iter()
.map(|cohort| &cohort.metrics.realized.cap.cents.height)
.collect();
let coindays_created: Vec<_> = cointime
.age_range
.iter()
.map(|cohort| &cohort.coindays_created.cumulative.height)
.collect();
let transfer_volumes = AgeRange::from_fn(|id| {
&id.select(&distribution.utxo_cohorts.age_range)
.metrics
.activity
.transfer_volume
.cumulative
.sats
.height
});
let supplies = AgeRange::from_fn(|id| {
&id.select(&distribution.utxo_cohorts.age_range)
.metrics
.supply
.total
.sats
.height
});
let loss_supplies = AgeRange::from_fn(|id| {
&id.select(&distribution.utxo_cohorts.age_range)
.metrics
.supply
.in_loss
.sats
.height
});
let realized_caps = AgeRange::from_fn(|id| {
&id.select(&distribution.utxo_cohorts.age_range)
.metrics
.realized
.cap
.cents
.height
});
let coindays_created = &cointime.age_range.coindays_created.cumulative;
self.compute_primary(
&starting_lengths,
&indexes.timestamp.monotonic,
&transfer_volumes,
&coindays_created,
coindays_created,
&supplies,
&loss_supplies,
&realized_caps,
@@ -142,28 +146,22 @@ impl Vecs {
&mut self,
starting_lengths: &Lengths,
timestamps: &T,
transfer_volumes: &[&S],
coindays_created: &[&D],
supplies: &[&S],
loss_supplies: &[&S],
realized_caps: &[&C],
transfer_volumes: &AgeRange<&S>,
coindays_created: &D,
supplies: &AgeRange<&S>,
loss_supplies: &AgeRange<&S>,
realized_caps: &AgeRange<&C>,
exit: &Exit,
) -> Result<Height>
where
T: ReadableVec<Height, Timestamp>,
D: ReadableVec<Height, StoredF64>,
D: ReadableVec<Height, [StoredF64; AGE_COHORT_COUNT]>,
S: ReadableVec<Height, Sats>,
C: ReadableVec<Height, Cents>,
{
debug_assert_eq!(transfer_volumes.len(), AGE_COHORT_COUNT);
debug_assert_eq!(coindays_created.len(), AGE_COHORT_COUNT);
debug_assert_eq!(supplies.len(), AGE_COHORT_COUNT);
debug_assert_eq!(loss_supplies.len(), AGE_COHORT_COUNT);
debug_assert_eq!(realized_caps.len(), AGE_COHORT_COUNT);
let source_version: Version = std::iter::once(timestamps.version())
.chain(transfer_volumes.iter().map(|vec| vec.version()))
.chain(coindays_created.iter().map(|vec| vec.version()))
.chain(std::iter::once(coindays_created.version()))
.chain(supplies.iter().map(|vec| vec.version()))
.chain(loss_supplies.iter().map(|vec| vec.version()))
.chain(realized_caps.iter().map(|vec| vec.version()))
@@ -175,7 +173,6 @@ impl Vecs {
let start = self
.primary_vecs_mut()
.into_iter()
.map(|vec| vec.len())
.min()
.unwrap_or_default()
@@ -188,7 +185,7 @@ impl Vecs {
let source_end = transfer_volumes
.iter()
.map(|vec| vec.len())
.chain(coindays_created.iter().map(|vec| vec.len()))
.chain(std::iter::once(coindays_created.len()))
.chain(supplies.iter().map(|vec| vec.len()))
.chain(loss_supplies.iter().map(|vec| vec.len()))
.chain(realized_caps.iter().map(|vec| vec.len()))
@@ -204,61 +201,63 @@ impl Vecs {
.collect_one(Height::ZERO)
.unwrap_or(Timestamp::ZERO);
let bounds = age_bounds_days();
let mut age_filters = AGE_RANGE_FILTERS.iter();
let is_sth: [bool; AGE_COHORT_COUNT] =
std::array::from_fn(|_| TERM_FILTERS.short.includes(age_filters.next().unwrap()));
let mut chunk_start = start;
while chunk_start < source_end {
let chunk_end = (chunk_start + WRITE_INTERVAL).min(source_end);
let timestamp_batch = timestamps.collect_range_at(chunk_start, chunk_end);
let transfer_batches: Vec<_> = transfer_volumes
.iter()
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
.collect();
let coinday_batches: Vec<_> = coindays_created
.iter()
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
.collect();
let supply_batches: Vec<_> = supplies
.iter()
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
.collect();
let loss_supply_batches: Vec<_> = loss_supplies
.iter()
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
.collect();
let cap_batches: Vec<_> = realized_caps
.iter()
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
.collect();
let transfer_batches = AgeRange::from_fn(|id| {
id.select(transfer_volumes)
.collect_range_at(chunk_start, chunk_end)
});
let coinday_batch = coindays_created.collect_range_at(chunk_start, chunk_end);
let supply_batches = AgeRange::from_fn(|id| {
id.select(supplies).collect_range_at(chunk_start, chunk_end)
});
let loss_supply_batches = AgeRange::from_fn(|id| {
id.select(loss_supplies)
.collect_range_at(chunk_start, chunk_end)
});
let cap_batches = AgeRange::from_fn(|id| {
id.select(realized_caps)
.collect_range_at(chunk_start, chunk_end)
});
for offset in 0..(chunk_end - chunk_start) {
let hazards = std::array::from_fn(|index| {
let hazards = AgeRange::from_fn(|id| {
spending_rate(
transfer_batches[index][offset],
coinday_batches[index][offset],
id.select(&transfer_batches)[offset],
*id.get(&coinday_batch[offset]),
)
});
let network_age = timestamp_batch[offset]
.difference_in_days_between_float(genesis_timestamp)
.max(MINIMUM_DURATION_DAYS);
let exposures = spending_exposures(&hazards, network_age, &bounds);
let mobilities = exposures.map(mobility);
let horizon_mobilities: Horizons<[f64; AGE_COHORT_COUNT]> =
let exposures = DecayFit::exposures(&hazards, network_age, &bounds);
let mobilities = AgeRange::from_fn(|id| mobility(*id.select(&exposures)));
let horizon_mobilities: Horizons<AgeRange<f64>> =
Horizons::from_fn(|_, horizon| {
std::array::from_fn(|age| horizon_mobility(&hazards, age, horizon, &bounds))
AgeRange::from_fn(|age| horizon_mobility(&hazards, age, horizon, &bounds))
});
self.age_range.spending_rate.push(AgeRangeId::from_fn(|id| {
StoredF64::from(*id.select(&hazards))
}));
self.age_range
.spending_exposure
.push(AgeRangeId::from_fn(|id| {
StoredF64::from(*id.select(&exposures))
}));
let mut terms = ByTerm::<AggregateState>::default();
let mut mobile_supply = AgeRange::default();
let mut immobile_supply = AgeRange::default();
for (index, cohort) in self.age_range.iter_mut().enumerate() {
let mobility = StoredF64::from(mobilities[index]);
let total_supply = supply_batches[index][offset];
let total_cap = cap_batches[index][offset];
let loss_supply = loss_supply_batches[index][offset];
for &id in AgeRangeId::ALL {
let mobility = StoredF64::from(*id.select(&mobilities));
let total_supply = id.select(&supply_batches)[offset];
let total_cap = id.select(&cap_batches)[offset];
let loss_supply = id.select(&loss_supply_batches)[offset];
let term = if is_sth[index] {
let term = if TERM_FILTERS.short.includes(id.filter()) {
&mut terms.short
} else {
&mut terms.long
@@ -269,31 +268,22 @@ impl Vecs {
total_cap,
mobility,
&horizon_mobilities,
index,
id,
);
cohort
.spending_rate
.height
.push(StoredF64::from(hazards[index]));
cohort
.spending_exposure
.height
.push(StoredF64::from(exposures[index]));
cohort
.supply
.mobile
.sats
.height
.push(contribution.weighted_supply);
cohort
.supply
.immobile
.sats
.height
.push(contribution.complement_supply);
*id.select_mut(&mut mobile_supply) = contribution.weighted_supply;
*id.select_mut(&mut immobile_supply) = contribution.complement_supply;
}
self.age_range
.supply
.mobile
.push(AgeRangeId::from_fn(|id| *id.select(&mobile_supply)));
self.age_range
.supply
.immobile
.push(AgeRangeId::from_fn(|id| *id.select(&immobile_supply)));
self.all.push(terms.short.merged(terms.long));
self.sth.push(terms.short);
self.lth.push(terms.long);
@@ -311,17 +301,17 @@ impl Vecs {
Ok(Height::from(start))
}
fn primary_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs = Vec::with_capacity(AGE_COHORT_COUNT * 4 + 3 * (5 + HORIZON_COUNT));
for cohort in self.age_range.iter_mut() {
vecs.extend(cohort.primary_vecs_mut());
}
vecs.extend(self.all.primary_vecs_mut());
vecs.extend(self.sth.primary_vecs_mut());
vecs.extend(self.lth.primary_vecs_mut());
vecs
fn primary_vecs_mut(&mut self) -> impl Iterator<Item = &mut dyn AnyStoredVec> {
[
self.age_range.spending_rate.stored_mut(),
self.age_range.spending_exposure.stored_mut(),
self.age_range.supply.mobile.stored_mut(),
self.age_range.supply.immobile.stored_mut(),
]
.into_iter()
.chain(self.all.primary_vecs_mut())
.chain(self.sth.primary_vecs_mut())
.chain(self.lth.primary_vecs_mut())
}
}
@@ -348,38 +338,26 @@ impl AggregateVecs {
output.supply_in_loss_share.height.push(ratio.value());
}
self.cap.cents.height.push(state.weighted.weighted_cap);
self.price.cents.height.push(realized_price(
state.weighted.weighted_cap,
state.weighted.weighted_supply,
));
self.price
.cents
.height
.push(state.weighted.realized_price());
}
fn primary_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs = Vec::with_capacity(5 + HORIZON_COUNT);
vecs.extend([
fn primary_vecs_mut(&mut self) -> impl Iterator<Item = &mut dyn AnyStoredVec> {
[
&mut self.supply.mobile.sats.height as &mut dyn AnyStoredVec,
&mut self.supply.immobile.sats.height,
&mut self.supply_in_loss_share.height,
&mut self.cap.cents.height,
&mut self.price.cents.height,
]);
vecs.extend(
]
.into_iter()
.chain(
self.horizon
.iter_mut()
.map(|horizon| &mut horizon.supply_in_loss_share.height as &mut dyn AnyStoredVec),
);
vecs
}
}
impl CohortVecs {
fn primary_vecs_mut(&mut self) -> [&mut dyn AnyStoredVec; 4] {
[
&mut self.spending_rate.height,
&mut self.spending_exposure.height,
&mut self.supply.mobile.sats.height,
&mut self.supply.immobile.sats.height,
]
)
}
}
@@ -393,135 +371,135 @@ fn spending_rate(transfer_volume: Sats, coindays_created: StoredF64) -> f64 {
}
}
fn fit_decay(
hazards: &[f64; AGE_COHORT_COUNT],
network_age: f64,
bounds: &[AgeBand; AGE_COHORT_COUNT],
) -> Option<DecayFit> {
let mut total_duration = 0.0;
let mut weighted_age = 0.0;
let mut weighted_log_hazard = 0.0;
let mut anchor = None;
impl DecayFit {
fn fit(hazards: &AgeRange<f64>, network_age: f64, bounds: &AgeRange<AgeBand>) -> Option<Self> {
let mut total_duration = 0.0;
let mut weighted_age = 0.0;
let mut weighted_log_hazard = 0.0;
let mut anchor = None;
for (index, band) in bounds[..AGE_COHORT_COUNT - 1].iter().enumerate() {
let hazard = hazards[index];
if band.upper > network_age || !hazard.is_finite() || hazard <= 0.0 {
continue;
for &id in &AgeRangeId::ALL[..AGE_COHORT_COUNT - 1] {
let band = *id.select(bounds);
let hazard = *id.select(hazards);
if band.upper > network_age || !hazard.is_finite() || hazard <= 0.0 {
continue;
}
let age = (band.lower + band.upper) / 2.0;
let duration = band.upper - band.lower;
let log_hazard = hazard.ln();
total_duration += duration;
weighted_age += duration * age;
weighted_log_hazard += duration * log_hazard;
anchor = Some((band.upper, hazard));
}
let age = (band.lower + band.upper) / 2.0;
let duration = band.upper - band.lower;
let log_hazard = hazard.ln();
total_duration += duration;
weighted_age += duration * age;
weighted_log_hazard += duration * log_hazard;
anchor = Some((band.upper, hazard));
}
if total_duration <= 0.0 {
return None;
}
let mean_age = weighted_age / total_duration;
let mean_log_hazard = weighted_log_hazard / total_duration;
let mut covariance = 0.0;
let mut age_variance = 0.0;
for (index, band) in bounds[..AGE_COHORT_COUNT - 1].iter().enumerate() {
let hazard = hazards[index];
if band.upper > network_age || !hazard.is_finite() || hazard <= 0.0 {
continue;
if total_duration <= 0.0 {
return None;
}
let age = (band.lower + band.upper) / 2.0;
let duration = band.upper - band.lower;
let log_hazard = hazard.ln();
let age_offset = age - mean_age;
covariance += duration * age_offset * (log_hazard - mean_log_hazard);
age_variance += duration * age_offset.powi(2);
}
let mean_age = weighted_age / total_duration;
let mean_log_hazard = weighted_log_hazard / total_duration;
let mut covariance = 0.0;
let mut age_variance = 0.0;
if age_variance <= f64::EPSILON {
return None;
}
for &id in &AgeRangeId::ALL[..AGE_COHORT_COUNT - 1] {
let band = *id.select(bounds);
let hazard = *id.select(hazards);
if band.upper > network_age || !hazard.is_finite() || hazard <= 0.0 {
continue;
}
let slope = covariance / age_variance;
if slope >= 0.0 {
return None;
}
let tau = -1.0 / slope;
if !tau.is_finite() || tau <= 0.0 {
return None;
}
let (anchor_age, anchor_hazard) = anchor?;
Some(DecayFit {
slope,
tau,
anchor_age,
anchor_hazard,
})
}
fn spending_exposures(
hazards: &[f64; AGE_COHORT_COUNT],
network_age: f64,
bounds: &[AgeBand; AGE_COHORT_COUNT],
) -> [f64; AGE_COHORT_COUNT] {
let Some(fit) = fit_decay(hazards, network_age, bounds) else {
return [0.0; AGE_COHORT_COUNT];
};
std::array::from_fn(|start_band| {
spending_exposure(hazards, start_band, network_age, bounds, fit)
})
}
fn spending_exposure(
hazards: &[f64; AGE_COHORT_COUNT],
start_band: usize,
network_age: f64,
bounds: &[AgeBand; AGE_COHORT_COUNT],
fit: DecayFit,
) -> f64 {
let start = bounds[start_band];
let occupied_upper = if start.upper.is_finite() {
start.upper.min(network_age.max(start.lower))
} else {
start.lower
};
let mut age = if start.upper.is_finite() {
(start.lower + occupied_upper) / 2.0
} else {
start.lower
};
let mut exposure = 0.0;
for band_index in start_band..AGE_COHORT_COUNT - 1 {
let band = bounds[band_index];
let duration = (band.upper - age.max(band.lower)).max(MINIMUM_DURATION_DAYS);
let hazard = hazards[band_index];
let observed = band.upper <= network_age && hazard.is_finite() && hazard > 0.0;
if !observed {
break;
let age = (band.lower + band.upper) / 2.0;
let duration = band.upper - band.lower;
let log_hazard = hazard.ln();
let age_offset = age - mean_age;
covariance += duration * age_offset * (log_hazard - mean_log_hazard);
age_variance += duration * age_offset.powi(2);
}
exposure += hazard * duration;
age = band.upper;
if age_variance <= f64::EPSILON {
return None;
}
let slope = covariance / age_variance;
if slope >= 0.0 {
return None;
}
let tau = -1.0 / slope;
if !tau.is_finite() || tau <= 0.0 {
return None;
}
let (anchor_age, anchor_hazard) = anchor?;
Some(Self {
slope,
tau,
anchor_age,
anchor_hazard,
})
}
let tail = bounds[AGE_COHORT_COUNT - 1];
let tail_hazard = hazards[AGE_COHORT_COUNT - 1];
let observed_tail = network_age > tail.lower && tail_hazard.is_finite() && tail_hazard > 0.0;
let (anchor_age, anchor_hazard) = if observed_tail {
(tail.lower, tail_hazard)
} else {
(fit.anchor_age, fit.anchor_hazard)
};
let continuation_age = age.max(anchor_age);
let continuation_hazard = anchor_hazard * (fit.slope * (continuation_age - anchor_age)).exp();
exposure + (continuation_hazard * fit.tau).max(0.0)
fn exposures(
hazards: &AgeRange<f64>,
network_age: f64,
bounds: &AgeRange<AgeBand>,
) -> AgeRange<f64> {
let Some(fit) = Self::fit(hazards, network_age, bounds) else {
return AgeRange::default();
};
AgeRange::from_fn(|start_band| fit.exposure(hazards, start_band, network_age, bounds))
}
fn exposure(
self,
hazards: &AgeRange<f64>,
start_band: AgeRangeId,
network_age: f64,
bounds: &AgeRange<AgeBand>,
) -> f64 {
let start = *start_band.select(bounds);
let occupied_upper = if start.upper.is_finite() {
start.upper.min(network_age.max(start.lower))
} else {
start.lower
};
let mut age = if start.upper.is_finite() {
(start.lower + occupied_upper) / 2.0
} else {
start.lower
};
let mut exposure = 0.0;
for &band_id in &AgeRangeId::ALL[start_band.index()..AGE_COHORT_COUNT - 1] {
let band = *band_id.select(bounds);
let duration = (band.upper - age.max(band.lower)).max(MINIMUM_DURATION_DAYS);
let hazard = *band_id.select(hazards);
let observed = band.upper <= network_age && hazard.is_finite() && hazard > 0.0;
if !observed {
break;
}
exposure += hazard * duration;
age = band.upper;
}
let tail = bounds.over_15y;
let tail_hazard = hazards.over_15y;
let observed_tail =
network_age > tail.lower && tail_hazard.is_finite() && tail_hazard > 0.0;
let (anchor_age, anchor_hazard) = if observed_tail {
(tail.lower, tail_hazard)
} else {
(self.anchor_age, self.anchor_hazard)
};
let continuation_age = age.max(anchor_age);
let continuation_hazard =
anchor_hazard * (self.slope * (continuation_age - anchor_age)).exp();
exposure + (continuation_hazard * self.tau).max(0.0)
}
}
#[cfg(test)]
@@ -541,8 +519,8 @@ mod tests {
#[test]
fn fixed_horizon_compounds_hazards_across_age_ranges() {
let bounds = age_bounds_days();
let hazards = [0.01; AGE_COHORT_COUNT];
let probability = horizon_mobility(&hazards, 2, 30.0, &bounds);
let hazards = AgeRange::from_fn(|_| 0.01);
let probability = horizon_mobility(&hazards, AgeRangeId::From1DTo1W, 30.0, &bounds);
assert!((probability - mobility(0.3)).abs() < 1e-12);
}
@@ -551,8 +529,8 @@ mod tests {
fn decay_fit_recovers_an_exponential_lifetime() {
let bounds = age_bounds_days();
let expected_tau = 1_000.0;
let hazards = std::array::from_fn(|index| {
let band = bounds[index];
let hazards = AgeRange::from_fn(|id| {
let band = *id.select(&bounds);
let age = if band.upper.is_finite() {
(band.lower + band.upper) / 2.0
} else {
@@ -561,7 +539,7 @@ mod tests {
(-age / expected_tau).exp()
});
let fit = fit_decay(&hazards, 20.0 * 365.0, &bounds).unwrap();
let fit = DecayFit::fit(&hazards, 20.0 * 365.0, &bounds).unwrap();
assert!((fit.tau - expected_tau).abs() < 1e-9);
}
@@ -569,8 +547,8 @@ mod tests {
#[test]
fn oldest_cohort_exposure_is_its_observed_tail_lifetime() {
let bounds = age_bounds_days();
let hazards = std::array::from_fn(|index| {
let band = bounds[index];
let hazards = AgeRange::from_fn(|id| {
let band = *id.select(&bounds);
let age = if band.upper.is_finite() {
(band.lower + band.upper) / 2.0
} else {
@@ -579,13 +557,10 @@ mod tests {
(-age / 1_000.0).exp()
});
let network_age = 20.0 * 365.0;
let fit = fit_decay(&hazards, network_age, &bounds).unwrap();
let exposures = spending_exposures(&hazards, network_age, &bounds);
let fit = DecayFit::fit(&hazards, network_age, &bounds).unwrap();
let exposures = DecayFit::exposures(&hazards, network_age, &bounds);
assert!(
(exposures[AGE_COHORT_COUNT - 1] - hazards[AGE_COHORT_COUNT - 1] * fit.tau).abs()
< 1e-12
);
assert!((exposures.over_15y - hazards.over_15y * fit.tau).abs() < 1e-12);
}
#[test]
@@ -605,7 +580,7 @@ mod tests {
#[test]
fn term_aggregates_merge_into_all() {
let horizons = Horizons::from_fn(|_, _| [0.25; AGE_COHORT_COUNT]);
let horizons = Horizons::from_fn(|_, _| AgeRange::from_fn(|_| 0.25));
let mut direct = AggregateState::default();
direct.add(
Sats::from(100_u64),
@@ -613,7 +588,7 @@ mod tests {
Cents::from(1_000_u64),
StoredF64::from(0.3),
&horizons,
0,
AgeRangeId::Under1H,
);
direct.add(
Sats::from(200_u64),
@@ -621,7 +596,7 @@ mod tests {
Cents::from(3_000_u64),
StoredF64::from(0.4),
&horizons,
1,
AgeRangeId::From1HTo1D,
);
let mut sth = AggregateState::default();
@@ -631,7 +606,7 @@ mod tests {
Cents::from(1_000_u64),
StoredF64::from(0.3),
&horizons,
0,
AgeRangeId::Under1H,
);
let mut lth = AggregateState::default();
lth.add(
@@ -640,7 +615,7 @@ mod tests {
Cents::from(3_000_u64),
StoredF64::from(0.4),
&horizons,
1,
AgeRangeId::From1HTo1D,
);
let merged = sth.merged(lth);
@@ -668,10 +643,10 @@ mod tests {
#[test]
fn age_ranges_belong_to_exactly_one_term() {
for filter in AGE_RANGE_FILTERS.iter() {
for id in AgeRangeId::ALL {
assert_ne!(
TERM_FILTERS.short.includes(filter),
TERM_FILTERS.long.includes(filter)
TERM_FILTERS.short.includes(id.filter()),
TERM_FILTERS.long.includes(id.filter())
);
}
}
@@ -1,15 +1,23 @@
use brk_cohort::{AgeRange, CohortContext, TERM_NAMES};
use brk_cohort::{AgeRangeId, CohortContext, TERM_NAMES};
use brk_error::Result;
use brk_types::{Cents, Height, StoredF64, Version};
use vecdb::{CachedBoxedVec, Database, ReadableCloneableVec, UnaryTransform};
use super::{AggregateVecs, CohortVecs, HorizonVecs, Horizons, Split, Vecs, mobility};
use crate::{
indexes,
internal::{FiatPerBlock, LazyPerBlock, PerBlock, PriceWithRatioPerBlock, SpotValuePerBlock},
use vecdb::{
CachedBoxedVec, Database, PcoVec, ReadOnlyColumnarVec, ReadableCloneableVec, UnaryTransform,
};
const VERSION: Version = Version::TWO;
use super::{
AgeRangeVecs, AggregateVecs, HorizonVecs, Horizons, SpendingExposureSeries, Split, Vecs,
mobility,
};
use crate::{
indexes,
internal::{
ColumnarPerBlock, FiatPerBlock, LazyColumnPerBlock, LazyColumnSpotValuePerBlock,
LazyPerBlock, PerBlock, PriceWithRatioPerBlock, SpotValuePerBlock,
},
};
const VERSION: Version = Version::new(4);
struct ExposureToMobility;
@@ -20,46 +28,35 @@ impl UnaryTransform<StoredF64, StoredF64> for ExposureToMobility {
}
}
fn import_split<T>(mut import: impl FnMut(&str) -> Result<T>) -> Result<Split<T>> {
Ok(Split {
mobile: import("mobile")?,
immobile: import("immobile")?,
})
}
impl CohortVecs {
fn forced_import(
db: &vecdb::Database,
name: &str,
impl SpendingExposureSeries {
fn new(
version: Version,
source: &ReadOnlyColumnarVec<PcoVec<Height, StoredF64>, AgeRangeId>,
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let spending_rate =
PerBlock::forced_import(db, &format!("{name}_spending_rate"), version, indexes)?;
let spending_exposure =
PerBlock::forced_import(db, &format!("{name}_spending_exposure"), version, indexes)?;
let mobility = LazyPerBlock::from_computed::<ExposureToMobility>(
&format!("{name}_mobility"),
version,
spending_exposure.height.read_only_boxed_clone(),
&spending_exposure,
);
) -> Self {
let age_range = AgeRangeId::series(CohortContext::Utxo, |column, name| {
LazyColumnPerBlock::new(
&format!("{name}_spending_exposure"),
version,
source,
column,
indexes,
)
});
let mobility = AgeRangeId::series(CohortContext::Utxo, |column, name| {
let exposure = column.select(&age_range);
LazyPerBlock::from_resolutions::<ExposureToMobility>(
&format!("{name}_mobility"),
version,
exposure.height.read_only_boxed_clone(),
&exposure.resolutions,
)
});
Ok(Self {
spending_rate,
spending_exposure,
Self {
age_range,
mobility,
supply: import_split(|side| {
SpotValuePerBlock::forced_import(
db,
&format!("{name}_{side}_supply"),
version,
indexes,
spot_price,
)
})?,
})
}
}
}
@@ -78,7 +75,7 @@ impl AggregateVecs {
};
Ok(Self {
supply: import_split(|side| {
supply: Split::try_from_fn(|side| {
SpotValuePerBlock::forced_import(
db,
&format!("{prefix}{side}_supply"),
@@ -130,12 +127,54 @@ impl Vecs {
let version = parent_version + VERSION;
let aggregate_version = version + Version::ONE;
let spot_price = prices.spot.cents.height.read_only_cached_boxed_clone();
let spending_rate = ColumnarPerBlock::forced_import(
db,
&CohortContext::Utxo.prefixed("age_range_spending_rate"),
version,
|source| {
AgeRangeId::series(CohortContext::Utxo, |column, name| {
LazyColumnPerBlock::new(
&format!("{name}_spending_rate"),
version,
source,
column,
indexes,
)
})
},
)?;
let spending_exposure = ColumnarPerBlock::forced_import(
db,
&CohortContext::Utxo.prefixed("age_range_spending_exposure"),
version,
|source| SpendingExposureSeries::new(version, source, indexes),
)?;
let supply = Split::try_from_fn(|side| {
ColumnarPerBlock::forced_import(
db,
&CohortContext::Utxo.prefixed(&format!("age_range_{side}_supply_sats")),
version,
|source| {
AgeRangeId::series(CohortContext::Utxo, |column, name| {
LazyColumnSpotValuePerBlock::new(
&format!("{name}_{side}_supply"),
version,
source,
column,
indexes,
&spot_price,
)
})
},
)
})?;
let this = Self {
age_range: AgeRange::try_new(|_, name| {
let name = CohortContext::Utxo.prefixed(name);
CohortVecs::forced_import(db, &name, version, indexes, &spot_price)
})?,
age_range: AgeRangeVecs {
spending_rate,
spending_exposure,
supply,
},
all: AggregateVecs::forced_import(db, "", version, indexes, &spot_price)?,
sth: AggregateVecs::forced_import(
db,
@@ -2,32 +2,34 @@ mod compute;
mod import;
mod vecs;
use brk_cohort::AGE_RANGE_BOUNDS;
use brk_cohort::{AgeRange, AgeRangeId};
use vecdb::ColumnId;
pub(crate) use brk_cohort::AGE_RANGE_COUNT as AGE_COHORT_COUNT;
pub(crate) use vecs::HORIZON_DAYS;
pub use vecs::{AggregateVecs, CohortVecs, HorizonVecs, Horizons, Split, Vecs};
pub use vecs::{
AgeRangeVecs, AggregateVecs, HorizonVecs, Horizons, SpendingExposureSeries, Split, Vecs,
};
pub(crate) const HORIZON_COUNT: usize = 7;
pub(crate) const HOURS_PER_DAY: f64 = 24.0;
pub(crate) const MINIMUM_DURATION_DAYS: f64 = 1.0 / HOURS_PER_DAY;
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Debug)]
pub(crate) struct AgeBand {
pub lower: f64,
pub upper: f64,
}
pub(crate) fn age_bounds_days() -> [AgeBand; AGE_COHORT_COUNT] {
let mut bounds = AGE_RANGE_BOUNDS.iter();
std::array::from_fn(|index| {
let bound = bounds.next().unwrap();
pub(crate) fn age_bounds_days() -> AgeRange<AgeBand> {
AgeRange::from_fn(|id| {
let bound = id.bounds();
AgeBand {
lower: bound.start as f64 / HOURS_PER_DAY,
upper: if index + 1 < AGE_COHORT_COUNT {
bound.end as f64 / HOURS_PER_DAY
} else {
upper: if id == AgeRangeId::Over15Y {
f64::INFINITY
} else {
bound.end as f64 / HOURS_PER_DAY
},
}
})
@@ -43,34 +45,34 @@ pub(crate) fn mobility(exposure: f64) -> f64 {
}
pub(crate) fn horizon_mobility(
hazards: &[f64; AGE_COHORT_COUNT],
start_band: usize,
hazards: &AgeRange<f64>,
start_band: AgeRangeId,
horizon: f64,
bounds: &[AgeBand; AGE_COHORT_COUNT],
bounds: &AgeRange<AgeBand>,
) -> f64 {
let start = bounds[start_band];
let start = *start_band.select(bounds);
let mut age = if start.upper.is_finite() {
(start.lower + start.upper) / 2.0
} else {
start.lower
};
let mut remaining = horizon;
let mut band = start_band;
let mut exposure = 0.0;
while remaining > 0.0 && band < AGE_COHORT_COUNT {
let upper = bounds[band].upper;
for &band in &AgeRangeId::ALL[start_band.index()..] {
if remaining <= 0.0 {
break;
}
let bound = *band.select(bounds);
let upper = bound.upper;
let duration = if upper.is_finite() {
remaining.min((upper - age).max(MINIMUM_DURATION_DAYS))
} else {
remaining
};
exposure += hazards[band].max(0.0) * duration;
exposure += band.select(hazards).max(0.0) * duration;
remaining -= duration;
band += 1;
if band < AGE_COHORT_COUNT {
age = bounds[band].lower;
}
age = upper;
}
mobility(exposure)
@@ -1,10 +1,12 @@
use brk_cohort::AgeRange;
use brk_cohort::{AgeRange, AgeRangeId};
use brk_traversable::Traversable;
use brk_types::{Cents, StoredF64};
use brk_types::{Cents, Sats, StoredF64};
use derive_more::{Deref, DerefMut};
use vecdb::{Rw, StorageMode};
use crate::internal::{
FiatPerBlock, LazyPerBlock, PerBlock, PriceWithRatioPerBlock, SpotValuePerBlock,
ColumnarPerBlock, FiatPerBlock, LazyColumnPerBlock, LazyColumnSpotValuePerBlock, LazyPerBlock,
PerBlock, PriceWithRatioPerBlock, SpotValuePerBlock,
};
#[derive(Clone, Copy, Traversable)]
@@ -108,12 +110,36 @@ pub struct Split<T> {
pub immobile: T,
}
impl<T> Split<T> {
pub(crate) fn try_from_fn<E>(mut create: impl FnMut(&str) -> Result<T, E>) -> Result<Self, E> {
Ok(Self {
mobile: create("mobile")?,
immobile: create("immobile")?,
})
}
}
#[derive(Clone, Deref, DerefMut, Traversable)]
pub struct SpendingExposureSeries {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub age_range: AgeRange<LazyColumnPerBlock<StoredF64, AgeRangeId>>,
pub mobility: AgeRange<LazyPerBlock<StoredF64>>,
}
#[derive(Traversable)]
pub struct CohortVecs<M: StorageMode = Rw> {
pub spending_rate: PerBlock<StoredF64, M>,
pub spending_exposure: PerBlock<StoredF64, M>,
pub mobility: LazyPerBlock<StoredF64>,
pub supply: Split<SpotValuePerBlock<M>>,
pub struct AgeRangeVecs<M: StorageMode = Rw> {
pub spending_rate: ColumnarPerBlock<
StoredF64,
AgeRangeId,
AgeRange<LazyColumnPerBlock<StoredF64, AgeRangeId>>,
M,
>,
pub spending_exposure: ColumnarPerBlock<StoredF64, AgeRangeId, SpendingExposureSeries, M>,
pub supply: Split<
ColumnarPerBlock<Sats, AgeRangeId, AgeRange<LazyColumnSpotValuePerBlock<AgeRangeId>>, M>,
>,
}
#[derive(Traversable)]
@@ -128,7 +154,7 @@ pub struct AggregateVecs<M: StorageMode = Rw> {
#[derive(Traversable)]
pub struct Vecs<M: StorageMode = Rw> {
pub age_range: AgeRange<CohortVecs<M>>,
pub age_range: AgeRangeVecs<M>,
#[traversable(flatten)]
pub all: AggregateVecs<M>,
pub sth: AggregateVecs<M>,
@@ -2,5 +2,4 @@ mod compute;
mod import;
mod vecs;
pub(crate) use compute::compute_rest;
pub use vecs::{DerivedVecs, Vecs};
@@ -1,12 +1,11 @@
use brk_cohort::{AGE_RANGE_BOUNDS, AGE_RANGE_COUNT};
use brk_cohort::{AgeRange, AgeRangeId};
use brk_error::Result;
use brk_indexer::Indexer;
use brk_types::{Bitcoin, Height, ONE_DAY_IN_SEC_F64, Sats, StoredF64, Timestamp, Version};
use vecdb::{AnyVec, Exit, ReadableVec};
use vecdb::{AnyVec, CheckedSub, ColumnId, Exit, ReadableVec, StoredVec, WritableVec};
use super::super::activity;
use super::{CohortVecs, SupplyVecs, Vecs};
use crate::{distribution, indexes};
use super::Vecs;
use crate::{distribution, frameworks::WeightedCohortState, indexes};
const HOURS_PER_DAY: f64 = 24.0;
const WRITE_INTERVAL: usize = 10_000;
@@ -20,19 +19,29 @@ impl Vecs {
exit: &Exit,
) -> Result<()> {
let starting_height = indexer.safe_lengths().height;
let source_cohorts: Vec<_> = distribution.utxo_cohorts.age_range.iter().collect();
let supplies: Vec<_> = source_cohorts
.iter()
.map(|cohort| &cohort.metrics.supply.total.sats.height)
.collect();
let transfer_volumes: Vec<_> = source_cohorts
.iter()
.map(|cohort| &cohort.metrics.activity.transfer_volume.block.sats)
.collect();
let coindays_destroyed: Vec<_> = source_cohorts
.iter()
.map(|cohort| &cohort.metrics.activity.coindays_destroyed.block)
.collect();
let supplies = AgeRange::from_fn(|id| {
&id.select(&distribution.utxo_cohorts.age_range)
.metrics
.supply
.total
.sats
.height
});
let transfer_volumes = AgeRange::from_fn(|id| {
&id.select(&distribution.utxo_cohorts.age_range)
.metrics
.activity
.transfer_volume
.block
.sats
});
let coindays_destroyed = AgeRange::from_fn(|id| {
&id.select(&distribution.utxo_cohorts.age_range)
.metrics
.activity
.coindays_destroyed
.block
});
self.compute_created(
starting_height,
@@ -53,116 +62,73 @@ impl Vecs {
&mut self,
starting_height: Height,
timestamps: &T,
supplies: &[&S],
supplies: &AgeRange<&S>,
exit: &Exit,
) -> Result<()>
where
T: ReadableVec<Height, Timestamp>,
S: ReadableVec<Height, Sats>,
{
debug_assert_eq!(supplies.len(), AGE_RANGE_COUNT);
let created_version: Version = std::iter::once(timestamps.version())
let version: Version = std::iter::once(timestamps.version())
.chain(supplies.iter().map(|vec| vec.version()))
.sum();
let mut cohorts: Vec<&mut CohortVecs> = self.iter_mut().collect();
for cohort in cohorts.iter_mut() {
cohort
.coindays_created
.validate_computed_version_or_reset(created_version)?;
}
let start = cohorts
.iter()
.map(|cohort| cohort.coindays_created.cumulative.height.len())
.min()
.unwrap_or_default()
.min(usize::from(starting_height));
for cohort in cohorts.iter_mut() {
cohort.coindays_created.truncate_if_needed_at(start)?;
}
let source_end = supplies
.iter()
.map(|vec| vec.len())
.chain(std::iter::once(timestamps.len()))
.min()
.unwrap_or_default();
self.coindays_created.cumulative.compute_batched_to(
starting_height,
source_end,
version,
WRITE_INTERVAL,
|created, range| {
let mut cumulative = created
.collect_last()
.unwrap_or_else(|| AgeRangeId::from_fn(|_| StoredF64::default()));
let timestamp_start = range.start.saturating_sub(1);
let timestamp_batch = timestamps.collect_range_at(timestamp_start, range.end);
let supply_batches = AgeRange::from_fn(|id| {
id.select(supplies).collect_range_at(range.start, range.end)
});
let mut chunk_start = start;
while chunk_start < source_end {
let chunk_end = (chunk_start + WRITE_INTERVAL).min(source_end);
let timestamp_start = chunk_start.saturating_sub(1);
let timestamp_batch = timestamps.collect_range_at(timestamp_start, chunk_end);
let supply_batches: Vec<_> = supplies
.iter()
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
.collect();
for (offset, _) in supply_batches.first().unwrap().iter().enumerate() {
let interval_seconds =
monotonic_interval_seconds(&timestamp_batch, chunk_start, offset);
for (index, cohort) in cohorts.iter_mut().enumerate() {
cohort.coindays_created.push_block(coindays_created(
supply_batches[index][offset],
interval_seconds,
));
for offset in 0..range.len() {
let interval_seconds =
monotonic_interval_seconds(&timestamp_batch, range.start, offset);
created.push(AgeRangeId::from_fn(|column| {
let value = column.get_mut(&mut cumulative);
*value += coindays_created(
column.select(&supply_batches)[offset],
interval_seconds,
);
*value
}));
}
}
{
let _lock = exit.lock();
for cohort in cohorts.iter_mut() {
cohort.coindays_created.write()?;
}
}
chunk_start = chunk_end;
}
Ok(())
},
exit,
)?;
Ok(())
}
fn compute_consumed<V, D>(
&mut self,
starting_height: Height,
transfer_volumes: &[&V],
source_coindays_destroyed: &[&D],
transfer_volumes: &AgeRange<&V>,
source_coindays_destroyed: &AgeRange<&D>,
exit: &Exit,
) -> Result<()>
where
V: ReadableVec<Height, Sats>,
D: ReadableVec<Height, StoredF64>,
{
debug_assert_eq!(transfer_volumes.len(), AGE_RANGE_COUNT);
debug_assert_eq!(source_coindays_destroyed.len(), AGE_RANGE_COUNT);
let destroyed_version: Version = transfer_volumes
let version: Version = transfer_volumes
.iter()
.map(|vec| vec.version())
.chain(source_coindays_destroyed.iter().map(|vec| vec.version()))
.sum();
let mut cohorts: Vec<&mut CohortVecs> = self.iter_mut().collect();
for cohort in cohorts.iter_mut() {
cohort
.coindays_consumed
.validate_computed_version_or_reset(destroyed_version)?;
}
let start = cohorts
.iter()
.map(|cohort| cohort.coindays_consumed.cumulative.height.len())
.min()
.unwrap_or_default()
.min(usize::from(starting_height));
for cohort in cohorts.iter_mut() {
cohort.coindays_consumed.truncate_if_needed_at(start)?;
}
let source_end = transfer_volumes
.iter()
.map(|vec| vec.len())
@@ -170,107 +136,156 @@ impl Vecs {
.min()
.unwrap_or_default();
let bounds = age_bounds_days();
let mut chunk_start = start;
while chunk_start < source_end {
let chunk_end = (chunk_start + WRITE_INTERVAL).min(source_end);
let transfer_batches: Vec<_> = transfer_volumes
.iter()
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
.collect();
let destroyed_batches: Vec<_> = source_coindays_destroyed
.iter()
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
.collect();
for offset in 0..(chunk_end - chunk_start) {
let volumes_btc: [f64; AGE_RANGE_COUNT] = std::array::from_fn(|index| {
f64::from(Bitcoin::from(transfer_batches[index][offset]))
self.coindays_consumed.cumulative.compute_batched_to(
starting_height,
source_end,
version,
WRITE_INTERVAL,
|target, range| {
let mut cumulative = target
.collect_last()
.unwrap_or_else(|| AgeRangeId::from_fn(|_| StoredF64::default()));
let transfer_batches = AgeRange::from_fn(|id| {
id.select(transfer_volumes)
.collect_range_at(range.start, range.end)
});
let destroyed_batches = AgeRange::from_fn(|id| {
id.select(source_coindays_destroyed)
.collect_range_at(range.start, range.end)
});
let cdd: [f64; AGE_RANGE_COUNT] =
std::array::from_fn(|index| f64::from(destroyed_batches[index][offset]));
let consumed = allocate_consumed_coindays(volumes_btc, cdd, &bounds);
for (index, cohort) in cohorts.iter_mut().enumerate() {
cohort
.coindays_consumed
.push_block(StoredF64::from(consumed[index]));
for offset in 0..range.len() {
let volumes_btc = AgeRange::from_fn(|id| {
f64::from(Bitcoin::from(id.select(&transfer_batches)[offset]))
});
let coindays_destroyed =
AgeRange::from_fn(|id| f64::from(id.select(&destroyed_batches)[offset]));
let consumed =
allocate_consumed_coindays(volumes_btc, coindays_destroyed, &bounds);
target.push(AgeRangeId::from_fn(|column| {
let value = column.get_mut(&mut cumulative);
*value += StoredF64::from(*column.select(&consumed));
*value
}));
}
}
{
let _lock = exit.lock();
for cohort in cohorts.iter_mut() {
cohort.coindays_consumed.write()?;
}
}
chunk_start = chunk_end;
}
Ok(())
},
exit,
)?;
Ok(())
}
fn compute_rest<S>(
&mut self,
starting_height: Height,
supplies: &[&S],
supplies: &AgeRange<&S>,
exit: &Exit,
) -> Result<()>
where
S: ReadableVec<Height, Sats>,
{
debug_assert_eq!(supplies.len(), AGE_RANGE_COUNT);
for (cohort, &total_supply) in self.iter_mut().zip(supplies) {
let CohortVecs {
coindays_created,
coindays_consumed,
coindays_stored,
activity: activity_vecs,
supply,
} = cohort;
activity::compute_rest(
{
let created = self.coindays_created.cumulative.read_only_clone();
let consumed = self.coindays_consumed.cumulative.read_only_clone();
self.coindays_stored.cumulative.compute_transform2_batched(
starting_height,
coindays_created,
coindays_consumed,
coindays_stored,
&mut activity_vecs.wakefulness,
&created,
&consumed,
WRITE_INTERVAL,
|(height, created, consumed, ..)| {
let stored = AgeRangeId::from_fn(|column| {
(*column.get(&created))
.checked_sub(*column.get(&consumed))
.expect("coindays stored underflow")
});
(height, stored)
},
exit,
)?;
supply.compute_from(
self.activity.height.compute_transform2_batched(
starting_height,
total_supply,
&activity_vecs.wakefulness.height,
&activity_vecs.dormancy.height,
&consumed,
&created,
WRITE_INTERVAL,
|(height, consumed, created, ..)| {
let wakefulness = AgeRangeId::from_fn(|column| {
*column.get(&consumed) / *column.get(&created)
});
(height, wakefulness)
},
exit,
)?;
}
Ok(())
self.compute_supply(starting_height, supplies, exit)
}
}
impl SupplyVecs {
fn compute_from(
fn compute_supply<S>(
&mut self,
starting_height: Height,
total_supply: &impl ReadableVec<Height, Sats>,
wakefulness: &impl ReadableVec<Height, StoredF64>,
dormancy: &impl ReadableVec<Height, StoredF64>,
supplies: &AgeRange<&S>,
exit: &Exit,
) -> Result<()> {
self.awake.sats.height.compute_multiply(
starting_height,
total_supply,
wakefulness,
exit,
)?;
self.dormant
.sats
.height
.compute_multiply(starting_height, total_supply, dormancy, exit)?;
) -> Result<()>
where
S: ReadableVec<Height, Sats>,
{
let wakefulness = self.activity.height.read_only_clone();
let source_version: Version = std::iter::once(wakefulness.version())
.chain(supplies.iter().map(|vec| vec.version()))
.sum();
let supply = &mut self.supply;
supply
.awake
.validate_computed_version_or_reset(source_version)?;
supply
.dormant
.validate_computed_version_or_reset(source_version)?;
let start = [supply.awake.height.len(), supply.dormant.height.len()]
.into_iter()
.min()
.unwrap_or_default()
.min(usize::from(starting_height));
supply.awake.truncate_if_needed_at(start)?;
supply.dormant.truncate_if_needed_at(start)?;
let source_end = supplies
.iter()
.map(|vec| vec.len())
.chain(std::iter::once(wakefulness.len()))
.min()
.unwrap_or_default();
let mut chunk_start = start;
while chunk_start < source_end {
let chunk_end = (chunk_start + WRITE_INTERVAL).min(source_end);
let supply_batches = AgeRange::from_fn(|id| {
id.select(supplies).collect_range_at(chunk_start, chunk_end)
});
let wakefulness_batch = wakefulness.collect_range_at(chunk_start, chunk_end);
for (offset, weights) in wakefulness_batch.iter().enumerate() {
let split = AgeRange::from_fn(|id| {
WeightedCohortState::split_supply(
id.select(&supply_batches)[offset],
*id.get(weights),
)
});
supply
.awake
.push(AgeRangeId::from_fn(|id| id.select(&split).0));
supply
.dormant
.push(AgeRangeId::from_fn(|id| id.select(&split).1));
}
{
let _lock = exit.lock();
supply.awake.write()?;
supply.dormant.write()?;
}
chunk_start = chunk_end;
}
Ok(())
}
@@ -295,34 +310,34 @@ fn coindays_created(supply: Sats, interval_seconds: u32) -> StoredF64 {
StoredF64::from(f64::from(Bitcoin::from(supply)) * interval_seconds as f64 / ONE_DAY_IN_SEC_F64)
}
fn age_bounds_days() -> [(f64, f64); AGE_RANGE_COUNT] {
let mut bounds = AGE_RANGE_BOUNDS.iter();
std::array::from_fn(|index| {
let bound = bounds.next().unwrap();
fn age_bounds_days() -> AgeRange<(f64, f64)> {
AgeRange::from_fn(|id| {
let bound = id.bounds();
let lower = bound.start as f64 / HOURS_PER_DAY;
let width = if index + 1 < AGE_RANGE_COUNT {
(bound.end - bound.start) as f64 / HOURS_PER_DAY
} else {
let width = if id == AgeRangeId::Over15Y {
0.0
} else {
(bound.end - bound.start) as f64 / HOURS_PER_DAY
};
(lower, width)
})
}
fn allocate_consumed_coindays(
transfer_volume_btc: [f64; AGE_RANGE_COUNT],
coindays_destroyed: [f64; AGE_RANGE_COUNT],
bounds: &[(f64, f64); AGE_RANGE_COUNT],
) -> [f64; AGE_RANGE_COUNT] {
let mut result = [0.0; AGE_RANGE_COUNT];
transfer_volume_btc: AgeRange<f64>,
coindays_destroyed: AgeRange<f64>,
bounds: &AgeRange<(f64, f64)>,
) -> AgeRange<f64> {
let mut result = AgeRange::default();
let mut older_transfer_volume = 0.0;
for index in (0..AGE_RANGE_COUNT).rev() {
let (lower_days, width_days) = bounds[index];
for &id in AgeRangeId::ALL.iter().rev() {
let (lower_days, width_days) = *id.select(bounds);
let transfer_volume = *id.select(&transfer_volume_btc);
let within_cohort =
(coindays_destroyed[index] - transfer_volume_btc[index] * lower_days).max(0.0);
result[index] = within_cohort + older_transfer_volume * width_days;
older_transfer_volume += transfer_volume_btc[index];
(*id.select(&coindays_destroyed) - transfer_volume * lower_days).max(0.0);
*id.select_mut(&mut result) = within_cohort + older_transfer_volume * width_days;
older_transfer_volume += transfer_volume;
}
result
@@ -330,6 +345,8 @@ fn allocate_consumed_coindays(
#[cfg(test)]
mod tests {
use brk_cohort::AGE_RANGE_COUNT;
use super::*;
#[test]
@@ -355,67 +372,76 @@ mod tests {
#[test]
fn destruction_at_a_boundary_stays_in_the_ranges_already_traversed() {
let mut volumes = [0.0; AGE_RANGE_COUNT];
let mut cdd = [0.0; AGE_RANGE_COUNT];
volumes[2] = 1.0;
cdd[2] = 1.0;
let mut volumes = AgeRange::default();
let mut cdd = AgeRange::default();
volumes._1d_to_1w = 1.0;
cdd._1d_to_1w = 1.0;
let allocated = allocate_consumed_coindays(volumes, cdd, &age_bounds_days());
assert!((allocated[0] - 1.0 / HOURS_PER_DAY).abs() < 1e-12);
assert!((allocated[1] - 23.0 / HOURS_PER_DAY).abs() < 1e-12);
assert!(allocated[2].abs() < 1e-12);
assert!((allocated.under_1h - 1.0 / HOURS_PER_DAY).abs() < 1e-12);
assert!((allocated._1h_to_1d - 23.0 / HOURS_PER_DAY).abs() < 1e-12);
assert!(allocated._1d_to_1w.abs() < 1e-12);
assert!((allocated.iter().sum::<f64>() - 1.0).abs() < 1e-12);
}
#[test]
fn consumed_coindays_cover_every_traversed_cohort() {
let mut volumes = [0.0; AGE_RANGE_COUNT];
let mut cdd = [0.0; AGE_RANGE_COUNT];
volumes[2] = 2.0;
cdd[2] = 20.0;
let mut volumes = AgeRange::default();
let mut cdd = AgeRange::default();
volumes._1d_to_1w = 2.0;
cdd._1d_to_1w = 20.0;
let allocated = allocate_consumed_coindays(volumes, cdd, &age_bounds_days());
assert!((allocated[0] - 2.0 / HOURS_PER_DAY).abs() < 1e-12);
assert!((allocated[1] - 46.0 / HOURS_PER_DAY).abs() < 1e-12);
assert!((allocated[2] - 18.0).abs() < 1e-12);
assert!((allocated.under_1h - 2.0 / HOURS_PER_DAY).abs() < 1e-12);
assert!((allocated._1h_to_1d - 46.0 / HOURS_PER_DAY).abs() < 1e-12);
assert!((allocated._1d_to_1w - 18.0).abs() < 1e-12);
assert!((allocated.iter().sum::<f64>() - 20.0).abs() < 1e-12);
}
#[test]
fn allocated_coindays_conserve_mixed_cohort_destruction() {
let bounds = age_bounds_days();
let mut volumes = [0.0; AGE_RANGE_COUNT];
let mut cdd = [0.0; AGE_RANGE_COUNT];
let mut volumes = AgeRange::default();
let mut cdd = AgeRange::default();
for index in [0, 1, 2, 10, 20, AGE_RANGE_COUNT - 2, AGE_RANGE_COUNT - 1] {
let (lower, width) = bounds[index];
let volume = index as f64 + 1.0;
for id in [
AgeRangeId::Under1H,
AgeRangeId::From1HTo1D,
AgeRangeId::From1DTo1W,
AgeRangeId::From9MTo1Y,
AgeRangeId::From10YTo12Y,
AgeRangeId::From12YTo15Y,
AgeRangeId::Over15Y,
] {
let (lower, width) = *id.select(&bounds);
let volume = id.index() as f64 + 1.0;
let age = lower + if width > 0.0 { width / 2.0 } else { 30.0 };
volumes[index] = volume;
cdd[index] = volume * age;
*id.select_mut(&mut volumes) = volume;
*id.select_mut(&mut cdd) = volume * age;
}
let expected = cdd.iter().sum::<f64>();
let allocated = allocate_consumed_coindays(volumes, cdd, &bounds);
assert!((allocated.iter().sum::<f64>() - cdd.iter().sum::<f64>()).abs() < 1e-9);
assert!((allocated.iter().sum::<f64>() - expected).abs() < 1e-9);
}
#[test]
fn bounds_and_allocation_cover_every_canonical_age_range() {
let bounds = age_bounds_days();
let mut volumes = [0.0; AGE_RANGE_COUNT];
let mut cdd = [0.0; AGE_RANGE_COUNT];
let last = AGE_RANGE_COUNT - 1;
let mut volumes = AgeRange::default();
let mut cdd = AgeRange::default();
volumes[last] = 1.0;
cdd[last] = bounds[last].0 + 30.0;
volumes.over_15y = 1.0;
cdd.over_15y = bounds.over_15y.0 + 30.0;
let expected = cdd.iter().sum::<f64>();
let allocated = allocate_consumed_coindays(volumes, cdd, &bounds);
assert_eq!(bounds.len(), AGE_RANGE_BOUNDS.iter().count());
assert!(allocated[last] > 0.0);
assert!((allocated.iter().sum::<f64>() - cdd.iter().sum::<f64>()).abs() < 1e-9);
assert_eq!(bounds.iter().count(), AGE_RANGE_COUNT);
assert!(allocated.over_15y > 0.0);
assert!((allocated.iter().sum::<f64>() - expected).abs() < 1e-9);
}
}
@@ -1,110 +1,59 @@
use brk_cohort::{AgeRange, CohortContext};
use brk_cohort::{AgeRangeId, CohortContext};
use brk_error::Result;
use brk_types::{Cents, Height, Version};
use vecdb::{CachedBoxedVec, Database, ReadableCloneableVec};
use brk_types::{Cents, Height, StoredF64, Version};
use vecdb::{CachedBoxedVec, Database, PcoVec, ReadOnlyColumnarVec, ReadableCloneableVec};
use super::{ActivityVecs, CohortVecs, SupplyVecs, Vecs};
use super::{ActivitySeries, SupplyVecs, Vecs};
use crate::{
indexes,
internal::{
CachedWindowStartVec, LazyPerBlock, OddsF64, OneMinusF64, PerBlock,
PerBlockCumulativeRolling, SpotValuePerBlock, Windows,
CachedWindowStartVec, ColumnarPerBlock, ColumnarPerBlockCumulativeRolling,
LazyColumnPerBlock, LazyColumnPerBlockCumulativeRolling, LazyColumnSpotValuePerBlock,
LazyPerBlock, OddsF64, OneMinusF64, Windows,
},
};
const VERSION: Version = Version::ONE;
const VERSION: Version = Version::TWO;
impl ActivityVecs {
fn forced_import(
db: &Database,
name: &str,
impl ActivitySeries {
fn new(
version: Version,
source: &ReadOnlyColumnarVec<PcoVec<Height, StoredF64>, AgeRangeId>,
indexes: &indexes::Vecs,
) -> Result<Self> {
let wakefulness =
PerBlock::forced_import(db, &format!("{name}_wakefulness"), version, indexes)?;
let dormancy = LazyPerBlock::from_computed::<OneMinusF64>(
&format!("{name}_dormancy"),
version,
wakefulness.height.read_only_boxed_clone(),
&wakefulness,
);
let wakefulness_to_dormancy = LazyPerBlock::from_computed::<OddsF64>(
&format!("{name}_wakefulness_to_dormancy"),
version,
wakefulness.height.read_only_boxed_clone(),
&wakefulness,
);
) -> Self {
let wakefulness = AgeRangeId::series(CohortContext::Utxo, |column, name| {
LazyColumnPerBlock::new(
&format!("{name}_wakefulness"),
version,
source,
column,
indexes,
)
});
let dormancy = AgeRangeId::series(CohortContext::Utxo, |column, name| {
let wakefulness = column.select(&wakefulness);
LazyPerBlock::from_resolutions::<OneMinusF64>(
&format!("{name}_dormancy"),
version,
wakefulness.height.read_only_boxed_clone(),
&wakefulness.resolutions,
)
});
let wakefulness_to_dormancy = AgeRangeId::series(CohortContext::Utxo, |column, name| {
let wakefulness = column.select(&wakefulness);
LazyPerBlock::from_resolutions::<OddsF64>(
&format!("{name}_wakefulness_to_dormancy"),
version,
wakefulness.height.read_only_boxed_clone(),
&wakefulness.resolutions,
)
});
Ok(Self {
Self {
wakefulness,
dormancy,
wakefulness_to_dormancy,
})
}
}
impl SupplyVecs {
fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
Ok(Self {
awake: SpotValuePerBlock::forced_import(
db,
&format!("{name}_awake_supply"),
version,
indexes,
spot_price,
)?,
dormant: SpotValuePerBlock::forced_import(
db,
&format!("{name}_dormant_supply"),
version,
indexes,
spot_price,
)?,
})
}
}
impl CohortVecs {
fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &Windows<&CachedWindowStartVec>,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
Ok(Self {
coindays_created: PerBlockCumulativeRolling::forced_import(
db,
&format!("{name}_coindays_created"),
version,
indexes,
cached_starts,
)?,
coindays_consumed: PerBlockCumulativeRolling::forced_import(
db,
&format!("{name}_coindays_consumed"),
version,
indexes,
cached_starts,
)?,
coindays_stored: PerBlockCumulativeRolling::forced_import(
db,
&format!("{name}_coindays_stored"),
version,
indexes,
cached_starts,
)?,
activity: ActivityVecs::forced_import(db, name, version, indexes)?,
supply: SupplyVecs::forced_import(db, name, version, indexes, spot_price)?,
})
}
}
}
@@ -117,10 +66,93 @@ impl Vecs {
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let version = parent_version + VERSION;
let coindays_created = ColumnarPerBlockCumulativeRolling::forced_import(
db,
&CohortContext::Utxo.prefixed("age_range_coindays_created_cumulative"),
version,
|source| {
AgeRangeId::series(CohortContext::Utxo, |column, name| {
LazyColumnPerBlockCumulativeRolling::new(
&format!("{name}_coindays_created"),
version,
source,
column,
indexes,
cached_starts,
)
})
},
)?;
let coindays_consumed = ColumnarPerBlockCumulativeRolling::forced_import(
db,
&CohortContext::Utxo.prefixed("age_range_coindays_consumed_cumulative"),
version,
|source| {
AgeRangeId::series(CohortContext::Utxo, |column, name| {
LazyColumnPerBlockCumulativeRolling::new(
&format!("{name}_coindays_consumed"),
version,
source,
column,
indexes,
cached_starts,
)
})
},
)?;
let coindays_stored = ColumnarPerBlockCumulativeRolling::forced_import(
db,
&CohortContext::Utxo.prefixed("age_range_coindays_stored_cumulative"),
version,
|source| {
AgeRangeId::series(CohortContext::Utxo, |column, name| {
LazyColumnPerBlockCumulativeRolling::new(
&format!("{name}_coindays_stored"),
version,
source,
column,
indexes,
cached_starts,
)
})
},
)?;
let activity = ColumnarPerBlock::forced_import(
db,
&CohortContext::Utxo.prefixed("age_range_wakefulness"),
version,
|source| ActivitySeries::new(version, source, indexes),
)?;
let import_supply = |side: &str| {
ColumnarPerBlock::forced_import(
db,
&CohortContext::Utxo.prefixed(&format!("age_range_{side}_supply_sats")),
version,
|source| {
AgeRangeId::series(CohortContext::Utxo, |column, name| {
LazyColumnSpotValuePerBlock::new(
&format!("{name}_{side}_supply"),
version,
source,
column,
indexes,
spot_price,
)
})
},
)
};
let supply = SupplyVecs {
awake: import_supply("awake")?,
dormant: import_supply("dormant")?,
};
Ok(Self(AgeRange::try_new(|_, name| {
let name = CohortContext::Utxo.prefixed(name);
CohortVecs::forced_import(db, &name, version, indexes, cached_starts, spot_price)
})?))
Ok(Self {
coindays_created,
coindays_consumed,
coindays_stored,
activity,
supply,
})
}
}
@@ -2,4 +2,4 @@ mod compute;
mod import;
mod vecs;
pub use vecs::{ActivityVecs, CohortVecs, SupplyVecs, Vecs};
pub use vecs::{ActivitySeries, SupplyVecs, Vecs};
@@ -1,36 +1,48 @@
use brk_cohort::AgeRange;
use brk_cohort::{AgeRange, AgeRangeId};
use brk_traversable::Traversable;
use brk_types::StoredF64;
use derive_more::{Deref, DerefMut};
use brk_types::{Sats, StoredF64};
use vecdb::{Rw, StorageMode};
use crate::internal::{LazyPerBlock, PerBlock, PerBlockCumulativeRolling, SpotValuePerBlock};
use crate::internal::{
ColumnarPerBlock, ColumnarPerBlockCumulativeRolling, LazyColumnPerBlock,
LazyColumnPerBlockCumulativeRolling, LazyColumnSpotValuePerBlock, LazyPerBlock,
};
#[derive(Traversable)]
pub struct ActivityVecs<M: StorageMode = Rw> {
pub wakefulness: PerBlock<StoredF64, M>,
pub dormancy: LazyPerBlock<StoredF64>,
pub wakefulness_to_dormancy: LazyPerBlock<StoredF64>,
#[derive(Clone, Traversable)]
pub struct ActivitySeries {
pub wakefulness: AgeRange<LazyColumnPerBlock<StoredF64, AgeRangeId>>,
pub dormancy: AgeRange<LazyPerBlock<StoredF64>>,
pub wakefulness_to_dormancy: AgeRange<LazyPerBlock<StoredF64>>,
}
#[derive(Traversable)]
pub struct SupplyVecs<M: StorageMode = Rw> {
pub awake: SpotValuePerBlock<M>,
pub dormant: SpotValuePerBlock<M>,
pub struct SupplyVecs<T> {
pub awake: T,
pub dormant: T,
}
#[derive(Deref, DerefMut, Traversable)]
pub struct CohortVecs<M: StorageMode = Rw> {
pub coindays_created: PerBlockCumulativeRolling<StoredF64, M>,
pub coindays_consumed: PerBlockCumulativeRolling<StoredF64, M>,
pub coindays_stored: PerBlockCumulativeRolling<StoredF64, M>,
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub activity: ActivityVecs<M>,
pub supply: SupplyVecs<M>,
#[derive(Traversable)]
pub struct Vecs<M: StorageMode = Rw> {
pub coindays_created: ColumnarPerBlockCumulativeRolling<
StoredF64,
AgeRangeId,
AgeRange<LazyColumnPerBlockCumulativeRolling<StoredF64, AgeRangeId>>,
M,
>,
pub coindays_consumed: ColumnarPerBlockCumulativeRolling<
StoredF64,
AgeRangeId,
AgeRange<LazyColumnPerBlockCumulativeRolling<StoredF64, AgeRangeId>>,
M,
>,
pub coindays_stored: ColumnarPerBlockCumulativeRolling<
StoredF64,
AgeRangeId,
AgeRange<LazyColumnPerBlockCumulativeRolling<StoredF64, AgeRangeId>>,
M,
>,
pub activity: ColumnarPerBlock<StoredF64, AgeRangeId, ActivitySeries, M>,
pub supply: SupplyVecs<
ColumnarPerBlock<Sats, AgeRangeId, AgeRange<LazyColumnSpotValuePerBlock<AgeRangeId>>, M>,
>,
}
#[derive(Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct Vecs<M: StorageMode = Rw>(pub AgeRange<CohortVecs<M>>);
@@ -1,13 +1,13 @@
use brk_cohort::{AGE_RANGE_COUNT, AGE_RANGE_FILTERS, ByTerm, TERM_FILTERS};
use brk_cohort::{AGE_RANGE_COUNT, AgeRange, AgeRangeId, ByTerm, TERM_FILTERS};
use brk_error::Result;
use brk_indexer::Indexer;
use brk_types::{Cents, Height, Sats, StoredF64, Version};
use vecdb::{AnyStoredVec, AnyVec, Exit, ReadableVec, WritableVec};
use vecdb::{AnyStoredVec, AnyVec, ColumnId, Exit, ReadableVec, WritableVec};
use super::{AllCohortVecs, StoredCohortVecs, Vecs};
use crate::{
distribution,
frameworks::{WeightedCohortState, realized_price},
frameworks::WeightedCohortState,
internal::{PerBlock, db_utils::validate_any_computed_version_or_reset},
};
@@ -25,30 +25,38 @@ impl Vecs {
exit: &Exit,
) -> Result<()> {
let starting_height = indexer.safe_lengths().height;
let source_cohorts: Vec<_> = distribution.utxo_cohorts.age_range.iter().collect();
let supplies: Vec<_> = source_cohorts
.iter()
.map(|cohort| &cohort.metrics.supply.total.sats.height)
.collect();
let loss_supplies: Vec<_> = source_cohorts
.iter()
.map(|cohort| &cohort.metrics.supply.in_loss.sats.height)
.collect();
let realized_caps: Vec<_> = source_cohorts
.iter()
.map(|cohort| &cohort.metrics.realized.cap.cents.height)
.collect();
let weights: Vec<_> = age_range
.iter()
.map(|cohort| &cohort.wakefulness.height)
.collect();
let supplies = AgeRange::from_fn(|id| {
&id.select(&distribution.utxo_cohorts.age_range)
.metrics
.supply
.total
.sats
.height
});
let loss_supplies = AgeRange::from_fn(|id| {
&id.select(&distribution.utxo_cohorts.age_range)
.metrics
.supply
.in_loss
.sats
.height
});
let realized_caps = AgeRange::from_fn(|id| {
&id.select(&distribution.utxo_cohorts.age_range)
.metrics
.realized
.cap
.cents
.height
});
let weights = &age_range.activity.height;
self.compute_primary(
starting_height,
&supplies,
&loss_supplies,
&realized_caps,
&weights,
weights,
all_supply_in_loss_share,
exit,
)
@@ -58,29 +66,24 @@ impl Vecs {
fn compute_primary<S, C, W>(
&mut self,
starting_height: Height,
supplies: &[&S],
loss_supplies: &[&S],
realized_caps: &[&C],
weights: &[&W],
supplies: &AgeRange<&S>,
loss_supplies: &AgeRange<&S>,
realized_caps: &AgeRange<&C>,
weights: &W,
all_supply_in_loss_share: &mut PerBlock<StoredF64>,
exit: &Exit,
) -> Result<()>
where
S: ReadableVec<Height, Sats>,
C: ReadableVec<Height, Cents>,
W: ReadableVec<Height, StoredF64>,
W: ReadableVec<Height, [StoredF64; AGE_RANGE_COUNT]>,
{
debug_assert_eq!(supplies.len(), AGE_RANGE_COUNT);
debug_assert_eq!(loss_supplies.len(), AGE_RANGE_COUNT);
debug_assert_eq!(realized_caps.len(), AGE_RANGE_COUNT);
debug_assert_eq!(weights.len(), AGE_RANGE_COUNT);
let source_version: Version = supplies
.iter()
.map(|vec| vec.version())
.chain(loss_supplies.iter().map(|vec| vec.version()))
.chain(realized_caps.iter().map(|vec| vec.version()))
.chain(weights.iter().map(|vec| vec.version()))
.chain(std::iter::once(weights.version()))
.sum();
for vec in self.primary_vecs_mut() {
@@ -91,7 +94,6 @@ impl Vecs {
let start = self
.primary_vecs_mut()
.into_iter()
.map(|vec| vec.len())
.chain(std::iter::once(all_supply_in_loss_share.len()))
.min()
@@ -108,47 +110,39 @@ impl Vecs {
.map(|vec| vec.len())
.chain(loss_supplies.iter().map(|vec| vec.len()))
.chain(realized_caps.iter().map(|vec| vec.len()))
.chain(weights.iter().map(|vec| vec.len()))
.chain(std::iter::once(weights.len()))
.min()
.unwrap_or_default();
let mut age_filters = AGE_RANGE_FILTERS.iter();
let is_sth: [bool; AGE_RANGE_COUNT] =
std::array::from_fn(|_| TERM_FILTERS.short.includes(age_filters.next().unwrap()));
let mut chunk_start = start;
while chunk_start < source_end {
let chunk_end = (chunk_start + WRITE_INTERVAL).min(source_end);
let supply_batches: Vec<_> = supplies
.iter()
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
.collect();
let loss_batches: Vec<_> = loss_supplies
.iter()
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
.collect();
let cap_batches: Vec<_> = realized_caps
.iter()
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
.collect();
let weight_batches: Vec<_> = weights
.iter()
.map(|vec| vec.collect_range_at(chunk_start, chunk_end))
.collect();
let supply_batches = AgeRange::from_fn(|id| {
id.select(supplies).collect_range_at(chunk_start, chunk_end)
});
let loss_batches = AgeRange::from_fn(|id| {
id.select(loss_supplies)
.collect_range_at(chunk_start, chunk_end)
});
let cap_batches = AgeRange::from_fn(|id| {
id.select(realized_caps)
.collect_range_at(chunk_start, chunk_end)
});
let weight_batch = weights.collect_range_at(chunk_start, chunk_end);
for offset in 0..(chunk_end - chunk_start) {
for (offset, weights) in weight_batch.iter().enumerate() {
let mut terms = ByTerm::<WeightedCohortState>::default();
for age in 0..AGE_RANGE_COUNT {
let term = if is_sth[age] {
for &id in AgeRangeId::ALL {
let term = if TERM_FILTERS.short.includes(id.filter()) {
&mut terms.short
} else {
&mut terms.long
};
term.add(
supply_batches[age][offset],
loss_batches[age][offset],
cap_batches[age][offset],
weight_batches[age][offset],
id.select(&supply_batches)[offset],
id.select(&loss_batches)[offset],
id.select(&cap_batches)[offset],
*id.get(weights),
);
}
let all = terms.short.merged(terms.long);
@@ -171,12 +165,12 @@ impl Vecs {
Ok(())
}
fn primary_vecs_mut(&mut self) -> Vec<&mut dyn AnyStoredVec> {
let mut vecs = Vec::with_capacity(ALL_PRIMARY_VEC_COUNT + 2 * STORED_PRIMARY_VEC_COUNT);
vecs.extend(self.all.primary_vecs_mut());
vecs.extend(self.sth.primary_vecs_mut());
vecs.extend(self.lth.primary_vecs_mut());
vecs
fn primary_vecs_mut(&mut self) -> impl Iterator<Item = &mut dyn AnyStoredVec> {
self.all
.primary_vecs_mut()
.into_iter()
.chain(self.sth.primary_vecs_mut())
.chain(self.lth.primary_vecs_mut())
}
}
@@ -189,11 +183,7 @@ impl AllCohortVecs {
.height
.push(state.complement_supply);
self.awake.cap.cents.height.push(state.weighted_cap);
self.awake
.price
.cents
.height
.push(realized_price(state.weighted_cap, state.weighted_supply));
self.awake.price.cents.height.push(state.realized_price());
}
fn primary_vecs_mut(&mut self) -> [&mut dyn AnyStoredVec; ALL_PRIMARY_VEC_COUNT] {
@@ -219,11 +209,7 @@ impl StoredCohortVecs {
.height
.push(state.supply_in_loss.value());
self.awake.cap.cents.height.push(state.weighted_cap);
self.awake
.price
.cents
.height
.push(realized_price(state.weighted_cap, state.weighted_supply));
self.awake.price.cents.height.push(state.realized_price());
}
fn primary_vecs_mut(&mut self) -> [&mut dyn AnyStoredVec; STORED_PRIMARY_VEC_COUNT] {
+20 -17
View File
@@ -58,6 +58,11 @@ pub(crate) struct WeightedCohortState {
}
impl WeightedCohortState {
#[inline]
pub(crate) fn split_supply(total: Sats, weight: StoredF64) -> (Sats, Sats) {
(weight * total, (StoredF64::from(1.0) - weight) * total)
}
#[inline]
pub(crate) fn add(
&mut self,
@@ -66,9 +71,10 @@ impl WeightedCohortState {
total_cap: Cents,
weight: StoredF64,
) -> WeightedCohortContribution {
let (weighted_supply, complement_supply) = Self::split_supply(total_supply, weight);
let contribution = WeightedCohortContribution {
weighted_supply: weight * total_supply,
complement_supply: (StoredF64::from(1.0) - weight) * total_supply,
weighted_supply,
complement_supply,
weighted_cap: if total_supply.is_zero() {
Cents::ZERO
} else {
@@ -96,18 +102,18 @@ impl WeightedCohortState {
self.supply_in_loss.merge(other.supply_in_loss);
self
}
}
#[inline]
pub(crate) fn realized_price(cap: Cents, supply: Sats) -> Cents {
if unlikely(cap.is_nan()) {
return Cents::NAN;
#[inline]
pub(crate) fn realized_price(&self) -> Cents {
if unlikely(self.weighted_cap.is_nan()) {
return Cents::NAN;
}
(self.weighted_cap.as_u128() * Sats::ONE_BTC_U128)
.checked_div(self.weighted_supply.as_u128())
.map(Cents::from)
.unwrap_or(Cents::ZERO)
}
(cap.as_u128() * Sats::ONE_BTC_U128)
.checked_div(supply.as_u128())
.map(Cents::from)
.unwrap_or(Cents::ZERO)
}
#[cfg(test)]
@@ -122,10 +128,7 @@ mod tests {
assert_eq!(contribution.weighted_cap, Cents::ZERO);
assert_eq!(state.weighted_cap, Cents::ZERO);
assert_eq!(
realized_price(state.weighted_cap, state.weighted_supply),
Cents::ZERO
);
assert_eq!(state.realized_price(), Cents::ZERO);
}
#[test]
@@ -140,6 +143,6 @@ mod tests {
);
assert!(state.weighted_cap.is_nan());
assert!(realized_price(state.weighted_cap, state.weighted_supply).is_nan());
assert!(state.realized_price().is_nan());
}
}
@@ -28,7 +28,6 @@ pub struct Timestamps<M: StorageMode = Rw> {
#[deref]
#[deref_mut]
#[traversable(flatten)]
#[allow(clippy::type_complexity)]
pub resolutions: PerResolution<
LazyVec<Minute10, Timestamp, Minute10, Height>,
LazyVec<Minute30, Timestamp, Minute30, Height>,
@@ -1,6 +1,8 @@
use brk_cohort::SpendableTypeId;
use brk_error::{OptionData, Result};
use brk_indexer::Indexer;
use vecdb::{AnyVec, Exit, ReadableVec, VecIndex};
use brk_types::{StoredU16, StoredU64};
use vecdb::{AnyVec, ColumnId, Exit, ReadableVec, VecIndex};
use super::Vecs;
use crate::internal::{CoinbasePolicy, walk_blocks};
@@ -18,13 +20,15 @@ impl Vecs {
self.input_count
.validate_and_truncate(dep_version, starting_lengths.height)?;
self.input_count.clear();
self.tx_count
.validate_and_truncate(dep_version, starting_lengths.height)?;
let skip = self
.input_count
.min_stateful_len()
.min(self.tx_count.min_stateful_len());
.height
.len()
.min(self.tx_count.cumulative.len());
let first_tx_index = &indexer.vecs().transactions.first_tx_index;
let end = first_tx_index.len();
@@ -59,8 +63,14 @@ impl Vecs {
Ok(())
},
|agg| {
self.input_count.push_block(&agg.entries_per_type);
self.tx_count.push_block(&agg.txs_per_type);
self.input_count.push(SpendableTypeId::from_fn(|column| {
let value = agg.entries_per_type[column.output_type() as usize];
debug_assert!(u16::try_from(value).is_ok());
StoredU16::new(value as u16)
}));
self.tx_count.push_block(SpendableTypeId::from_fn(|column| {
StoredU64::from(agg.txs_per_type[column.output_type() as usize])
}));
height += 1;
if height.is_multiple_of(WRITE_INTERVAL) {
@@ -1,11 +1,14 @@
use brk_cohort::SpendableTypeId;
use brk_error::Result;
use brk_types::{Height, StoredU64, Version};
use brk_types::{Height, StoredU16, StoredU64, Version};
use vecdb::Database;
use super::{Vecs, WithInputTypes};
use crate::{
indexes,
internal::{CachedWindowStartVec, Windows},
internal::{
CachedWindowStartVec, ColumnarPerBlock, ColumnarPerBlockCumulativeRolling, Windows,
},
};
fn identity(_: Height, value: StoredU64) -> StoredU64 {
@@ -23,34 +26,50 @@ impl Vecs {
indexes: &indexes::Vecs,
cached_starts: &Windows<&CachedWindowStartVec>,
) -> Result<Self> {
let input_count_source = indexes.input_count_source();
let input_count = WithInputTypes::forced_import_counts(
let columnar_version = version + Version::ONE;
let all_input_count = indexes.input_count_source();
let input_count = ColumnarPerBlock::<StoredU16, SpendableTypeId, _>::forced_import(
db,
"input_count_bis",
|t| format!("{t}_prevout_count"),
version,
(input_count_source, identity),
indexes,
cached_starts,
"prevout_count_by_type",
columnar_version,
|source| {
WithInputTypes::from_columnar_count_source(
"input_count_bis",
|t| format!("{t}_prevout_count"),
columnar_version,
(all_input_count, identity),
source,
indexes,
cached_starts,
)
},
)?;
let input_share = input_count.lazy_shares(
version,
columnar_version,
|name| format!("{name}_prevout_share"),
cached_starts,
indexes,
);
let transaction_count_source = indexes.transaction_count_source();
let tx_count = WithInputTypes::forced_import(
db,
"non_coinbase_tx_count",
|t| format!("tx_count_with_{t}_prevout"),
version,
(transaction_count_source, without_coinbase),
indexes,
cached_starts,
)?;
let tx_count =
ColumnarPerBlockCumulativeRolling::<StoredU64, SpendableTypeId, _>::forced_import(
db,
"tx_count_with_prevout_by_type_cumulative",
columnar_version,
|source| {
WithInputTypes::from_columnar_source(
"non_coinbase_tx_count",
|t| format!("tx_count_with_{t}_prevout"),
columnar_version,
(transaction_count_source, without_coinbase),
source,
indexes,
cached_starts,
)
},
)?;
let tx_share = tx_count.lazy_shares(
version,
columnar_version,
|name| format!("tx_share_with_{name}_prevout"),
cached_starts,
indexes,
+16 -5
View File
@@ -1,17 +1,28 @@
use brk_cohort::SpendableType;
use brk_cohort::{SpendableType, SpendableTypeId};
use brk_traversable::Traversable;
use brk_types::{PartsPerMillion32, StoredU64};
use brk_types::{PartsPerMillion32, StoredU16, StoredU64};
use vecdb::{Rw, StorageMode};
use super::WithInputTypes;
use crate::internal::{
CachedCountPerBlockCumulativeRolling, LazyPercentCumulativeRolling, PerBlockCumulativeRolling,
ColumnarPerBlock, ColumnarPerBlockCumulativeRolling, LazyColumnCountPerBlockCumulativeRolling,
LazyColumnPerBlockCumulativeRolling, LazyPercentCumulativeRolling,
};
#[derive(Traversable)]
pub struct Vecs<M: StorageMode = Rw> {
pub input_count: WithInputTypes<CachedCountPerBlockCumulativeRolling<M>>,
pub input_count: ColumnarPerBlock<
StoredU16,
SpendableTypeId,
WithInputTypes<LazyColumnCountPerBlockCumulativeRolling>,
M,
>,
pub input_share: SpendableType<LazyPercentCumulativeRolling<PartsPerMillion32>>,
pub tx_count: WithInputTypes<PerBlockCumulativeRolling<StoredU64, M>>,
pub tx_count: ColumnarPerBlockCumulativeRolling<
StoredU64,
SpendableTypeId,
WithInputTypes<LazyColumnPerBlockCumulativeRolling<StoredU64, SpendableTypeId>>,
M,
>,
pub tx_share: SpendableType<LazyPercentCumulativeRolling<PartsPerMillion32>>,
}
@@ -1,27 +1,26 @@
//! Generic `all` + per-input-type container (11 spendable types — no
//! op_return since op_return outputs are non-spendable).
use brk_cohort::{ByAddrType, Filter, SpendableType};
use brk_error::Result;
use brk_cohort::{ByAddrType, Filter, SpendableType, SpendableTypeId};
use brk_traversable::Traversable;
use brk_types::{Height, PartsPerMillion32, StoredU64, Version};
use brk_types::{Height, PartsPerMillion32, StoredU16, StoredU64, Version};
use vecdb::{
AnyStoredVec, AnyVec, CachedBoxedVec, CachedReadableVec, Database, ReadableCloneableVec,
ReadableVec, TypedVec, WritableVec,
CachedBoxedVec, CachedReadableVec, PcoVec, ReadOnlyColumnarVec, ReadableCloneableVec,
ReadableVec, TypedVec,
};
use crate::{
indexes,
internal::{
CachedBlockCountReader, CachedCountPerBlockCumulativeRolling, CachedWindowStartVec,
LazyPerBlockCumulativeRolling, LazyPercentCumulativeRolling, PerBlockCumulativeRolling,
RatioU64, Windows,
CachedBlockCountReader, CachedWindowStartVec, LazyColumnCountPerBlockCumulativeRolling,
LazyColumnPerBlockCumulativeRolling, LazyPerBlockCumulativeRolling,
LazyPercentCumulativeRolling, RatioU64, Windows,
},
};
/// `all` aggregate plus per-input-type breakdown across the 11 spendable
/// output types. The "type" of an input is the previous output it spends.
#[derive(Traversable)]
#[derive(Clone, Traversable)]
pub struct WithInputTypes<V> {
pub all: LazyPerBlockCumulativeRolling<StoredU64>,
#[traversable(skip)]
@@ -64,20 +63,6 @@ impl<V> WithInputTypes<V> {
}
}
fn min_stateful_len_with(&self, len: impl Fn(&V) -> usize) -> usize {
self.by_type.iter().map(len).min().unwrap()
}
fn try_for_each_type_mut(&mut self, mut apply: impl FnMut(&mut V) -> Result<()>) -> Result<()> {
self.by_type.iter_mut().try_for_each(&mut apply)
}
fn push_block_with(&mut self, per_type: &[u64; 12], mut push: impl FnMut(&mut V, StoredU64)) {
for (output_type, vec) in self.by_type.iter_typed_mut() {
push(vec, StoredU64::from(per_type[output_type as usize]));
}
}
pub(crate) fn lazy_share(
&self,
name: &str,
@@ -102,16 +87,16 @@ impl<V> WithInputTypes<V> {
}
}
impl WithInputTypes<CachedCountPerBlockCumulativeRolling> {
pub(crate) fn forced_import_counts<S>(
db: &Database,
impl WithInputTypes<LazyColumnCountPerBlockCumulativeRolling> {
pub(crate) fn from_columnar_count_source<S>(
all_name: &str,
per_type_name: impl Fn(&str) -> String,
version: Version,
all: (S, fn(Height, StoredU64) -> StoredU64),
source: &ReadOnlyColumnarVec<PcoVec<Height, StoredU16>, SpendableTypeId>,
indexes: &indexes::Vecs,
cached_starts: &Windows<&CachedWindowStartVec>,
) -> Result<Self>
) -> Self
where
S: TypedVec<I = Height, T = StoredU64>
+ ReadableVec<Height, StoredU64>
@@ -119,36 +104,21 @@ impl WithInputTypes<CachedCountPerBlockCumulativeRolling> {
+ Clone
+ 'static,
{
let by_type = SpendableType::try_new(|_, name| {
CachedCountPerBlockCumulativeRolling::forced_import(
db,
&per_type_name(name),
version,
indexes,
cached_starts,
)
})?;
Ok(Self::new(
all_name,
version,
all,
by_type,
indexes,
cached_starts,
))
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.min_stateful_len_with(|vec| vec.min_stateful_len())
}
pub(crate) fn cached_addr_type_counts(&self) -> ByAddrType<CachedBlockCountReader> {
ByAddrType::new(|filter| {
let by_type = SpendableType::new(|filter, name| {
let Filter::Type(output_type) = filter else {
unreachable!()
};
self.by_type.get(output_type).cached_cumulative()
})
LazyColumnCountPerBlockCumulativeRolling::new(
&per_type_name(name),
version,
source,
SpendableTypeId::from_output_type(output_type)
.expect("spendable output type column"),
indexes,
cached_starts,
)
});
Self::new(all_name, version, all, by_type, indexes, cached_starts)
}
pub(crate) fn lazy_shares(
@@ -173,38 +143,30 @@ impl WithInputTypes<CachedCountPerBlockCumulativeRolling> {
})
}
pub(crate) fn write(&mut self) -> Result<()> {
self.try_for_each_type_mut(|vec| vec.write())
pub(crate) fn cached_addr_type_counts(&self) -> ByAddrType<CachedBlockCountReader> {
ByAddrType::new(|filter| {
let Filter::Type(output_type) = filter else {
unreachable!()
};
self.by_type.get(output_type).cached_cumulative()
})
}
pub(crate) fn validate_and_truncate(
&mut self,
dependency_version: Version,
at_height: Height,
) -> Result<()> {
self.try_for_each_type_mut(|vec| vec.validate_and_truncate(dependency_version, at_height))
}
pub(crate) fn truncate_if_needed_at(&mut self, len: usize) -> Result<()> {
self.try_for_each_type_mut(|vec| vec.truncate_if_needed_at(len))
}
#[inline]
pub(crate) fn push_block(&mut self, per_type: &[u64; 12]) {
self.push_block_with(per_type, |vec, value| vec.push_block(value));
pub(crate) fn clear(&self) {
self.by_type.iter().for_each(|count| count.clear());
}
}
impl WithInputTypes<PerBlockCumulativeRolling<StoredU64>> {
pub(crate) fn forced_import<S>(
db: &Database,
impl WithInputTypes<LazyColumnPerBlockCumulativeRolling<StoredU64, SpendableTypeId>> {
pub(crate) fn from_columnar_source<S>(
all_name: &str,
per_type_name: impl Fn(&str) -> String,
version: Version,
all: (S, fn(Height, StoredU64) -> StoredU64),
source: &ReadOnlyColumnarVec<PcoVec<Height, StoredU64>, SpendableTypeId>,
indexes: &indexes::Vecs,
cached_starts: &Windows<&CachedWindowStartVec>,
) -> Result<Self>
) -> Self
where
S: TypedVec<I = Height, T = StoredU64>
+ ReadableVec<Height, StoredU64>
@@ -212,27 +174,21 @@ impl WithInputTypes<PerBlockCumulativeRolling<StoredU64>> {
+ Clone
+ 'static,
{
let by_type = SpendableType::try_new(|_, name| {
PerBlockCumulativeRolling::forced_import(
db,
let by_type = SpendableType::new(|filter, name| {
let Filter::Type(output_type) = filter else {
unreachable!()
};
LazyColumnPerBlockCumulativeRolling::new(
&per_type_name(name),
version,
source,
SpendableTypeId::from_output_type(output_type)
.expect("spendable output type column"),
indexes,
cached_starts,
)
})?;
Ok(Self::new(
all_name,
version,
all,
by_type,
indexes,
cached_starts,
))
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.min_stateful_len_with(|vec| vec.cumulative.height.len())
});
Self::new(all_name, version, all, by_type, indexes, cached_starts)
}
pub(crate) fn lazy_shares(
@@ -255,33 +211,4 @@ impl WithInputTypes<PerBlockCumulativeRolling<StoredU64>> {
)
})
}
pub(crate) fn write(&mut self) -> Result<()> {
self.try_for_each_type_mut(|vec| {
vec.cumulative.height.write()?;
Ok(())
})
}
pub(crate) fn validate_and_truncate(
&mut self,
dependency_version: Version,
at_height: Height,
) -> Result<()> {
self.try_for_each_type_mut(|vec| {
Ok(vec
.cumulative
.height
.validate_and_truncate(dependency_version, at_height)?)
})
}
pub(crate) fn truncate_if_needed_at(&mut self, len: usize) -> Result<()> {
self.try_for_each_type_mut(|vec| Ok(vec.cumulative.height.truncate_if_needed_at(len)?))
}
#[inline]
pub(crate) fn push_block(&mut self, per_type: &[u64; 12]) {
self.push_block_with(per_type, |vec, value| vec.push_block(value));
}
}
@@ -4,13 +4,13 @@
//! to the caller.
use brk_error::Result;
use brk_types::TxIndex;
use brk_types::{OutputType, TxIndex};
use vecdb::VecIndex;
/// Aggregated per-block counters produced by [`walk_blocks`].
pub(crate) struct BlockAggregate {
pub entries_per_type: [u64; 12],
pub txs_per_type: [u64; 12],
pub entries_per_type: [u64; OutputType::COUNT],
pub txs_per_type: [u64; OutputType::COUNT],
}
/// Whether to include the coinbase tx (first tx in each block) in the walk.
@@ -21,7 +21,7 @@ pub(crate) enum CoinbasePolicy {
}
/// Walk every block in `fi_batch`, calling `scan_tx` once per tx (which
/// fills a `[u32; 12]` with the per-output-type count for that tx),
/// fills a per-output-type count array for that tx),
/// aggregating into a [`BlockAggregate`] and handing it to `store`.
///
#[inline]
@@ -29,7 +29,7 @@ pub(crate) fn walk_blocks(
fi_batch: &[TxIndex],
txid_len: usize,
coinbase: CoinbasePolicy,
mut scan_tx: impl FnMut(usize, &mut [u32; 12]) -> Result<()>,
mut scan_tx: impl FnMut(usize, &mut [u32; OutputType::COUNT]) -> Result<()>,
mut store: impl FnMut(BlockAggregate) -> Result<()>,
) -> Result<()> {
for (j, first_tx) in fi_batch.iter().enumerate() {
@@ -44,11 +44,11 @@ pub(crate) fn walk_blocks(
CoinbasePolicy::Skip => fi + 1,
};
let mut entries_per_type = [0u64; 12];
let mut txs_per_type = [0u64; 12];
let mut entries_per_type = [0u64; OutputType::COUNT];
let mut txs_per_type = [0u64; OutputType::COUNT];
for tx_pos in start_tx..next_fi {
let mut per_tx = [0u32; 12];
let mut per_tx = [0u32; OutputType::COUNT];
scan_tx(tx_pos, &mut per_tx)?;
for (i, &n) in per_tx.iter().enumerate() {
if n > 0 {
@@ -1,4 +1,96 @@
use brk_traversable::Traversable;
use brk_types::Version;
use vecdb::{ColumnId, VecValue};
const WINDOW_COUNT: usize = 4;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum WindowId {
Day1,
Week1,
Month1,
Year1,
}
const WINDOW_IDS: [WindowId; WINDOW_COUNT] = [
WindowId::Day1,
WindowId::Week1,
WindowId::Month1,
WindowId::Year1,
];
impl WindowId {
pub const fn suffix(self) -> &'static str {
match self {
Self::Day1 => "24h",
Self::Week1 => "1w",
Self::Month1 => "1m",
Self::Year1 => "1y",
}
}
pub fn series<T>(mut create: impl FnMut(Self) -> T) -> Windows<T> {
Windows {
_24h: create(Self::Day1),
_1w: create(Self::Week1),
_1m: create(Self::Month1),
_1y: create(Self::Year1),
}
}
pub fn select<T>(self, windows: &Windows<T>) -> &T {
match self {
Self::Day1 => &windows._24h,
Self::Week1 => &windows._1w,
Self::Month1 => &windows._1m,
Self::Year1 => &windows._1y,
}
}
}
impl ColumnId for WindowId {
type Row<T>
= [T; WINDOW_COUNT]
where
T: VecValue;
const VERSION: Version = Version::ONE;
const ALL: &'static [Self] = &WINDOW_IDS;
#[inline]
fn index(self) -> usize {
self as usize
}
#[inline]
fn get<T: VecValue>(self, row: &Self::Row<T>) -> &T {
&row[self.index()]
}
#[inline]
fn get_mut<T: VecValue>(self, row: &mut Self::Row<T>) -> &mut T {
&mut row[self.index()]
}
#[inline]
fn from_fn<T, F>(mut create: F) -> Self::Row<T>
where
T: VecValue,
F: FnMut(Self) -> T,
{
std::array::from_fn(|index| create(WINDOW_IDS[index]))
}
#[inline]
fn map<T, U, F>(row: Self::Row<T>, create: F) -> Self::Row<U>
where
T: VecValue,
U: VecValue,
F: FnMut(T) -> U,
{
row.map(create)
}
}
#[derive(Clone, Copy, Traversable)]
pub struct Windows<A> {
@@ -79,3 +171,21 @@ impl<A, B> Windows<(A, B)> {
)
}
}
#[cfg(test)]
mod tests {
use vecdb::ColumnId;
use super::{WINDOW_IDS, WindowId};
#[test]
fn window_columns_match_named_fields() {
assert_eq!(WindowId::ALL, WINDOW_IDS);
let windows = WindowId::series(|window| window);
assert_eq!(windows._24h, WindowId::Day1);
assert_eq!(windows._1w, WindowId::Week1);
assert_eq!(windows._1m, WindowId::Month1);
assert_eq!(windows._1y, WindowId::Year1);
}
}
@@ -1,4 +1,101 @@
use brk_traversable::Traversable;
use brk_types::Version;
use vecdb::{ColumnId, VecValue};
use super::Windows;
const WINDOW_FROM_1W_COUNT: usize = 3;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum WindowFrom1wId {
Week1,
Month1,
Year1,
}
const WINDOW_FROM_1W_IDS: [WindowFrom1wId; WINDOW_FROM_1W_COUNT] = [
WindowFrom1wId::Week1,
WindowFrom1wId::Month1,
WindowFrom1wId::Year1,
];
impl WindowFrom1wId {
pub const fn suffix(self) -> &'static str {
match self {
Self::Week1 => "1w",
Self::Month1 => "1m",
Self::Year1 => "1y",
}
}
pub fn series<T>(mut create: impl FnMut(Self) -> T) -> WindowsFrom1w<T> {
WindowsFrom1w {
_1w: create(Self::Week1),
_1m: create(Self::Month1),
_1y: create(Self::Year1),
}
}
pub fn select<T>(self, windows: &WindowsFrom1w<T>) -> &T {
match self {
Self::Week1 => &windows._1w,
Self::Month1 => &windows._1m,
Self::Year1 => &windows._1y,
}
}
pub fn select_full<T>(self, windows: &Windows<T>) -> &T {
match self {
Self::Week1 => &windows._1w,
Self::Month1 => &windows._1m,
Self::Year1 => &windows._1y,
}
}
}
impl ColumnId for WindowFrom1wId {
type Row<T>
= [T; WINDOW_FROM_1W_COUNT]
where
T: VecValue;
const VERSION: Version = Version::ONE;
const ALL: &'static [Self] = &WINDOW_FROM_1W_IDS;
#[inline]
fn index(self) -> usize {
self as usize
}
#[inline]
fn get<T: VecValue>(self, row: &Self::Row<T>) -> &T {
&row[self.index()]
}
#[inline]
fn get_mut<T: VecValue>(self, row: &mut Self::Row<T>) -> &mut T {
&mut row[self.index()]
}
#[inline]
fn from_fn<T, F>(mut create: F) -> Self::Row<T>
where
T: VecValue,
F: FnMut(Self) -> T,
{
std::array::from_fn(|index| create(WINDOW_FROM_1W_IDS[index]))
}
#[inline]
fn map<T, U, F>(row: Self::Row<T>, create: F) -> Self::Row<U>
where
T: VecValue,
U: VecValue,
F: FnMut(T) -> U,
{
row.map(create)
}
}
#[derive(Clone, Traversable)]
pub struct WindowsFrom1w<A> {
@@ -28,3 +125,20 @@ impl<A> WindowsFrom1w<A> {
[&mut self._1w, &mut self._1m, &mut self._1y]
}
}
#[cfg(test)]
mod tests {
use vecdb::ColumnId;
use super::{WINDOW_FROM_1W_IDS, WindowFrom1wId};
#[test]
fn window_from_1w_columns_match_named_fields() {
assert_eq!(WindowFrom1wId::ALL, WINDOW_FROM_1W_IDS);
let windows = WindowFrom1wId::series(|window| window);
assert_eq!(windows._1w, WindowFrom1wId::Week1);
assert_eq!(windows._1m, WindowFrom1wId::Month1);
assert_eq!(windows._1y, WindowFrom1wId::Year1);
}
}
@@ -6,12 +6,14 @@ use brk_types::{
Date, Day1, Day3, Epoch, Halving, Height, Hour1, Hour4, Hour12, Minute10, Minute30, Month1,
Month3, Month6, Timestamp, Version, Week1, Year1, Year10,
};
use derive_more::{Deref, DerefMut};
use schemars::JsonSchema;
use serde::Serialize;
use vecdb::{
AnyExportableVec, AnyVec, Database, EagerVec, Formattable, ImportableVec, LazyVec, PcoVec,
PcoVecValue, ReadableBoxedVec, ReadableCloneableVec, ReadableVec, Rw, StorageMode, TypedVec,
UnaryTransform, VecIndex, VecValue, short_type_name,
AnyExportableVec, AnyStoredVec, AnyVec, ColumnId, ColumnarVec, Database, EagerVec, Formattable,
ImportableVec, LazyColumnVec, LazyVec, PcoVec, PcoVecValue, ReadOnlyClone, ReadOnlyColumnarVec,
ReadableBoxedVec, ReadableCloneableVec, ReadableColumnarVec, ReadableVec, Rw, StorageMode,
TypedVec, UnaryTransform, VecIndex, VecValue, WritableVec, short_type_name,
};
use crate::indexes;
@@ -172,6 +174,83 @@ where
}
}
#[derive(Clone, Traversable)]
#[traversable(merge)]
pub struct LazyColumnDailyMetric<T, C>
where
T: DailyValue + PcoVecValue,
C: ColumnId,
{
pub day1: LazyColumnVec<ReadOnlyColumnarVec<PcoVec<Day1, T>, C>, C>,
#[traversable(flatten)]
pub views: Box<DailyViews<T>>,
}
impl<T, C> LazyColumnDailyMetric<T, C>
where
T: DailyValue + PcoVecValue,
C: ColumnId,
{
pub(crate) fn new(
name: &str,
version: Version,
source: &ReadOnlyColumnarVec<PcoVec<Day1, T>, C>,
column: C,
mappings: &DailyMappings,
) -> Self {
let day1 = source.column(name, version, column);
let views = Box::new(DailyViews::new(
name,
day1.read_only_boxed_clone(),
version,
mappings,
));
Self { day1, views }
}
}
#[derive(Deref, DerefMut, Traversable)]
pub struct ColumnarDailyMetric<T, C, S: Clone, M: StorageMode = Rw>
where
T: DailyValue + PcoVecValue,
C: ColumnId,
{
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub series: S,
#[traversable(hidden)]
pub day1: M::Stored<EagerVec<ColumnarVec<PcoVec<Day1, T>, C>>>,
}
impl<T, C, S: Clone> ColumnarDailyMetric<T, C, S>
where
T: DailyValue + PcoVecValue,
C: ColumnId,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
build_series: impl FnOnce(&ReadOnlyColumnarVec<PcoVec<Day1, T>, C>) -> S,
) -> Result<Self> {
let day1 = EagerVec::forced_import(db, name, version)?;
let series = build_series(&day1.read_only_clone());
Ok(Self { series, day1 })
}
#[inline(always)]
pub(crate) fn push(&mut self, row: C::Row<T>) {
self.day1.push(row);
}
pub(crate) fn stored_mut(&mut self) -> &mut dyn AnyStoredVec {
&mut self.day1
}
}
type LazyDay<T, S> = LazyVec<Day1, T, Day1, S>;
#[derive(Clone, Traversable)]
@@ -12,7 +12,6 @@ use crate::internal::{ComputedVecValue, PerResolution};
#[derive(Clone, Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct LazyIndexes<T, S>(
#[allow(clippy::type_complexity)]
pub PerResolution<
LazyVec<Minute10, T, Minute10, S>,
LazyVec<Minute30, T, Minute30, S>,
@@ -1,217 +0,0 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, StoredU16, StoredU64, Version};
use vecdb::{
AnyStoredVec, AnyVec, CachedVec, Database, EagerVec, ImportableVec, LazyVec, PcoVec, Rw,
StorageMode, WritableVec,
};
use crate::{
indexes,
internal::{
CachedBlockCountReader, CachedWindowStartVec, Identity, LazyPerBlock,
LazyRollingAvgsFromHeight, LazyRollingSumsFromHeight, StoredU16ToStoredU64, Windows,
},
};
#[derive(Traversable)]
pub struct CachedCountPerBlockCumulativeRolling<M: StorageMode = Rw> {
pub block: LazyVec<Height, StoredU64, Height, StoredU16>,
pub cumulative: LazyPerBlock<StoredU64>,
pub sum: LazyRollingSumsFromHeight<StoredU64>,
pub average: LazyRollingAvgsFromHeight<StoredU64>,
#[traversable(hidden)]
source: CachedVec<M::Stored<EagerVec<PcoVec<Height, StoredU16>>>>,
#[traversable(skip)]
cached_cumulative: CachedBlockCountReader,
}
impl CachedCountPerBlockCumulativeRolling {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &Windows<&CachedWindowStartVec>,
) -> Result<Self> {
let source = CachedVec::wrap(EagerVec::forced_import(db, name, version)?);
let cached_cumulative = CachedBlockCountReader::new(source.read_only_cached_boxed_clone());
let block = LazyVec::transformed::<StoredU16ToStoredU64>(
name,
version,
source.read_only_boxed_clone(),
);
let cumulative = LazyPerBlock::from_uncached_height_source::<Identity<StoredU64>, _>(
&format!("{name}_cumulative"),
version,
cached_cumulative.clone(),
indexes,
);
let sum = LazyRollingSumsFromHeight::new_uncached(
&format!("{name}_sum"),
version,
&cached_cumulative,
cached_starts,
indexes,
);
let average = LazyRollingAvgsFromHeight::new(
&format!("{name}_average"),
version,
&cached_cumulative,
cached_starts,
indexes,
);
Ok(Self {
block,
cumulative,
sum,
average,
source,
cached_cumulative,
})
}
#[inline(always)]
pub(crate) fn cached_cumulative(&self) -> CachedBlockCountReader {
self.cached_cumulative.clone()
}
#[inline(always)]
pub(crate) fn min_stateful_len(&self) -> usize {
self.source.len()
}
#[inline(always)]
pub(crate) fn push_block(&mut self, value: StoredU64) {
let value = u64::from(value);
debug_assert!(u16::try_from(value).is_ok());
self.source.inner.push(StoredU16::new(value as u16));
}
pub(crate) fn validate_and_truncate(
&mut self,
dependency_version: Version,
at_height: Height,
) -> Result<()> {
self.source
.inner
.validate_and_truncate(dependency_version, at_height)?;
self.source.clear();
Ok(())
}
pub(crate) fn truncate_if_needed_at(&mut self, len: usize) -> Result<()> {
self.source.inner.truncate_if_needed_at(len)?;
self.source.clear();
Ok(())
}
pub(crate) fn write(&mut self) -> Result<()> {
self.source.inner.write()?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::time::{SystemTime, UNIX_EPOCH};
use brk_indexer::Indexer;
use brk_reader::Reader;
use brk_rpc::{Auth, Client};
use brk_traversable::Traversable;
use vecdb::{AnyVec, ReadableVec};
use super::*;
use crate::blocks::LookbackVecs;
#[test]
fn owns_retains_and_refreshes_its_compact_source() {
let suffix = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = std::env::temp_dir().join(format!(
"brk-cached-count-owner-{}-{suffix}",
std::process::id()
));
let client = Client::new("http://127.0.0.1:1", Auth::None).unwrap();
let reader = Reader::new_without_rlimit(path.join("blocks"), &client);
let indexer = Indexer::import(&path, &reader).unwrap();
let indexes = indexes::Vecs::forced_import(&path, Version::ONE, &indexer).unwrap();
let lookback = LookbackVecs::new(
Version::ONE,
indexes.timestamp.monotonic.read_only_cached_boxed_clone(),
);
let cached_starts = lookback.cached_window_starts();
let db = Database::open(&path.join("counts")).unwrap();
let mut count = CachedCountPerBlockCumulativeRolling::forced_import(
&db,
"count",
Version::ONE,
&indexes,
&cached_starts,
)
.unwrap();
count
.validate_and_truncate(Version::ONE, Height::ZERO)
.unwrap();
for value in [1_u64, 2, 3] {
count.push_block(StoredU64::from(value));
}
count.write().unwrap();
assert_eq!(
count.block.collect_range_at(0, 3),
[1_u64, 2, 3].map(StoredU64::from)
);
assert_eq!(
count.cumulative.height.collect_range_at(0, 3),
[1_u64, 3, 6].map(StoredU64::from)
);
let source_regions = count.source.region_names();
assert!(!source_regions.is_empty());
let exportable_regions = count
.iter_any_exportable()
.flat_map(AnyVec::region_names)
.collect::<Vec<_>>();
assert!(
source_regions
.iter()
.all(|region| exportable_regions.contains(region))
);
assert!(
count
.iter_any_visible()
.flat_map(AnyVec::region_names)
.next()
.is_none()
);
count.truncate_if_needed_at(0).unwrap();
for value in [4_u64, 5, 6] {
count.push_block(StoredU64::from(value));
}
count.write().unwrap();
assert_eq!(
count.block.collect_range_at(0, 3),
[4_u64, 5, 6].map(StoredU64::from)
);
assert_eq!(
count.cumulative.height.collect_range_at(0, 3),
[4_u64, 9, 15].map(StoredU64::from)
);
drop(count);
drop(db);
drop(lookback);
drop(indexes);
drop(indexer);
std::fs::remove_dir_all(path).unwrap();
}
}
@@ -0,0 +1,363 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{
AnyStoredVec, AnyVec, ColumnId, ColumnarVec, Database, EagerVec, Exit, ImportableVec, PcoVec,
PcoVecValue, ReadOnlyClone, ReadOnlyColumnarVec, ReadableVec, Rw, StorageMode, VecValue,
WritableVec,
};
#[derive(Deref, DerefMut, Traversable)]
pub struct ColumnarPerBlock<T, C, S: Clone, M: StorageMode = Rw>
where
T: PcoVecValue,
C: ColumnId,
{
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub series: S,
pub height: M::Stored<EagerVec<ColumnarVec<PcoVec<Height, T>, C>>>,
}
impl<T, C, S: Clone> ColumnarPerBlock<T, C, S>
where
T: PcoVecValue,
C: ColumnId,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
build_series: impl FnOnce(&ReadOnlyColumnarVec<PcoVec<Height, T>, C>) -> S,
) -> Result<Self> {
let height = EagerVec::forced_import(db, name, version)?;
let series = build_series(&height.read_only_clone());
Ok(Self { series, height })
}
#[inline(always)]
pub(crate) fn push(&mut self, row: C::Row<T>) {
self.height.push(row);
}
pub(crate) fn validate_and_truncate(&mut self, version: Version, height: Height) -> Result<()> {
self.height
.validate_and_truncate(version, height)
.map_err(Into::into)
}
pub(crate) fn validate_computed_version_or_reset(&mut self, version: Version) -> Result<()> {
self.height
.validate_computed_version_or_reset(version)
.map_err(Into::into)
}
pub(crate) fn truncate_if_needed_at(&mut self, len: usize) -> Result<()> {
self.height.truncate_if_needed_at(len).map_err(Into::into)
}
pub(crate) fn write(&mut self) -> Result<()> {
self.height.write().map(|_| ()).map_err(Into::into)
}
pub(crate) fn stored_mut(&mut self) -> &mut dyn AnyStoredVec {
&mut self.height
}
/// Computes one stored matrix from two scalar sources per column.
pub(crate) fn compute_columns2<'a, A, B, V1, V2>(
&mut self,
max_from: Height,
source1: impl Fn(C) -> &'a V1,
source2: impl Fn(C) -> &'a V2,
mut transform: impl FnMut(C, A, B) -> T,
exit: &Exit,
) -> Result<()>
where
A: VecValue,
B: VecValue,
V1: ReadableVec<Height, A> + 'a,
V2: ReadableVec<Height, B> + 'a,
{
let dependency_version = C::ALL
.iter()
.flat_map(|&column| [source1(column).version(), source2(column).version()])
.sum();
let source_end = C::ALL
.iter()
.flat_map(|&column| [source1(column).len(), source2(column).len()])
.min()
.unwrap_or_default();
self.height
.validate_computed_version_or_reset(dependency_version)?;
self.height.truncate_if_needed(max_from)?;
self.height.repeat_until_complete(exit, |target| {
let start = target.len();
let end = target.batch_end(source_end);
if start >= end {
return Ok(());
}
let source1_batches: Vec<_> = C::ALL
.iter()
.map(|&column| source1(column).collect_range_at(start, end))
.collect();
let source2_batches: Vec<_> = C::ALL
.iter()
.map(|&column| source2(column).collect_range_at(start, end))
.collect();
for offset in 0..(end - start) {
target.push(C::from_fn(|column| {
transform(
column,
source1_batches[column.index()][offset].clone(),
source2_batches[column.index()][offset].clone(),
)
}));
}
Ok(())
})?;
Ok(())
}
/// Computes one stored matrix from a matrix source and one scalar source per column.
pub(crate) fn compute_matrix_columns2<'a, A, B, V1, V2>(
&mut self,
max_from: Height,
source1: &V1,
source2: impl Fn(C) -> &'a V2,
mut transform: impl FnMut(C, A, B) -> T,
exit: &Exit,
) -> Result<()>
where
A: VecValue,
B: VecValue,
V1: ReadableVec<Height, C::Row<A>>,
V2: ReadableVec<Height, B> + 'a,
{
let dependency_version = C::ALL
.iter()
.map(|&column| source2(column).version())
.sum::<Version>()
+ source1.version();
let source_end = C::ALL
.iter()
.map(|&column| source2(column).len())
.chain(std::iter::once(source1.len()))
.min()
.unwrap_or_default();
self.height
.validate_computed_version_or_reset(dependency_version)?;
self.height.truncate_if_needed(max_from)?;
self.height.repeat_until_complete(exit, |target| {
let start = target.len();
let end = target.batch_end(source_end);
if start >= end {
return Ok(());
}
let source1_rows = source1.collect_range_at(start, end);
let source2_batches: Vec<_> = C::ALL
.iter()
.map(|&column| source2(column).collect_range_at(start, end))
.collect();
for (offset, source1_row) in source1_rows.into_iter().enumerate() {
target.push(C::from_fn(|column| {
transform(
column,
column.get(&source1_row).clone(),
source2_batches[column.index()][offset].clone(),
)
}));
}
Ok(())
})?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use brk_types::{Height, StoredU64, Version};
use vecdb::{
AnyStoredVec, ColumnId, Database, EagerVec, Exit, ImportableVec, PcoVec, ReadOnlyClone,
ReadableVec, VecValue, WritableVec,
};
use super::ColumnarPerBlock;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum Column {
A,
B,
}
impl ColumnId for Column {
type Row<T>
= [T; 2]
where
T: VecValue;
const VERSION: Version = Version::ONE;
const ALL: &'static [Self] = &[Self::A, Self::B];
fn index(self) -> usize {
self as usize
}
fn get<T: VecValue>(self, row: &Self::Row<T>) -> &T {
&row[self.index()]
}
fn get_mut<T: VecValue>(self, row: &mut Self::Row<T>) -> &mut T {
&mut row[self.index()]
}
fn from_fn<T, F>(mut f: F) -> Self::Row<T>
where
T: VecValue,
F: FnMut(Self) -> T,
{
[f(Self::A), f(Self::B)]
}
fn map<T, U, F>(row: Self::Row<T>, mut f: F) -> Self::Row<U>
where
T: VecValue,
U: VecValue,
F: FnMut(T) -> U,
{
let [a, b] = row;
[f(a), f(b)]
}
}
#[test]
fn computes_from_scalar_columns_and_matrix_rows() {
let suffix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = std::env::temp_dir().join(format!(
"brk-columnar-compute-{}-{suffix}",
std::process::id()
));
let db = Database::open(&path).unwrap();
let mut left_a: EagerVec<PcoVec<Height, StoredU64>> =
EagerVec::forced_import(&db, "left_a", Version::ONE).unwrap();
let mut left_b: EagerVec<PcoVec<Height, StoredU64>> =
EagerVec::forced_import(&db, "left_b", Version::ONE).unwrap();
let mut right_a: EagerVec<PcoVec<Height, StoredU64>> =
EagerVec::forced_import(&db, "right_a", Version::ONE).unwrap();
let mut right_b: EagerVec<PcoVec<Height, StoredU64>> =
EagerVec::forced_import(&db, "right_b", Version::ONE).unwrap();
for value in [1_u64, 2, 3] {
left_a.push(value.into());
}
for value in [4_u64, 5] {
left_b.push(value.into());
}
for value in [10_u64, 20, 30] {
right_a.push(value.into());
}
for value in [40_u64, 50, 60] {
right_b.push(value.into());
}
left_a.write().unwrap();
left_b.write().unwrap();
right_a.write().unwrap();
right_b.write().unwrap();
let mut sums = ColumnarPerBlock::<StoredU64, Column, _>::forced_import(
&db,
"sums",
Version::ONE,
|_| (),
)
.unwrap();
sums.compute_columns2(
Height::ZERO,
|column| match column {
Column::A => &left_a,
Column::B => &left_b,
},
|column| match column {
Column::A => &right_a,
Column::B => &right_b,
},
|_, left, right| StoredU64::from(u64::from(left) + u64::from(right)),
&Exit::new(),
)
.unwrap();
assert_eq!(
sums.height.collect_range_at(0, 3),
[
[StoredU64::from(11_u64), StoredU64::from(44_u64)],
[StoredU64::from(22_u64), StoredU64::from(55_u64)],
]
);
let mut factor_a: EagerVec<PcoVec<Height, StoredU64>> =
EagerVec::forced_import(&db, "factor_a", Version::ONE).unwrap();
let mut factor_b: EagerVec<PcoVec<Height, StoredU64>> =
EagerVec::forced_import(&db, "factor_b", Version::ONE).unwrap();
for value in [2_u64, 3] {
factor_a.push(value.into());
}
for value in [5_u64, 7] {
factor_b.push(value.into());
}
factor_a.write().unwrap();
factor_b.write().unwrap();
let sums = sums.height.read_only_clone();
let mut products = ColumnarPerBlock::<StoredU64, Column, _>::forced_import(
&db,
"products",
Version::ONE,
|_| (),
)
.unwrap();
products
.compute_matrix_columns2(
Height::ZERO,
&sums,
|column| match column {
Column::A => &factor_a,
Column::B => &factor_b,
},
|_, value, factor| StoredU64::from(u64::from(value) * u64::from(factor)),
&Exit::new(),
)
.unwrap();
assert_eq!(
products.height.collect_range_at(0, 3),
[
[StoredU64::from(22_u64), StoredU64::from(220_u64)],
[StoredU64::from(66_u64), StoredU64::from(385_u64)],
]
);
drop(products);
drop(sums);
drop(factor_b);
drop(factor_a);
drop(right_b);
drop(right_a);
drop(left_b);
drop(left_a);
drop(db);
std::fs::remove_dir_all(path).unwrap();
}
}
@@ -0,0 +1,277 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Height, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{
AnyStoredVec, AnyVec, ColumnId, ColumnarVec, Database, EagerVec, Exit, ImportableVec, PcoVec,
ReadOnlyClone, ReadOnlyColumnarVec, ReadableVec, Rw, StorageMode, VecValue, WritableVec,
};
use crate::internal::NumericValue;
#[derive(Deref, DerefMut, Traversable)]
pub struct ColumnarPerBlockCumulativeRolling<T, C, S: Clone, M: StorageMode = Rw>
where
T: NumericValue,
C: ColumnId,
{
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub series: S,
pub cumulative: M::Stored<EagerVec<ColumnarVec<PcoVec<Height, T>, C>>>,
#[traversable(skip)]
last_cumulative: Option<(usize, C::Row<T>)>,
}
impl<T, C, S: Clone> ColumnarPerBlockCumulativeRolling<T, C, S>
where
T: NumericValue,
C: ColumnId,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
build_series: impl FnOnce(&ReadOnlyColumnarVec<PcoVec<Height, T>, C>) -> S,
) -> Result<Self> {
let cumulative = EagerVec::forced_import(db, name, version)?;
let last_cumulative = cumulative.collect_last().map(|row| (cumulative.len(), row));
let series = build_series(&cumulative.read_only_clone());
Ok(Self {
series,
cumulative,
last_cumulative,
})
}
#[inline(always)]
pub(crate) fn push_block(&mut self, row: C::Row<T>) {
let len = self.cumulative.len();
let mut cumulative = match &self.last_cumulative {
Some((cached_len, row)) if *cached_len == len => row.clone(),
_ => self
.cumulative
.collect_last()
.unwrap_or_else(|| C::from_fn(|_| T::default())),
};
for &column in C::ALL {
*column.get_mut(&mut cumulative) += *column.get(&row);
}
self.cumulative.push(cumulative.clone());
self.last_cumulative = Some((len + 1, cumulative));
}
pub(crate) fn reset(&mut self) -> Result<()> {
self.last_cumulative = None;
self.cumulative.reset().map_err(Into::into)
}
pub(crate) fn stored_mut(&mut self) -> &mut dyn AnyStoredVec {
self.last_cumulative = None;
&mut self.cumulative
}
pub(crate) fn validate_and_truncate(&mut self, version: Version, height: Height) -> Result<()> {
self.cumulative
.validate_and_truncate(version, height)
.map_err(Into::into)
}
pub(crate) fn truncate_if_needed_at(&mut self, len: usize) -> Result<()> {
self.cumulative
.truncate_if_needed_at(len)
.map_err(Into::into)
}
pub(crate) fn write(&mut self) -> Result<()> {
self.cumulative.write().map(|_| ()).map_err(Into::into)
}
/// Computes one cumulative matrix from one scalar per-block source per column.
pub(crate) fn compute_columns<'a, V, U>(
&mut self,
max_from: Height,
source: impl Fn(C) -> &'a V,
exit: &Exit,
) -> Result<()>
where
V: ReadableVec<Height, U> + 'a,
U: VecValue + Into<T>,
{
let dependency_version = C::ALL.iter().map(|&column| source(column).version()).sum();
let source_end = C::ALL
.iter()
.map(|&column| source(column).len())
.min()
.unwrap_or_default();
self.cumulative
.validate_computed_version_or_reset(dependency_version)?;
self.cumulative.truncate_if_needed(max_from)?;
self.last_cumulative = None;
let mut last_cumulative = None;
self.cumulative.repeat_until_complete(exit, |target| {
let start = target.len();
let end = target.batch_end(source_end);
if start >= end {
return Ok(());
}
let mut batches: Vec<_> = C::ALL
.iter()
.map(|&column| source(column).collect_range_at(start, end).into_iter())
.collect();
let mut cumulative = last_cumulative.take().unwrap_or_else(|| {
target
.collect_last()
.unwrap_or_else(|| C::from_fn(|_| T::default()))
});
for _ in start..end {
for (&column, batch) in C::ALL.iter().zip(&mut batches) {
*column.get_mut(&mut cumulative) +=
batch.next().expect("complete source batch").into();
}
target.push(cumulative.clone());
}
last_cumulative = Some(cumulative);
Ok(())
})?;
let len = self.cumulative.len();
self.last_cumulative = last_cumulative
.or_else(|| self.cumulative.collect_last())
.map(|row| (len, row));
Ok(())
}
}
#[cfg(test)]
mod tests {
use brk_types::{Height, StoredU64, Version};
use vecdb::{
AnyStoredVec, ColumnId, Database, EagerVec, Exit, ImportableVec, PcoVec, ReadableVec,
VecValue, WritableVec,
};
use super::ColumnarPerBlockCumulativeRolling;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum Column {
A,
B,
}
impl ColumnId for Column {
type Row<T>
= [T; 2]
where
T: VecValue;
const VERSION: Version = Version::ONE;
const ALL: &'static [Self] = &[Self::A, Self::B];
fn index(self) -> usize {
self as usize
}
fn get<T: VecValue>(self, row: &Self::Row<T>) -> &T {
&row[self.index()]
}
fn get_mut<T: VecValue>(self, row: &mut Self::Row<T>) -> &mut T {
&mut row[self.index()]
}
fn from_fn<T, F>(mut f: F) -> Self::Row<T>
where
T: VecValue,
F: FnMut(Self) -> T,
{
[f(Self::A), f(Self::B)]
}
fn map<T, U, F>(row: Self::Row<T>, mut f: F) -> Self::Row<U>
where
T: VecValue,
U: VecValue,
F: FnMut(T) -> U,
{
let [a, b] = row;
[f(a), f(b)]
}
}
#[test]
fn pushes_delta_rows_and_recovers_after_truncation() {
let suffix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = std::env::temp_dir().join(format!(
"brk-columnar-cumulative-{}-{suffix}",
std::process::id()
));
let db = Database::open(&path).unwrap();
let mut vec = ColumnarPerBlockCumulativeRolling::<StoredU64, Column, _>::forced_import(
&db,
"values",
Version::ONE,
|_| (),
)
.unwrap();
vec.push_block([1_u64.into(), 2_u64.into()]);
vec.push_block([3_u64.into(), 4_u64.into()]);
assert_eq!(
vec.cumulative.collect_range_at(0, 2),
[[1_u64.into(), 2_u64.into()], [4_u64.into(), 6_u64.into()]]
);
vec.truncate_if_needed_at(1).unwrap();
vec.push_block([10_u64.into(), 20_u64.into()]);
assert_eq!(
vec.cumulative.collect_one(Height::new(1)),
Some([11_u64.into(), 22_u64.into()])
);
vec.write().unwrap();
let mut a: EagerVec<PcoVec<Height, StoredU64>> =
EagerVec::forced_import(&db, "a", Version::ONE).unwrap();
let mut b: EagerVec<PcoVec<Height, StoredU64>> =
EagerVec::forced_import(&db, "b", Version::ONE).unwrap();
for value in [1_u64, 3] {
a.push(value.into());
}
for value in [2_u64, 4] {
b.push(value.into());
}
a.write().unwrap();
b.write().unwrap();
vec.compute_columns(
Height::ZERO,
|column| match column {
Column::A => &a,
Column::B => &b,
},
&Exit::new(),
)
.unwrap();
assert_eq!(
vec.cumulative.collect_range_at(0, 2),
[
[StoredU64::from(1_u64), StoredU64::from(2_u64)],
[StoredU64::from(4_u64), StoredU64::from(6_u64)],
]
);
drop(vec);
drop(b);
drop(a);
drop(db);
std::fs::remove_dir_all(path).unwrap();
}
}
@@ -0,0 +1,47 @@
use brk_traversable::Traversable;
use brk_types::{Height, Version};
use derive_more::{Deref, DerefMut};
use schemars::JsonSchema;
use serde::Serialize;
use vecdb::{
ColumnId, Formattable, LazyColumnVec, PcoVec, PcoVecValue, ReadOnlyColumnarVec,
ReadableColumnarVec,
};
use crate::{indexes, internal::Resolutions};
#[derive(Clone, Deref, DerefMut, Traversable)]
#[traversable(merge)]
pub struct LazyColumnPerBlock<T, C>
where
T: PcoVecValue + Formattable + PartialOrd + Serialize + JsonSchema,
C: ColumnId,
{
pub height: LazyColumnVec<ReadOnlyColumnarVec<PcoVec<Height, T>, C>, C>,
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub resolutions: Box<Resolutions<T>>,
}
impl<T, C> LazyColumnPerBlock<T, C>
where
T: PcoVecValue + Formattable + PartialOrd + Serialize + JsonSchema + 'static,
C: ColumnId,
{
pub(crate) fn new(
name: &str,
version: Version,
source: &ReadOnlyColumnarVec<PcoVec<Height, T>, C>,
column: C,
indexes: &indexes::Vecs,
) -> Self {
let height = source.column(name, version, column);
let resolutions = Resolutions::forced_import(name, height.clone(), version, indexes);
Self {
height,
resolutions: Box::new(resolutions),
}
}
}
@@ -0,0 +1,79 @@
use brk_traversable::Traversable;
use brk_types::{Height, StoredU16, StoredU64, Version};
use vecdb::{
CachedReadableVec, CachedVec, ColumnId, LazyVec, PcoVec, ReadOnlyColumnarVec,
ReadableColumnarVec,
};
use crate::{
indexes,
internal::{
CachedBlockCountReader, CachedWindowStartVec, Identity, LazyPerBlock,
LazyRollingAvgsFromHeight, LazyRollingSumsFromHeight, StoredU16ToStoredU64, Windows,
},
};
#[derive(Clone, Traversable)]
pub struct LazyColumnCountPerBlockCumulativeRolling {
pub block: LazyVec<Height, StoredU64, Height, StoredU16>,
pub cumulative: LazyPerBlock<StoredU64>,
pub sum: LazyRollingSumsFromHeight<StoredU64>,
pub average: LazyRollingAvgsFromHeight<StoredU64>,
#[traversable(skip)]
cached_cumulative: CachedBlockCountReader,
}
impl LazyColumnCountPerBlockCumulativeRolling {
pub(crate) fn new<C>(
name: &str,
version: Version,
source: &ReadOnlyColumnarVec<PcoVec<Height, StoredU16>, C>,
column: C,
indexes: &indexes::Vecs,
cached_starts: &Windows<&CachedWindowStartVec>,
) -> Self
where
C: ColumnId,
{
let column = CachedVec::wrap(source.column(name, version, column));
let cached_cumulative = CachedBlockCountReader::new(column.cached_boxed_clone());
let block = LazyVec::transformed::<StoredU16ToStoredU64>(name, version, Box::new(column));
let cumulative = LazyPerBlock::from_uncached_height_source::<Identity<StoredU64>, _>(
&format!("{name}_cumulative"),
version,
cached_cumulative.clone(),
indexes,
);
let sum = LazyRollingSumsFromHeight::new_uncached(
&format!("{name}_sum"),
version,
&cached_cumulative,
cached_starts,
indexes,
);
let average = LazyRollingAvgsFromHeight::new(
&format!("{name}_average"),
version,
&cached_cumulative,
cached_starts,
indexes,
);
Self {
block,
cumulative,
sum,
average,
cached_cumulative,
}
}
#[inline(always)]
pub(crate) fn cached_cumulative(&self) -> CachedBlockCountReader {
self.cached_cumulative.clone()
}
pub(crate) fn clear(&self) {
self.cached_cumulative.clear();
}
}
@@ -0,0 +1,57 @@
use brk_traversable::Traversable;
use brk_types::{Height, Version};
use schemars::JsonSchema;
use vecdb::{ColumnId, PcoVec, ReadOnlyColumnarVec};
use super::{super::lazy_cumulative_rolling::lazy_parts, LazyColumnPerBlock};
use crate::{
indexes,
internal::{
CachedWindowStartVec, LazyPreviousDeltaVec, LazyRollingAvgsFromHeight,
LazyRollingSumsFromHeight, NumericValue, Windows,
},
};
#[derive(Clone, Traversable)]
pub struct LazyColumnPerBlockCumulativeRolling<T, C>
where
T: NumericValue + JsonSchema,
C: ColumnId,
{
pub block: LazyPreviousDeltaVec<Height, T>,
pub cumulative: LazyColumnPerBlock<T, C>,
pub sum: LazyRollingSumsFromHeight<T>,
pub average: LazyRollingAvgsFromHeight<T>,
}
impl<T, C> LazyColumnPerBlockCumulativeRolling<T, C>
where
T: NumericValue + JsonSchema,
C: ColumnId,
{
pub(crate) fn new(
name: &str,
version: Version,
source: &ReadOnlyColumnarVec<PcoVec<Height, T>, C>,
column: C,
indexes: &indexes::Vecs,
cached_starts: &Windows<&CachedWindowStartVec>,
) -> Self {
let cumulative = LazyColumnPerBlock::new(
&format!("{name}_cumulative"),
version,
source,
column,
indexes,
);
let (block, sum, average) =
lazy_parts(name, version, &cumulative.height, cached_starts, indexes);
Self {
block,
cumulative,
sum,
average,
}
}
}
@@ -0,0 +1,11 @@
mod base;
mod cumulative_rolling;
mod lazy;
mod lazy_count_cumulative_rolling;
mod lazy_cumulative_rolling;
pub use base::*;
pub use cumulative_rolling::*;
pub use lazy::*;
pub use lazy_count_cumulative_rolling::*;
pub use lazy_cumulative_rolling::*;
@@ -78,22 +78,6 @@ where
self.last_cumulative = Some((len + 1, cumulative));
}
pub(crate) fn compute_cumulative<S>(
&mut self,
max_from: Height,
source: &impl ReadableVec<Height, S>,
exit: &Exit,
) -> Result<()>
where
S: VecValue + Into<T>,
T: Copy,
{
Ok(self
.cumulative
.height
.compute_cumulative(max_from, source, exit)?)
}
pub(crate) fn compute_cumulative_transformed<S>(
&mut self,
max_from: Height,
@@ -0,0 +1,69 @@
use brk_traversable::Traversable;
use brk_types::{Height, Version};
use schemars::JsonSchema;
use vecdb::{Ident, ReadableCloneableVec, UnaryTransform};
use crate::{
indexes,
internal::{
CachedWindowStartVec, LazyPreviousDeltaVec, LazyRollingAvgsFromHeight, NumericValue,
Windows,
},
};
/// Lazy exact per-block values and rolling averages backed by one cumulative source.
#[derive(Traversable)]
pub struct LazyPerBlockCumulativeAverage<T, C = T, F = Ident>
where
T: NumericValue + JsonSchema,
C: NumericValue + JsonSchema,
F: UnaryTransform<C, T>,
{
pub block: LazyPreviousDeltaVec<Height, C, T, F>,
#[traversable(flatten)]
pub average: LazyRollingAvgsFromHeight<C>,
}
impl<T, C, F> Clone for LazyPerBlockCumulativeAverage<T, C, F>
where
T: NumericValue + JsonSchema,
C: NumericValue + JsonSchema,
F: UnaryTransform<C, T>,
{
fn clone(&self) -> Self {
Self {
block: self.block.clone(),
average: self.average.clone(),
}
}
}
impl<T, C, F> LazyPerBlockCumulativeAverage<T, C, F>
where
T: NumericValue + JsonSchema,
C: NumericValue + JsonSchema,
F: UnaryTransform<C, T>,
{
pub(crate) fn new(
name: &str,
version: Version,
cumulative: &(impl ReadableCloneableVec<Height, C> + 'static),
indexes: &indexes::Vecs,
cached_starts: &Windows<&CachedWindowStartVec>,
) -> Self {
Self {
block: LazyPreviousDeltaVec::transformed(
name,
version,
cumulative.read_only_boxed_clone(),
),
average: LazyRollingAvgsFromHeight::new(
&format!("{name}_average"),
version + Version::TWO,
cumulative,
cached_starts,
indexes,
),
}
}
}
@@ -3,13 +3,13 @@
use brk_traversable::Traversable;
use brk_types::{Height, Version};
use schemars::JsonSchema;
use vecdb::{ReadableCloneableVec, ReadableVec, TypedVec, VecValue};
use vecdb::{ColumnId, ReadableCloneableVec, ReadableVec, TypedVec, VecValue};
use crate::{
indexes,
internal::{
CachedWindowStartVec, Identity, LazyPerBlock, LazyPreviousDeltaVec,
LazyRollingAvgsFromHeight, LazyRollingSumsFromHeight, NumericValue, PerBlock, Windows,
CachedWindowStartVec, Identity, LazyColumnPerBlock, LazyPerBlock, LazyPreviousDeltaVec,
LazyRollingAvgsFromHeight, LazyRollingSumsFromHeight, NumericValue, Windows,
},
};
@@ -99,18 +99,34 @@ where
Self::from_cumulative(name, version, cumulative, cached_starts, indexes)
}
pub(crate) fn from_source(
pub(crate) fn from_lazy_source(
name: &str,
version: Version,
source: &PerBlock<T>,
source: &LazyPerBlock<T>,
cached_starts: &Windows<&CachedWindowStartVec>,
indexes: &indexes::Vecs,
) -> Self {
let cumulative = LazyPerBlock::from_computed::<Identity<T>>(
let cumulative = LazyPerBlock::from_lazy::<Identity<T>, T>(
&format!("{name}_cumulative"),
version,
source,
);
Self::from_cumulative(name, version, cumulative, cached_starts, indexes)
}
pub(crate) fn from_column_source<C: ColumnId>(
name: &str,
version: Version,
source: &LazyColumnPerBlock<T, C>,
cached_starts: &Windows<&CachedWindowStartVec>,
indexes: &indexes::Vecs,
) -> Self {
let cumulative = LazyPerBlock::from_resolutions::<Identity<T>>(
&format!("{name}_cumulative"),
version,
source.height.read_only_boxed_clone(),
source,
&source.resolutions,
);
Self::from_cumulative(name, version, cumulative, cached_starts, indexes)
@@ -1,13 +1,14 @@
mod aggregated;
mod base;
mod cached;
mod cached_count_cumulative_rolling;
mod cached_cumulative_rolling;
mod cached_rolling;
mod columnar;
mod cumulative_rolling;
mod distribution;
mod full;
mod full_from_cumulative;
mod lazy_cumulative_average;
mod lazy_cumulative_rolling;
mod lazy_distribution;
mod lazy_rolling;
@@ -18,13 +19,14 @@ mod with_deltas;
pub use aggregated::*;
pub use base::*;
pub use cached::*;
pub use cached_count_cumulative_rolling::*;
pub use cached_cumulative_rolling::*;
pub use cached_rolling::*;
pub use columnar::*;
pub use cumulative_rolling::*;
pub use distribution::*;
pub use full::*;
pub use full_from_cumulative::*;
pub use lazy_cumulative_average::*;
pub use lazy_cumulative_rolling::*;
pub use lazy_distribution::*;
pub use lazy_rolling::*;
@@ -66,7 +66,6 @@ where
#[derive(Clone, Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct Resolutions<T>(
#[allow(clippy::type_complexity)]
pub PerResolution<
LazyAggVec<Minute10, Option<T>, Height, Height, T>,
LazyAggVec<Minute30, Option<T>, Height, Height, T>,
@@ -30,6 +30,10 @@ impl CachedBlockCountReader {
}
}
pub(crate) fn clear(&self) {
self.block.clear();
}
pub fn cumulative_at(&self, index: usize) -> Option<StoredU64> {
let (block, checkpoints) = self.snapshot();
(index < block.len())
@@ -17,7 +17,6 @@ use super::{LazyTransformLast, MapOption};
#[derive(Clone, Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct DerivedResolutions<T, S1T = T>(
#[allow(clippy::type_complexity)]
pub PerResolution<
LazyTransformLast<Minute10, Option<T>, Option<S1T>>,
LazyTransformLast<Minute30, Option<T>, Option<S1T>>,
@@ -0,0 +1,52 @@
use brk_traversable::Traversable;
use brk_types::{Height, StoredF32, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{ColumnId, PcoVec, ReadOnlyColumnarVec, ReadableCloneableVec};
use crate::{
indexes,
internal::{FixedRatio, LazyColumnPerBlock, LazyPerBlock, Percent},
};
/// Fixed-point column projection with lazy ratio and percentage views.
#[derive(Clone, Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct LazyColumnPercentPerBlock<B: FixedRatio, C: ColumnId>(
pub Percent<LazyColumnPerBlock<B, C>, LazyPerBlock<StoredF32, B>>,
);
impl<B: FixedRatio, C: ColumnId> LazyColumnPercentPerBlock<B, C> {
pub(crate) fn new(
name: &str,
version: Version,
source: &ReadOnlyColumnarVec<PcoVec<Height, B>, C>,
column: C,
indexes: &indexes::Vecs,
) -> Self {
let ppm = LazyColumnPerBlock::new(
&format!("{name}_{}", B::SUFFIX),
version,
source,
column,
indexes,
);
let ratio = LazyPerBlock::from_resolutions::<B::ToRatio>(
&format!("{name}_ratio"),
version,
ppm.height.read_only_boxed_clone(),
&ppm.resolutions,
);
let percent = LazyPerBlock::from_resolutions::<B::ToPercent>(
name,
version,
ppm.height.read_only_boxed_clone(),
&ppm.resolutions,
);
Self(Percent {
ppm,
ratio,
percent,
})
}
}
@@ -8,7 +8,6 @@ use crate::internal::{FixedRatio, Percent};
/// Fully lazy lightweight percent container with no derived resolutions.
#[derive(Clone, Deref, DerefMut, Traversable)]
#[traversable(transparent)]
#[allow(clippy::type_complexity)]
pub struct LazyPercentVec<B: FixedRatio, S: VecValue>(
pub Percent<LazyVec<Height, B, Height, S>, LazyVec<Height, StoredF32, Height, B>>,
);
@@ -1,4 +1,5 @@
mod base;
mod columnar;
mod lazy;
mod lazy_cumulative_rolling;
mod lazy_vec;
@@ -6,6 +7,7 @@ mod lazy_windows;
mod windows;
pub use base::*;
pub use columnar::*;
pub use lazy::*;
pub use lazy_cumulative_rolling::*;
pub use lazy_vec::*;
@@ -6,26 +6,38 @@ use vecdb::{Database, Rw, StorageMode};
use crate::{
indexes,
internal::{FixedRatio, PercentPerBlock, Windows},
internal::{ColumnarPerBlock, FixedRatio, LazyColumnPercentPerBlock, WindowId, Windows},
};
/// 4 rolling window vecs (24h, 1w, 1m, 1y), each storing fixed-point values
/// with lazy ratio and percent float views.
/// Four named fixed-point percentage views backed by one columnar source.
#[derive(Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct PercentRollingWindows<B: FixedRatio, M: StorageMode = Rw>(
pub Windows<PercentPerBlock<B, M>>,
pub struct ColumnarPercentRollingWindows<B: FixedRatio, M: StorageMode = Rw>(
pub ColumnarPerBlock<B, WindowId, Windows<LazyColumnPercentPerBlock<B, WindowId>>, M>,
);
impl<B: FixedRatio> PercentRollingWindows<B> {
impl<B: FixedRatio> ColumnarPercentRollingWindows<B> {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self(Windows::try_from_fn(|suffix| {
PercentPerBlock::forced_import(db, &format!("{name}_{suffix}"), version, indexes)
})?))
Ok(Self(ColumnarPerBlock::forced_import(
db,
&format!("{name}_{}", B::SUFFIX),
version,
|source| {
WindowId::series(|window| {
LazyColumnPercentPerBlock::new(
&format!("{name}_{}", window.suffix()),
version,
source,
window,
indexes,
)
})
},
)?))
}
}
@@ -1,18 +1,73 @@
use brk_error::Result;
use brk_traversable::{Traversable, TreeNode};
use brk_traversable::Traversable;
use brk_types::{Cents, Version};
use vecdb::{AnyExportableVec, Database, ReadOnlyClone, Ro, Rw, StorageMode, WritableVec};
pub use brk_types::{PERCENTILES, PERCENTILES_LEN, PercentileId};
use derive_more::{Deref, DerefMut};
use vecdb::{Database, Rw, StorageMode};
use crate::indexes;
use crate::internal::{PerBlock, Price};
use crate::internal::{ColumnarPerBlock, LazyColumnPerBlock, Price};
pub const PERCENTILES: [u8; 19] = [
5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95,
];
pub const PERCENTILES_LEN: usize = PERCENTILES.len();
#[derive(Clone, Traversable)]
pub struct PercentilePrices<T> {
pub pct05: T,
pub pct10: T,
pub pct15: T,
pub pct20: T,
pub pct25: T,
pub pct30: T,
pub pct35: T,
pub pct40: T,
pub pct45: T,
pub pct50: T,
pub pct55: T,
pub pct60: T,
pub pct65: T,
pub pct70: T,
pub pct75: T,
pub pct80: T,
pub pct85: T,
pub pct90: T,
pub pct95: T,
}
impl<T> PercentilePrices<T> {
fn from_fn(mut f: impl FnMut(PercentileId) -> T) -> Self {
Self {
pct05: f(PercentileId::Pct05),
pct10: f(PercentileId::Pct10),
pct15: f(PercentileId::Pct15),
pct20: f(PercentileId::Pct20),
pct25: f(PercentileId::Pct25),
pct30: f(PercentileId::Pct30),
pct35: f(PercentileId::Pct35),
pct40: f(PercentileId::Pct40),
pct45: f(PercentileId::Pct45),
pct50: f(PercentileId::Pct50),
pct55: f(PercentileId::Pct55),
pct60: f(PercentileId::Pct60),
pct65: f(PercentileId::Pct65),
pct70: f(PercentileId::Pct70),
pct75: f(PercentileId::Pct75),
pct80: f(PercentileId::Pct80),
pct85: f(PercentileId::Pct85),
pct90: f(PercentileId::Pct90),
pct95: f(PercentileId::Pct95),
}
}
}
#[derive(Deref, DerefMut, Traversable)]
pub struct PercentilesVecs<M: StorageMode = Rw> {
pub vecs: [Price<PerBlock<Cents, M>>; PERCENTILES_LEN],
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub prices: ColumnarPerBlock<
Cents,
PercentileId,
PercentilePrices<Price<LazyColumnPerBlock<Cents, PercentileId>>>,
M,
>,
}
const VERSION: Version = Version::ONE;
@@ -24,64 +79,36 @@ impl PercentilesVecs {
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let vecs = PERCENTILES
.into_iter()
.map(|p| {
let series_name = format!("{prefix}_pct{p:02}");
Price::forced_import(db, &series_name, version + VERSION, indexes)
})
.collect::<Result<Vec<_>>>()?
.try_into()
.ok()
.expect("PERCENTILES length mismatch");
let version = version + VERSION;
let prices = ColumnarPerBlock::<Cents, PercentileId, _>::forced_import(
db,
&format!("{prefix}_cents"),
version,
|source| {
PercentilePrices::from_fn(|id| {
Price::from_columnar_source(
&format!("{prefix}_pct{:02}", id.percentile()),
version,
source,
id,
indexes,
)
})
},
)?;
Ok(Self { vecs })
Ok(Self { prices })
}
/// Push percentile prices (in cents).
#[inline(always)]
pub(crate) fn push(&mut self, percentile_prices: &[Cents; PERCENTILES_LEN]) {
for (i, v) in self.vecs.iter_mut().enumerate() {
v.cents.height.push(percentile_prices[i]);
}
self.prices.push(*percentile_prices);
}
/// Validate computed versions or reset if mismatched.
pub(crate) fn validate_computed_version_or_reset(&mut self, version: Version) -> Result<()> {
for vec in self.vecs.iter_mut() {
vec.cents
.height
.validate_computed_version_or_reset(version)?;
}
self.prices.validate_computed_version_or_reset(version)?;
Ok(())
}
}
impl ReadOnlyClone for PercentilesVecs {
type ReadOnly = PercentilesVecs<Ro>;
fn read_only_clone(&self) -> Self::ReadOnly {
PercentilesVecs {
vecs: self.vecs.each_ref().map(|v| v.read_only_clone()),
}
}
}
impl<M: StorageMode> Traversable for PercentilesVecs<M>
where
Price<PerBlock<Cents, M>>: Traversable,
{
fn to_tree_node(&self) -> TreeNode {
TreeNode::Branch(
PERCENTILES
.iter()
.zip(self.vecs.iter())
.map(|(p, v)| (format!("pct{p:02}"), v.to_tree_node()))
.collect(),
)
}
fn iter_any_exportable(&self) -> impl Iterator<Item = &dyn AnyExportableVec> {
self.vecs.iter().flat_map(|p| p.iter_any_exportable())
}
}
@@ -6,11 +6,14 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Dollars, SatsFract, Version};
use brk_types::{Cents, Dollars, Height, SatsFract, Version};
use schemars::JsonSchema;
use vecdb::{Database, ReadableCloneableVec, ReadableVec, TypedVec, UnaryTransform};
use vecdb::{
ColumnId, Database, PcoVec, ReadOnlyColumnarVec, ReadableCloneableVec, ReadableVec, TypedVec,
UnaryTransform,
};
use super::{LazyPerBlock, PerBlock};
use super::{LazyColumnPerBlock, LazyPerBlock, PerBlock};
use crate::{
indexes,
internal::{CentsUnsignedToDollars, ComputedVecValue, DollarsToSatsFract},
@@ -48,6 +51,35 @@ impl Price<PerBlock<Cents>> {
}
}
impl<C> Price<LazyColumnPerBlock<Cents, C>>
where
C: ColumnId,
{
pub(crate) fn from_columnar_source(
name: &str,
version: Version,
source: &ReadOnlyColumnarVec<PcoVec<Height, Cents>, C>,
column: C,
indexes: &indexes::Vecs,
) -> Self {
let cents =
LazyColumnPerBlock::new(&format!("{name}_cents"), version, source, column, indexes);
let usd = LazyPerBlock::from_resolutions::<CentsUnsignedToDollars>(
name,
version,
cents.height.read_only_boxed_clone(),
&cents.resolutions,
);
let sats = LazyPerBlock::from_lazy::<DollarsToSatsFract, Cents>(
&format!("{name}_sats"),
version,
&usd,
);
Self { usd, cents, sats }
}
}
impl Price<LazyPerBlock<Cents, Cents>> {
pub(crate) fn from_lazy_cents_source<F, S>(
name: &str,
@@ -0,0 +1,48 @@
use brk_traversable::Traversable;
use brk_types::{Height, StoredF32, Version};
use vecdb::{ColumnId, PcoVec, ReadOnlyColumnarVec, ReadableCloneableVec};
use crate::{
indexes,
internal::{FixedRatio, LazyColumnPerBlock, LazyPerBlock},
};
#[derive(Clone, Traversable)]
pub struct LazyColumnRatioPerBlock<R, C>
where
R: FixedRatio,
C: ColumnId,
{
pub ppm: LazyColumnPerBlock<R, C>,
pub ratio: LazyPerBlock<StoredF32, R>,
}
impl<R, C> LazyColumnRatioPerBlock<R, C>
where
R: FixedRatio,
C: ColumnId,
{
pub(crate) fn new(
name: &str,
version: Version,
source: &ReadOnlyColumnarVec<PcoVec<Height, R>, C>,
column: C,
indexes: &indexes::Vecs,
) -> Self {
let ppm = LazyColumnPerBlock::new(
&format!("{name}_{}", R::SUFFIX),
version,
source,
column,
indexes,
);
let ratio = LazyPerBlock::from_resolutions::<R::ToRatio>(
name,
version,
ppm.height.read_only_boxed_clone(),
&ppm.resolutions,
);
Self { ppm, ratio }
}
}
@@ -0,0 +1,60 @@
use brk_traversable::Traversable;
use brk_types::{Cents, Dollars, Height, PartsPerMillion64, SatsFract, StoredF32, Version};
use vecdb::{CachedBoxedVec, ColumnId, PcoVec, ReadOnlyColumnarVec, ReadableCloneableVec};
use crate::{
indexes,
internal::{LazyColumnPerBlock, LazyIndexedVec, LazyPerBlock, LazyRatioPerBlock, Price},
};
use super::price::price_ratio;
#[derive(Clone, Traversable)]
pub struct LazyColumnPriceWithRatioPerBlock<C>
where
C: ColumnId,
{
pub usd: LazyPerBlock<Dollars, Cents>,
pub cents: LazyColumnPerBlock<Cents, C>,
pub sats: LazyPerBlock<SatsFract, Dollars>,
pub ppm: LazyPerBlock<PartsPerMillion64>,
pub ratio: LazyPerBlock<StoredF32, PartsPerMillion64>,
}
impl<C> LazyColumnPriceWithRatioPerBlock<C>
where
C: ColumnId,
{
pub(crate) fn new(
name: &str,
version: Version,
source: &ReadOnlyColumnarVec<PcoVec<Height, Cents>, C>,
column: C,
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Self {
let price = Price::from_columnar_source(name, version, source, column, indexes);
let ratio_version = version + Version::new(4);
let ppm_source = LazyIndexedVec::new(
&format!("{name}_ratio_ppm_source"),
ratio_version,
price.cents.height.read_only_boxed_clone(),
spot_price.clone(),
|_, price, spot| price_ratio(spot, price),
);
let ratio = LazyRatioPerBlock::from_height_source(
&format!("{name}_ratio"),
ratio_version,
ppm_source,
indexes,
);
Self {
usd: price.usd,
cents: price.cents,
sats: price.sats,
ppm: ratio.ppm,
ratio: ratio.ratio,
}
}
}
@@ -1,9 +1,13 @@
mod base;
mod columnar;
mod columnar_price;
mod lazy;
mod lazy_price;
mod price;
pub use base::*;
pub use columnar::*;
pub use columnar_price::*;
pub use lazy::*;
pub use lazy_price::*;
pub use price::*;
@@ -15,7 +15,8 @@ use crate::{
blocks::lookback::LazyWindowStartVec,
indexes,
internal::{
ComputedVecValue, NumericValue, PerBlock, RollingWindow24h, Windows, WindowsFrom1w,
ColumnarPerBlock, ComputedVecValue, LazyColumnPerBlock, NumericValue, PerBlock,
RollingWindow24h, WindowFrom1wId, WindowId, Windows, WindowsFrom1w,
},
};
@@ -48,6 +49,44 @@ where
}
}
/// Four named rolling-window views backed by one columnar source.
#[derive(Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct ColumnarRollingWindows<T, M: StorageMode = Rw>(
pub ColumnarPerBlock<T, WindowId, Windows<LazyColumnPerBlock<T, WindowId>>, M>,
)
where
T: NumericValue + JsonSchema;
impl<T> ColumnarRollingWindows<T>
where
T: NumericValue + JsonSchema,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self(ColumnarPerBlock::forced_import(
db,
name,
version,
|source| {
WindowId::series(|window| {
LazyColumnPerBlock::new(
&format!("{name}_{}", window.suffix()),
version,
source,
window,
indexes,
)
})
},
)?))
}
}
/// Single 24h rolling window backed by PerBlock (1 stored vec).
#[derive(Deref, DerefMut, Traversable)]
#[traversable(transparent)]
@@ -71,14 +110,16 @@ where
}
}
/// Extended rolling windows: 1w + 1m + 1y (3 stored vecs).
/// The 1w, 1m, and 1y views backed by one columnar source.
#[derive(Deref, DerefMut, Traversable)]
#[traversable(transparent)]
pub struct RollingWindowsFrom1w<T, M: StorageMode = Rw>(pub WindowsFrom1w<PerBlock<T, M>>)
pub struct ColumnarRollingWindowsFrom1w<T, M: StorageMode = Rw>(
pub ColumnarPerBlock<T, WindowFrom1wId, WindowsFrom1w<LazyColumnPerBlock<T, WindowFrom1wId>>, M>,
)
where
T: ComputedVecValue + PartialOrd + JsonSchema;
T: NumericValue + JsonSchema;
impl<T> RollingWindowsFrom1w<T>
impl<T> ColumnarRollingWindowsFrom1w<T>
where
T: NumericValue + JsonSchema,
{
@@ -88,8 +129,21 @@ where
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
Ok(Self(WindowsFrom1w::try_from_fn(|suffix| {
PerBlock::forced_import(db, &format!("{name}_{suffix}"), version, indexes)
})?))
Ok(Self(ColumnarPerBlock::forced_import(
db,
name,
version,
|source| {
WindowFrom1wId::series(|window| {
LazyColumnPerBlock::new(
&format!("{name}_{}", window.suffix()),
version,
source,
window,
indexes,
)
})
},
)?))
}
}
@@ -0,0 +1,239 @@
use brk_traversable::Traversable;
use brk_types::{
Bitcoin, Cents, Dollars, Height, PartsPerMillionSigned64, Sats, SatsSigned, Version,
};
use derive_more::{Deref, DerefMut};
use vecdb::{
BinaryTransform, CachedBoxedVec, ColumnId, PcoVec, ReadOnlyColumnarVec, ReadableCloneableVec,
};
use crate::{
indexes,
internal::{
CachedWindowStartVec, CentsUnsignedToDollars, Identity, LazyColumnPerBlock, LazyIndexedVec,
LazyPerBlock, LazyRollingAvgsAmountFromHeight, LazyRollingDeltasAmountFromHeight,
LazyRollingSumsAmountFromHeight, LazyValueBlock, SatsToBitcoin, SatsToCents, Windows,
},
};
#[derive(Clone, Traversable)]
pub struct LazyColumnValuePerBlock<C>
where
C: ColumnId,
{
pub btc: LazyPerBlock<Bitcoin, Sats>,
pub sats: LazyColumnPerBlock<Sats, C>,
pub usd: LazyPerBlock<Dollars, Cents>,
pub cents: LazyColumnPerBlock<Cents, C>,
}
#[derive(Clone, Traversable)]
pub struct LazyColumnSpotValuePerBlock<C>
where
C: ColumnId,
{
pub btc: LazyPerBlock<Bitcoin, Sats>,
pub sats: LazyColumnPerBlock<Sats, C>,
pub usd: LazyPerBlock<Dollars, Cents>,
pub cents: LazyPerBlock<Cents>,
}
impl<C> LazyColumnSpotValuePerBlock<C>
where
C: ColumnId,
{
pub(crate) fn new(
name: &str,
version: Version,
source: &ReadOnlyColumnarVec<PcoVec<Height, Sats>, C>,
column: C,
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Self {
let sats =
LazyColumnPerBlock::new(&format!("{name}_sats"), version, source, column, indexes);
let btc = LazyPerBlock::from_resolutions::<SatsToBitcoin>(
name,
version,
sats.height.read_only_boxed_clone(),
&sats.resolutions,
);
let cents_source = LazyIndexedVec::new(
&format!("{name}_cents_source"),
version,
sats.height.read_only_boxed_clone(),
spot_price.clone(),
|_, sats, spot| SatsToCents::apply(sats, spot),
);
let cents = LazyPerBlock::from_height_source::<Identity<Cents>, _>(
&format!("{name}_cents"),
version,
cents_source,
indexes,
);
let usd = LazyPerBlock::from_lazy::<CentsUnsignedToDollars, Cents>(
&format!("{name}_usd"),
version,
&cents,
);
Self {
btc,
sats,
usd,
cents,
}
}
}
#[derive(Clone, Deref, DerefMut, Traversable)]
pub struct LazyColumnSpotValuePerBlockWithDeltas<C>
where
C: ColumnId,
{
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub inner: LazyColumnSpotValuePerBlock<C>,
pub delta: LazyRollingDeltasAmountFromHeight<Sats, SatsSigned, PartsPerMillionSigned64>,
}
impl<C> LazyColumnSpotValuePerBlockWithDeltas<C>
where
C: ColumnId,
{
pub(crate) fn new(
name: &str,
version: Version,
source: &ReadOnlyColumnarVec<PcoVec<Height, Sats>, C>,
column: C,
indexes: &indexes::Vecs,
cached_starts: &Windows<&CachedWindowStartVec>,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Self {
let inner =
LazyColumnSpotValuePerBlock::new(name, version, source, column, indexes, spot_price);
let delta = LazyRollingDeltasAmountFromHeight::new(
&format!("{name}_delta"),
version + Version::TWO,
&inner.sats.height,
cached_starts,
indexes,
);
Self { inner, delta }
}
}
impl<C> LazyColumnValuePerBlock<C>
where
C: ColumnId,
{
pub(crate) fn new(
name: &str,
version: Version,
sats_source: &ReadOnlyColumnarVec<PcoVec<Height, Sats>, C>,
cents_source: &ReadOnlyColumnarVec<PcoVec<Height, Cents>, C>,
column: C,
indexes: &indexes::Vecs,
) -> Self {
let sats = LazyColumnPerBlock::new(
&format!("{name}_sats"),
version,
sats_source,
column,
indexes,
);
let btc = LazyPerBlock::from_resolutions::<SatsToBitcoin>(
name,
version,
sats.height.read_only_boxed_clone(),
&sats.resolutions,
);
let cents = LazyColumnPerBlock::new(
&format!("{name}_cents"),
version,
cents_source,
column,
indexes,
);
let usd = LazyPerBlock::from_resolutions::<CentsUnsignedToDollars>(
&format!("{name}_usd"),
version,
cents.height.read_only_boxed_clone(),
&cents.resolutions,
);
Self {
btc,
sats,
usd,
cents,
}
}
}
#[derive(Clone, Traversable)]
pub struct LazyColumnValuePerBlockCumulativeRolling<C>
where
C: ColumnId,
{
pub block: LazyValueBlock,
pub cumulative: LazyColumnValuePerBlock<C>,
pub sum: LazyRollingSumsAmountFromHeight,
pub average: LazyRollingAvgsAmountFromHeight,
}
impl<C> LazyColumnValuePerBlockCumulativeRolling<C>
where
C: ColumnId,
{
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
name: &str,
version: Version,
cumulative_sats: &ReadOnlyColumnarVec<PcoVec<Height, Sats>, C>,
cumulative_cents: &ReadOnlyColumnarVec<PcoVec<Height, Cents>, C>,
column: C,
indexes: &indexes::Vecs,
cached_starts: &Windows<&CachedWindowStartVec>,
) -> Self {
let cumulative = LazyColumnValuePerBlock::new(
&format!("{name}_cumulative"),
version,
cumulative_sats,
cumulative_cents,
column,
indexes,
);
let block = LazyValueBlock::from_cumulative_sources(
name,
version,
&cumulative.sats.height,
&cumulative.cents.height,
);
let sum = LazyRollingSumsAmountFromHeight::new(
&format!("{name}_sum"),
version,
&cumulative.sats.height,
&cumulative.cents.height,
cached_starts,
indexes,
);
let average = LazyRollingAvgsAmountFromHeight::new(
&format!("{name}_average"),
version,
&cumulative.sats.height,
&cumulative.cents.height,
cached_starts,
indexes,
);
Self {
block,
cumulative,
sum,
average,
}
}
}
@@ -1,6 +1,7 @@
mod base;
mod cached;
mod cached_full;
mod columnar;
mod cumulative;
mod cumulative_rolling;
mod lazy;
@@ -11,11 +12,11 @@ mod lazy_rolling_sum;
mod lazy_spot;
mod rolling_distribution;
mod spot;
mod spot_with_deltas;
pub use base::*;
pub use cached::*;
pub use cached_full::*;
pub use columnar::*;
pub use cumulative::*;
pub use cumulative_rolling::*;
pub use lazy::*;
@@ -26,4 +27,3 @@ pub use lazy_rolling_sum::*;
pub use lazy_spot::*;
pub use rolling_distribution::*;
pub use spot::*;
pub use spot_with_deltas::*;
@@ -1,44 +0,0 @@
use brk_error::Result;
use brk_traversable::Traversable;
use brk_types::{Cents, Height, PartsPerMillionSigned64, Sats, SatsSigned, Version};
use derive_more::{Deref, DerefMut};
use vecdb::{CachedBoxedVec, Database, Rw, StorageMode};
use crate::{
indexes,
internal::{
CachedWindowStartVec, LazyRollingDeltasAmountFromHeight, SpotValuePerBlock, Windows,
},
};
#[derive(Deref, DerefMut, Traversable)]
pub struct SpotValuePerBlockWithDeltas<M: StorageMode = Rw> {
#[deref]
#[deref_mut]
#[traversable(flatten)]
pub inner: SpotValuePerBlock<M>,
pub delta: LazyRollingDeltasAmountFromHeight<Sats, SatsSigned, PartsPerMillionSigned64>,
}
impl SpotValuePerBlockWithDeltas {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &Windows<&CachedWindowStartVec>,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let inner = SpotValuePerBlock::forced_import(db, name, version, indexes, spot_price)?;
let delta = LazyRollingDeltasAmountFromHeight::new(
&format!("{name}_delta"),
version + Version::TWO,
&inner.sats.height,
cached_starts,
indexes,
);
Ok(Self { inner, delta })
}
}
@@ -21,7 +21,7 @@ pub use derived::{
};
pub use fixed_ratio::{FixedToPercent, FixedToRatio};
pub use ratio::{
RatioCents, RatioCentsSignedCents, RatioDiffCents, RatioDiffDollars, RatioDiffF32,
RatioBytes, RatioCents, RatioCentsSignedCents, RatioDiffCents, RatioDiffDollars, RatioDiffF32,
RatioDollars, RatioSats, RatioU64,
};
pub use specialized::{
@@ -1,9 +1,9 @@
use std::marker::PhantomData;
use brk_types::{Cents, CentsSigned, Dollars, Sats, StoredF32, StoredU64};
use brk_types::{Bytes, Cents, CentsSigned, Dollars, Sats, StoredF32, StoredU64};
use vecdb::{BinaryTransform, unlikely};
use crate::internal::FixedRatio;
use crate::internal::{FixedRatio, NumericValue};
pub struct RatioU64<P>(PhantomData<P>);
@@ -18,6 +18,20 @@ impl<P: FixedRatio> BinaryTransform<StoredU64, StoredU64, P> for RatioU64<P> {
}
}
pub struct RatioBytes<P>(PhantomData<P>);
impl<P: FixedRatio, D: NumericValue> BinaryTransform<Bytes, D, P> for RatioBytes<P> {
#[inline(always)]
fn apply(numerator: Bytes, denominator: D) -> P {
let denominator: f64 = denominator.into();
if denominator > 0.0 {
P::from(f64::from(numerator) / denominator)
} else {
P::default()
}
}
}
pub struct RatioSats<P>(PhantomData<P>);
impl<P: FixedRatio> BinaryTransform<Sats, Sats, P> for RatioSats<P> {
@@ -1,218 +0,0 @@
//! Generic `all` + per-`AddrType` container, mirrors the `WithSth` pattern
//! along the address-type axis. Used by every metric that tracks one
//! aggregate value alongside a per-address-type breakdown.
use brk_cohort::ByAddrType;
use brk_error::Result;
use brk_indexer::Lengths;
use brk_traversable::Traversable;
use brk_types::{Cents, Height, Sats, Version};
use rayon::prelude::*;
use schemars::JsonSchema;
use vecdb::{AnyStoredVec, AnyVec, CachedBoxedVec, Database, EagerVec, Exit, PcoVec, WritableVec};
use crate::indexes;
use super::{
CachedWindowStartVec, NumericValue, PerBlock, PerBlockCumulativeRolling, SpotValuePerBlock,
Windows,
};
/// `all` aggregate plus per-`AddrType` breakdown.
#[derive(Clone, Traversable)]
pub struct WithAddrTypes<T> {
pub all: T,
#[traversable(flatten)]
pub by_addr_type: ByAddrType<T>,
}
impl<T> WithAddrTypes<PerBlock<T>>
where
T: NumericValue + JsonSchema,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
) -> Result<Self> {
let all = PerBlock::forced_import(db, name, version, indexes)?;
let by_addr_type = ByAddrType::new_with_name(|type_name| {
PerBlock::forced_import(db, &format!("{type_name}_{name}"), version, indexes)
})?;
Ok(Self { all, by_addr_type })
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.by_addr_type
.values()
.map(|v| v.height.len())
.min()
.unwrap()
.min(self.all.height.len())
}
pub(crate) fn par_iter_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
rayon::iter::once(&mut self.all.height as &mut dyn AnyStoredVec).chain(
self.by_addr_type
.par_values_mut()
.map(|v| &mut v.height as &mut dyn AnyStoredVec),
)
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
self.all.height.reset()?;
for v in self.by_addr_type.values_mut() {
v.height.reset()?;
}
Ok(())
}
#[inline(always)]
pub(crate) fn push_height<U>(&mut self, total: U, per_type: impl IntoIterator<Item = U>)
where
U: Into<T>,
{
self.all.height.push(total.into());
for (v, value) in self.by_addr_type.values_mut().zip(per_type) {
v.height.push(value.into());
}
}
/// Compute `all.height` as the per-block sum of the per-type vecs.
pub(crate) fn compute_rest(&mut self, starting_lengths: &Lengths, exit: &Exit) -> Result<()> {
let sources: Vec<&EagerVec<PcoVec<Height, T>>> =
self.by_addr_type.values().map(|v| &v.height).collect();
self.all
.height
.compute_sum_of_others(starting_lengths.height, &sources, exit)?;
Ok(())
}
}
impl<T> WithAddrTypes<PerBlockCumulativeRolling<T>>
where
T: NumericValue + JsonSchema,
{
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
cached_starts: &Windows<&CachedWindowStartVec>,
) -> Result<Self> {
let all =
PerBlockCumulativeRolling::forced_import(db, name, version, indexes, cached_starts)?;
let by_addr_type = ByAddrType::new_with_name(|type_name| {
PerBlockCumulativeRolling::forced_import(
db,
&format!("{type_name}_{name}"),
version,
indexes,
cached_starts,
)
})?;
Ok(Self { all, by_addr_type })
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.by_addr_type
.values()
.map(|v| v.cumulative.height.len())
.min()
.unwrap()
.min(self.all.cumulative.height.len())
}
pub(crate) fn par_iter_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
rayon::iter::once(&mut self.all.cumulative.height as &mut dyn AnyStoredVec).chain(
self.by_addr_type
.par_values_mut()
.map(|v| &mut v.cumulative.height as &mut dyn AnyStoredVec),
)
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
self.all.cumulative.height.reset()?;
for v in self.by_addr_type.values_mut() {
v.cumulative.height.reset()?;
}
Ok(())
}
#[inline(always)]
pub(crate) fn push_height<U>(&mut self, total: U, per_type: impl IntoIterator<Item = U>)
where
U: Into<T>,
{
self.all.push_block(total.into());
for (v, value) in self.by_addr_type.values_mut().zip(per_type) {
v.push_block(value.into());
}
}
}
impl WithAddrTypes<SpotValuePerBlock> {
pub(crate) fn forced_import(
db: &Database,
name: &str,
version: Version,
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Result<Self> {
let all = SpotValuePerBlock::forced_import(db, name, version, indexes, spot_price)?;
let by_addr_type = ByAddrType::new_with_name(|type_name| {
SpotValuePerBlock::forced_import(
db,
&format!("{type_name}_{name}"),
version,
indexes,
spot_price,
)
})?;
Ok(Self { all, by_addr_type })
}
pub(crate) fn min_stateful_len(&self) -> usize {
self.by_addr_type
.values()
.map(|v| v.sats.height.len())
.min()
.unwrap()
.min(self.all.sats.height.len())
}
pub(crate) fn par_iter_height_mut(
&mut self,
) -> impl ParallelIterator<Item = &mut dyn AnyStoredVec> {
rayon::iter::once(&mut self.all.sats.height as &mut dyn AnyStoredVec).chain(
self.by_addr_type
.par_values_mut()
.map(|v| &mut v.sats.height as &mut dyn AnyStoredVec),
)
}
pub(crate) fn reset_height(&mut self) -> Result<()> {
self.all.sats.height.reset()?;
for v in self.by_addr_type.values_mut() {
v.sats.height.reset()?;
}
Ok(())
}
/// Push the stateful sats value for `all` and each per-type.
#[inline(always)]
pub(crate) fn push_height<U>(&mut self, total: U, per_type: impl IntoIterator<Item = U>)
where
U: Into<Sats>,
{
self.all.sats.height.push(total.into());
for (v, value) in self.by_addr_type.values_mut().zip(per_type) {
v.sats.height.push(value.into());
}
}
}
@@ -0,0 +1,143 @@
use brk_cohort::{ADDR_TYPE_IDS, AddrTypeId};
use brk_types::{Cents, Height, Sats, Version};
use schemars::JsonSchema;
use vecdb::{
CachedBoxedVec, CachedVec, PcoVec, PcoVecValue, ReadOnlyColumnarVec, ReadableColumnarVec,
UnaryTransform,
};
use crate::{
indexes,
internal::{
CachedWindowStartVec, Identity, LazyColumnPerBlock, LazyColumnPerBlockCumulativeRolling,
LazyColumnSpotValuePerBlock, LazyPerBlock, LazyPerBlockCumulativeAverage,
LazyPerBlockCumulativeRolling, LazySpotValuePerBlock, NumericValue, Windows,
},
};
use super::WithAddrTypes;
impl<T> WithAddrTypes<LazyColumnPerBlock<T, AddrTypeId>, LazyPerBlock<T>>
where
T: NumericValue + JsonSchema + 'static,
{
pub(crate) fn from_columnar_source(
name: &str,
version: Version,
source: &ReadOnlyColumnarVec<PcoVec<Height, T>, AddrTypeId>,
indexes: &indexes::Vecs,
) -> Self {
let all = LazyPerBlock::from_height_source::<Identity<T>, _>(
name,
version,
source.sum_columns(name, version, ADDR_TYPE_IDS),
indexes,
);
let by_addr_type = AddrTypeId::series(|column, type_name| {
LazyColumnPerBlock::new(
&format!("{type_name}_{name}"),
version,
source,
column,
indexes,
)
});
Self { all, by_addr_type }
}
}
impl WithAddrTypes<LazyColumnSpotValuePerBlock<AddrTypeId>, LazySpotValuePerBlock> {
pub(crate) fn from_columnar_spot_value_source(
name: &str,
version: Version,
source: &ReadOnlyColumnarVec<PcoVec<Height, Sats>, AddrTypeId>,
indexes: &indexes::Vecs,
spot_price: &CachedBoxedVec<Height, Cents>,
) -> Self {
let sats =
CachedVec::wrap(source.sum_columns(&format!("{name}_sats"), version, ADDR_TYPE_IDS));
let all = LazySpotValuePerBlock::from_sats_source(name, version, sats, indexes, spot_price);
let by_addr_type = AddrTypeId::series(|column, type_name| {
LazyColumnSpotValuePerBlock::new(
&format!("{type_name}_{name}"),
version,
source,
column,
indexes,
spot_price,
)
});
Self { all, by_addr_type }
}
}
impl<T>
WithAddrTypes<
LazyColumnPerBlockCumulativeRolling<T, AddrTypeId>,
LazyPerBlockCumulativeRolling<T>,
>
where
T: NumericValue + JsonSchema + PcoVecValue,
{
pub(crate) fn from_columnar_cumulative_source(
name: &str,
version: Version,
source: &ReadOnlyColumnarVec<PcoVec<Height, T>, AddrTypeId>,
indexes: &indexes::Vecs,
cached_starts: &Windows<&CachedWindowStartVec>,
) -> Self {
let cumulative = CachedVec::wrap(source.sum_columns(
&format!("{name}_cumulative"),
version,
ADDR_TYPE_IDS,
));
let all = LazyPerBlockCumulativeRolling::from_cached_cumulative_source(
name,
version,
cumulative,
cached_starts,
indexes,
);
let by_addr_type = AddrTypeId::series(|column, type_name| {
LazyColumnPerBlockCumulativeRolling::new(
&format!("{type_name}_{name}"),
version,
source,
column,
indexes,
cached_starts,
)
});
Self { all, by_addr_type }
}
}
impl<T, C, F> WithAddrTypes<LazyPerBlockCumulativeAverage<T, C, F>>
where
T: NumericValue + JsonSchema,
C: NumericValue + JsonSchema + PcoVecValue,
F: UnaryTransform<C, T>,
{
pub(crate) fn from_columnar_cumulative_average_source(
name: &str,
version: Version,
source: &ReadOnlyColumnarVec<PcoVec<Height, C>, AddrTypeId>,
indexes: &indexes::Vecs,
cached_starts: &Windows<&CachedWindowStartVec>,
) -> Self {
let cumulative_name = format!("{name}_cumulative");
let cumulative = source.sum_columns(&cumulative_name, version, ADDR_TYPE_IDS);
let all =
LazyPerBlockCumulativeAverage::new(name, version, &cumulative, indexes, cached_starts);
let by_addr_type = AddrTypeId::series(|column, type_name| {
let name = format!("{type_name}_{name}");
let cumulative = source.column(&format!("{name}_cumulative"), version, column);
LazyPerBlockCumulativeAverage::new(&name, version, &cumulative, indexes, cached_starts)
});
Self { all, by_addr_type }
}
}
@@ -0,0 +1,12 @@
mod columnar;
use brk_cohort::ByAddrType;
use brk_traversable::Traversable;
/// `all` aggregate plus a per-address-type breakdown.
#[derive(Clone, Traversable)]
pub struct WithAddrTypes<T, A = T> {
pub all: A,
#[traversable(flatten)]
pub by_addr_type: ByAddrType<T>,
}
+1
View File
@@ -1,4 +1,5 @@
#![doc = include_str!("../README.md")]
#![allow(clippy::type_complexity)]
use std::{fs, path::Path, thread, time::Instant};
@@ -16,32 +16,12 @@ impl Vecs {
let starting_lengths = indexer.safe_lengths();
let close = &prices.spot.cents.height;
for (ema, period) in [
(&mut self.ema._1w, 7),
(&mut self.ema._8d, 8),
(&mut self.ema._12d, 12),
(&mut self.ema._13d, 13),
(&mut self.ema._21d, 21),
(&mut self.ema._26d, 26),
(&mut self.ema._1m, 30),
(&mut self.ema._34d, 34),
(&mut self.ema._55d, 55),
(&mut self.ema._89d, 89),
(&mut self.ema._144d, 144),
(&mut self.ema._200d, 200),
(&mut self.ema._1y, 365),
(&mut self.ema._2y, 2 * 365),
(&mut self.ema._200w, 200 * 7),
(&mut self.ema._4y, 4 * 365),
] {
let window_starts = blocks.lookback.start_vec(period);
ema.cents.height.compute_rolling_ema(
starting_lengths.height,
window_starts,
close,
exit,
)?;
}
self.ema.height.compute_rolling_ema_columns(
starting_lengths.height,
|period| blocks.lookback.start_vec(period.days()),
close,
exit,
)?;
Ok(())
}

Some files were not shown because too many files have changed in this diff Show More