heatmaps: part 12

This commit is contained in:
nym21
2026-06-01 10:56:58 +02:00
parent a94d31dfdf
commit e64ffac8d1
14 changed files with 346 additions and 93 deletions
+7 -1
View File
@@ -115,7 +115,13 @@ impl Vecs {
let seed_bin = cents_to_bin(prev_cents.inner() as f64);
let warmup = config.window_size.min(committed - START_HEIGHT_SLOW);
let mut oracle = Oracle::from_checkpoint(seed_bin, config, |o| {
Self::feed_blocks(o, indexer, (committed - warmup)..committed, None);
Self::feed_blocks_with(
o,
indexer,
(committed - warmup)..committed,
None,
|_, _, _| {},
);
});
let num_new = total_heights - committed;
+1 -1
View File
@@ -6,7 +6,7 @@ use std::path::PathBuf;
use brk_indexer::Indexer;
use brk_oracle::{
Config, Oracle, PRICES, HistogramRaw, START_HEIGHT_FAST, bin_to_cents, cents_to_bin,
Config, HistogramRaw, Oracle, PRICES, START_HEIGHT_FAST, bin_to_cents, cents_to_bin,
eligible_bin,
};
use brk_types::{OutputType, Sats, TxIndex, TxOutIndex};
+39
View File
@@ -1,3 +1,5 @@
use std::ops::Range;
/// First height the oracle computes on-chain, with the slow cold-start EMA
/// ([`slow`](Config::slow)). Below it, prices come from [`PRICES`](crate::PRICES).
pub const START_HEIGHT_SLOW: usize = 340_000;
@@ -61,4 +63,41 @@ impl Config {
Self::default()
}
}
/// Split a block range into sub-ranges with a single EMA configuration.
pub fn segments_for_range(range: Range<usize>) -> impl Iterator<Item = Range<usize>> {
let split = START_HEIGHT_FAST.clamp(range.start, range.end);
[range.start..split, split..range.end]
.into_iter()
.filter(|range| !range.is_empty())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn segments_for_range_splits_at_fast_start() {
let segments: Vec<_> =
Config::segments_for_range((START_HEIGHT_FAST - 2)..(START_HEIGHT_FAST + 2)).collect();
assert_eq!(
segments,
vec![
(START_HEIGHT_FAST - 2)..START_HEIGHT_FAST,
START_HEIGHT_FAST..(START_HEIGHT_FAST + 2),
]
);
}
#[test]
fn segments_for_range_omits_empty_sides() {
let slow: Vec<_> =
Config::segments_for_range((START_HEIGHT_FAST - 2)..START_HEIGHT_FAST).collect();
assert_eq!(slow, vec![(START_HEIGHT_FAST - 2)..START_HEIGHT_FAST]);
let fast: Vec<_> =
Config::segments_for_range(START_HEIGHT_FAST..(START_HEIGHT_FAST + 2)).collect();
assert_eq!(fast, vec![START_HEIGHT_FAST..(START_HEIGHT_FAST + 2)]);
}
}
+42
View File
@@ -169,4 +169,46 @@ mod tests {
assert!(switched.ema().iter().eq(fresh.ema().iter()));
}
#[test]
fn sequential_ema_matches_fresh_warmups() {
let hists: Vec<HistogramRaw> = (0..80)
.map(|i| {
let mut h = HistogramRaw::zeros();
h.increment(1000 + i % 11);
h.increment(1300 + i % 13);
h.increment(1700 + i % 17);
h
})
.collect();
for config in [Config::slow(), Config::default()] {
let query_start = config.window_size + 5;
let query_end = query_start + 20;
let seed = 1600.0;
let mut sequential = Oracle::from_checkpoint(seed, config.clone(), |o| {
hists[query_start + 1 - config.window_size..query_start + 1]
.iter()
.for_each(|h| {
o.process_histogram(h);
});
});
for height in query_start..query_end {
if height != query_start {
sequential.process_histogram(&hists[height]);
}
let fresh = Oracle::from_checkpoint(seed, config.clone(), |o| {
hists[height + 1 - config.window_size..height + 1]
.iter()
.for_each(|h| {
o.process_histogram(h);
});
});
assert!(sequential.ema().iter().eq(fresh.ema().iter()));
}
}
}
}
+5 -1
View File
@@ -61,7 +61,11 @@ impl ShapeAnchor {
/// shifts off the round-USD ladder. 0 for an empty (no-mass) center.
fn shape_match(&self, ema: &HistogramEma, center: i64) -> f64 {
match normalized_arms_at(ema, center) {
Some(arms) => 1.0 - (0..N_ARMS).map(|i| (arms[i] - self.profile[i]).abs()).sum::<f64>(),
Some(arms) => {
1.0 - (0..N_ARMS)
.map(|i| (arms[i] - self.profile[i]).abs())
.sum::<f64>()
}
None => 0.0,
}
}
+51 -57
View File
@@ -4,14 +4,15 @@ use brk_computer::prices::Vecs as PricesVecs;
use brk_error::{Error, Result};
use brk_indexer::Lengths;
use brk_oracle::{
Config, HistogramEma, HistogramEmaCompact, HistogramRaw, Oracle, START_HEIGHT_FAST,
cents_to_bin, sats_to_bin,
Config, HistogramEma, HistogramEmaCompact, HistogramRaw, Oracle, cents_to_bin, sats_to_bin,
};
use brk_types::{Day1, Dollars, Sats, TxOutIndex};
use vecdb::{AnyVec, ReadableVec, VecIndex};
use crate::Query;
const RAW_HISTOGRAM_VALUE_CHUNK: usize = 1_000_000;
impl Query {
pub fn live_price(&self) -> Result<Dollars> {
Ok(self.live_oracle()?.price_dollars())
@@ -48,9 +49,9 @@ impl Query {
let count = range.len();
let mut acc = HistogramEma::zeros();
for segment in ema_config_segments(range) {
for segment in Config::segments_for_range(range) {
let mut oracle = self.ema_oracle_at(segment.start, safe)?;
add_ema(&mut acc, oracle.ema());
acc.add_from(oracle.ema());
let feed_start = segment.start + 1;
if feed_start < segment.end {
@@ -59,13 +60,12 @@ impl Query {
self.indexer(),
feed_start..segment.end,
Some(safe),
|_, oracle, _| add_ema(&mut acc, oracle.ema()),
|_, oracle, _| acc.add_from(oracle.ema()),
);
}
}
let count = count as f64;
acc.iter_mut().for_each(|a| *a /= count);
acc.divide_by(count as f64);
Ok(acc)
}
@@ -83,7 +83,7 @@ impl Query {
/// in the block binned by value, with no payment filtering.
pub fn confirmed_histogram_raw(&self, height: usize) -> Result<HistogramRaw> {
let safe = self.check_histogram_height(height)?;
Ok(self.block_raw_histogram(height, &safe))
Ok(self.raw_histogram_for_blocks(height..height + 1, &safe))
}
/// Unfiltered per-bin output counts for a calendar `day`: every block's raw
@@ -92,14 +92,7 @@ impl Query {
pub fn confirmed_histogram_raw_day(&self, day: Day1) -> Result<HistogramRaw> {
let safe = self.safe_lengths();
let range = self.day_block_range(day, &safe)?;
let mut acc = HistogramRaw::zeros();
for height in range {
let block = self.block_raw_histogram(height, &safe);
acc.iter_mut()
.zip(block.iter())
.for_each(|(a, &v)| *a += v);
}
Ok(acc)
Ok(self.raw_histogram_for_blocks(range, &safe))
}
/// The live tip oracle: the cached committed base, with the forming block's
@@ -225,49 +218,51 @@ impl Query {
Ok(start..end)
}
/// One confirmed block's unfiltered histogram: every output in the block,
/// Unfiltered histogram for a contiguous confirmed block range: every output,
/// coinbase included, binned by value via `sats_to_bin` with no payment
/// filtering. Built from a single batched columnar read of the block's
/// output-value range.
fn block_raw_histogram(&self, height: usize, safe: &Lengths) -> HistogramRaw {
/// filtering. Raw counts are additive, so a day can be read as one output
/// range instead of one block at a time.
fn raw_histogram_for_blocks(&self, range: Range<usize>, safe: &Lengths) -> HistogramRaw {
let indexer = self.indexer();
let total_outputs = safe.txout_index.to_usize();
let next_height = (height + 2).min(safe.height.to_usize());
let collect_end = (range.end + 1).min(safe.height.to_usize());
let out_firsts: Vec<TxOutIndex> = indexer
.vecs
.outputs
.first_txout_index
.collect_range_at(height, next_height);
.collect_range_at(range.start, collect_end);
let out_start = out_firsts[0].to_usize();
let out_end = out_firsts
.get(1)
.get(range.end - range.start)
.copied()
.unwrap_or(TxOutIndex::from(total_outputs))
.to_usize();
let mut hist = HistogramRaw::zeros();
let values: Vec<Sats> = indexer.vecs.outputs.value.collect_range_at(out_start, out_end);
for sats in values {
if let Some(bin) = sats_to_bin(sats) {
hist.increment(bin);
}
let mut values: Vec<Sats> = Vec::new();
let mut start = out_start;
while start < out_end {
let end = (start + RAW_HISTOGRAM_VALUE_CHUNK).min(out_end);
values.clear();
indexer
.vecs
.outputs
.value
.collect_range_into_at(start, end, &mut values);
add_sats_to_raw_histogram(&mut hist, &values);
start = end;
}
hist
}
}
fn add_ema(acc: &mut HistogramEma, ema: &HistogramEma) {
acc.iter_mut()
.zip(ema.iter())
.for_each(|(a, &e)| *a += e);
}
fn ema_config_segments(range: Range<usize>) -> impl Iterator<Item = Range<usize>> {
let split = START_HEIGHT_FAST.clamp(range.start, range.end);
[range.start..split, split..range.end]
.into_iter()
.filter(|range| !range.is_empty())
fn add_sats_to_raw_histogram(hist: &mut HistogramRaw, values: &[Sats]) {
for &sats in values {
if let Some(bin) = sats_to_bin(sats) {
hist.increment(bin);
}
}
}
#[cfg(test)]
@@ -275,25 +270,24 @@ mod tests {
use super::*;
#[test]
fn ema_config_segments_split_at_fast_start() {
let segments: Vec<_> =
ema_config_segments((START_HEIGHT_FAST - 2)..(START_HEIGHT_FAST + 2)).collect();
assert_eq!(
segments,
vec![
(START_HEIGHT_FAST - 2)..START_HEIGHT_FAST,
START_HEIGHT_FAST..(START_HEIGHT_FAST + 2),
]
);
}
fn raw_histogram_accumulation_is_additive() {
let values = [
Sats::ZERO,
Sats::new(1),
Sats::new(10),
Sats::new(100_000_000),
Sats::new(1_000_000_000_000),
Sats::new(5_000_000_000),
];
#[test]
fn ema_config_segments_omits_empty_sides() {
let slow: Vec<_> = ema_config_segments((START_HEIGHT_FAST - 2)..START_HEIGHT_FAST).collect();
assert_eq!(slow, vec![(START_HEIGHT_FAST - 2)..START_HEIGHT_FAST]);
let mut one_shot = HistogramRaw::zeros();
add_sats_to_raw_histogram(&mut one_shot, &values);
let fast: Vec<_> =
ema_config_segments(START_HEIGHT_FAST..(START_HEIGHT_FAST + 2)).collect();
assert_eq!(fast, vec![START_HEIGHT_FAST..(START_HEIGHT_FAST + 2)]);
let mut chunked = HistogramRaw::zeros();
for chunk in values.chunks(2) {
add_sats_to_raw_histogram(&mut chunked, chunk);
}
assert!(one_shot.iter().eq(chunked.iter()));
}
}
+38
View File
@@ -66,6 +66,21 @@ impl<const N: usize> Histogram<f64, N> {
}
Histogram(out)
}
/// Add another floating-point histogram bin-by-bin.
#[inline]
pub fn add_from(&mut self, rhs: &Self) {
self.0
.iter_mut()
.zip(rhs.0.iter())
.for_each(|(a, &b)| *a += b);
}
/// Divide every bin by `rhs`.
#[inline]
pub fn divide_by(&mut self, rhs: f64) {
self.0.iter_mut().for_each(|v| *v /= rhs);
}
}
impl<T: Serialize, const N: usize> Serialize for Histogram<T, N> {
@@ -120,3 +135,26 @@ impl<T: JsonSchema, const N: usize> JsonSchema for Histogram<T, N> {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn float_histogram_add_and_divide_are_binwise() {
let mut a = Histogram::<f64, 3>::zeros();
a[0] = 1.0;
a[1] = 2.0;
a[2] = 3.0;
let mut b = Histogram::<f64, 3>::zeros();
b[0] = 3.0;
b[1] = 4.0;
b[2] = 5.0;
a.add_from(&b);
a.divide_by(2.0);
assert_eq!(*a, [2.0, 3.0, 4.0]);
}
}
+3 -3
View File
@@ -32,6 +32,9 @@ mod block_rewards_entry;
mod block_size_entry;
mod block_sizes_weights;
mod block_status;
mod block_template;
mod block_template_diff;
mod block_template_diff_entry;
mod block_timestamp;
mod block_tx_index;
mod block_weight_entry;
@@ -81,9 +84,6 @@ mod hour4;
mod index;
mod index_info;
mod limit;
mod block_template;
mod block_template_diff;
mod block_template_diff_entry;
mod mempool_block;
mod mempool_entry_info;
mod mempool_info;
+6 -1
View File
@@ -59,7 +59,12 @@ impl UrpdAggregation {
let size = self.linear_size_cents().unwrap();
(price_cents / size) * size
}
Self::Log10 | Self::Log50 | Self::Log100 | Self::Log200 | Self::Log500 | Self::Log1000 => {
Self::Log10
| Self::Log50
| Self::Log100
| Self::Log200
| Self::Log500
| Self::Log1000 => {
if price_cents == Cents::ZERO {
return Cents::ZERO;
}