heatmaps: part 14

This commit is contained in:
nym21
2026-06-01 12:01:24 +02:00
parent 102933b406
commit cb9f277d49
7 changed files with 92 additions and 110 deletions
+37 -31
View File
@@ -127,39 +127,45 @@ impl Vecs {
// Slow cold-start EMA up to START_HEIGHT_FAST, then switch to the fast
// mature-market EMA. Steady-state runs start past START_HEIGHT_FAST and skip
// the slow segment entirely.
let mut ref_bins = Vec::with_capacity(num_new);
if committed < START_HEIGHT_FAST {
let slow_end = START_HEIGHT_FAST.min(total_heights);
ref_bins.extend(Self::feed_blocks(
&mut oracle,
indexer,
committed..slow_end,
None,
));
if slow_end == START_HEIGHT_FAST {
oracle.reconfigure(Config::default());
{
let mut processed = 0usize;
let mut push_ref_bin = |ref_bin| {
self.spot
.cents
.height
.inner
.push(Cents::new(bin_to_cents(ref_bin)));
processed += 1;
let progress = (processed * 100 / num_new) as u8;
if processed > 1 && progress > (((processed - 1) * 100 / num_new) as u8) {
info!("Oracle price computation: {}%", progress);
}
};
if committed < START_HEIGHT_FAST {
let slow_end = START_HEIGHT_FAST.min(total_heights);
Self::feed_blocks_with(
&mut oracle,
indexer,
committed..slow_end,
None,
|_, _, ref_bin| push_ref_bin(ref_bin),
);
if slow_end == START_HEIGHT_FAST {
oracle.reconfigure(Config::default());
}
}
}
let fast_start = committed.max(START_HEIGHT_FAST);
if fast_start < total_heights {
ref_bins.extend(Self::feed_blocks(
&mut oracle,
indexer,
fast_start..total_heights,
None,
));
}
for (i, ref_bin) in ref_bins.into_iter().enumerate() {
self.spot
.cents
.height
.inner
.push(Cents::new(bin_to_cents(ref_bin)));
let progress = ((i + 1) * 100 / num_new) as u8;
if i > 0 && progress > ((i * 100 / num_new) as u8) {
info!("Oracle price computation: {}%", progress);
let fast_start = committed.max(START_HEIGHT_FAST);
if fast_start < total_heights {
Self::feed_blocks_with(
&mut oracle,
indexer,
fast_start..total_heights,
None,
|_, _, ref_bin| push_ref_bin(ref_bin),
);
}
}
+24 -58
View File
@@ -4,15 +4,13 @@ use brk_computer::prices::Vecs as PricesVecs;
use brk_error::{Error, Result};
use brk_indexer::Lengths;
use brk_oracle::{
Config, HistogramEma, HistogramEmaCompact, HistogramRaw, Oracle, cents_to_bin, sats_to_bin,
cents_to_bin, sats_to_bin, Config, HistogramEma, HistogramEmaCompact, HistogramRaw, Oracle,
};
use brk_types::{Day1, Dollars, Sats, TxOutIndex};
use brk_types::{Day1, Dollars, 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())
@@ -233,70 +231,38 @@ impl Query {
/// range instead of one block at a time.
fn output_histogram_for_blocks(&self, range: Range<usize>, safe: &Lengths) -> HistogramRaw {
let indexer = self.indexer();
let safe_height = safe.height.to_usize();
let total_outputs = safe.txout_index.to_usize();
let collect_end = (range.end + 1).min(safe.height.to_usize());
let out_firsts: Vec<TxOutIndex> = indexer
let out_start = indexer
.vecs
.outputs
.first_txout_index
.collect_range_at(range.start, collect_end);
let out_start = out_firsts[0].to_usize();
let out_end = out_firsts
.get(range.end - range.start)
.copied()
.unwrap_or(TxOutIndex::from(total_outputs))
.collect_one_at(range.start)
.unwrap()
.to_usize();
let mut hist = HistogramRaw::zeros();
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();
let out_end = if range.end < safe_height {
indexer
.vecs
.outputs
.value
.collect_range_into_at(start, end, &mut values);
add_sats_to_raw_histogram(&mut hist, &values);
start = end;
.first_txout_index
.collect_one_at(range.end)
.unwrap()
} else {
TxOutIndex::from(total_outputs)
}
.to_usize();
let mut hist = HistogramRaw::zeros();
indexer
.vecs
.outputs
.value
.for_each_range_at(out_start, out_end, |sats| {
if let Some(bin) = sats_to_bin(sats) {
hist.increment(bin);
}
});
hist
}
}
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)]
mod tests {
use super::*;
#[test]
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),
];
let mut one_shot = HistogramRaw::zeros();
add_sats_to_raw_histogram(&mut one_shot, &values);
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()));
}
}
+16 -15
View File
@@ -37,8 +37,7 @@ export function createAverageGrid({
const sums = new Float64Array(cols * rows);
const counts = new Uint32Array(cols * rows);
const maxByCol = new Float64Array(cols);
const cumulativeMaxByCol = new Float64Array(cols);
let cumulativeMaxDirty = true;
let maxValue = 0;
const ySpan = yMax - yMin;
/** @param {number} dateIndex */
@@ -85,7 +84,10 @@ export function createAverageGrid({
if (counts[index]) max = Math.max(max, sums[index] / counts[index]);
}
maxByCol[col] = max;
cumulativeMaxDirty = true;
maxValue = 0;
for (let c = 0; c < cols; c++) {
maxValue = Math.max(maxValue, maxByCol[c]);
}
}
/** @type {HeatmapGrid} */
@@ -99,7 +101,13 @@ export function createAverageGrid({
let dirty = false;
if (points.kind === "implicit") {
for (let i = 0; i < points.values.length; i++) {
if (addValue(col, points.yStart + i * points.yStep, points.values[i])) {
if (
addValue(
col,
points.yStart + i * points.yStep,
points.values[i],
)
) {
dirty = true;
}
}
@@ -110,8 +118,9 @@ export function createAverageGrid({
}
}
if (!dirty) return undefined;
const previousMax = maxValue;
updateColumnMax(col);
return col;
return { col, maxChanged: maxValue !== previousMax };
},
getValue(col, row) {
if (col < 0 || col >= cols || row < 0 || row >= rows) {
@@ -120,16 +129,8 @@ export function createAverageGrid({
const index = row * cols + col;
return counts[index] ? sums[index] / counts[index] : Number.NaN;
},
getMaxValue(col = cols - 1) {
if (cumulativeMaxDirty) {
let max = 0;
for (let c = 0; c < cols; c++) {
max = Math.max(max, maxByCol[c]);
cumulativeMaxByCol[c] = max;
}
cumulativeMaxDirty = false;
}
return cumulativeMaxByCol[clamp(col, 0, cols - 1)] ?? 0;
getMaxValue() {
return maxValue;
},
getDateIndexRange(col) {
if (col < 0 || col >= cols || dates.length === 0) {
+7 -2
View File
@@ -248,8 +248,13 @@ function rebuildGrid() {
*/
function addDateToGrid(dateIndex, points) {
if (!currentGrid) return;
const dirtyCol = currentGrid.add(dateIndex, points);
if (dirtyCol !== undefined) schedulePaint(dirtyCol);
const result = currentGrid.add(dateIndex, points);
if (!result) return;
if (result.maxChanged) {
paint();
} else {
schedulePaint(result.col);
}
}
/**
+1 -1
View File
@@ -38,7 +38,7 @@ export function intensityColor({ light, dark }) {
export function logIntensityColor({ light, dark }) {
return (value, context) => {
if (!Number.isFinite(value) || value <= 0) return 0x00000000;
const max = context.grid.getMaxValue(context.col);
const max = context.grid.getMaxValue();
if (max <= 0) return 0x00000000;
const lut = context.dark ? dark : light;
const t = Math.log2(value + 1) / Math.log2(max + 1);
+1 -1
View File
@@ -44,7 +44,7 @@ function createOracleHeatmapOption(mode, name) {
kind: "heatmap",
name,
title:
mode === "outputs" ? "Output Value Histogram" : "Payment Output Histogram",
mode === "outputs" ? "Output Value Histogram" : "Payment Value Histogram",
points: {
fetch: (date, signal, onPoints) =>
fetchOraclePoints(mode, date, signal, onPoints),
+6 -2
View File
@@ -19,13 +19,17 @@
* @property {number} start
* @property {number} end
*
* @typedef {Object} HeatmapGridAddResult
* @property {number} col
* @property {boolean} maxChanged
*
* @typedef {Object} HeatmapGrid
* @property {readonly string[]} dates
* @property {number} cols
* @property {number} rows
* @property {(dateIndex: number, points: HeatmapPoints) => number | undefined} add
* @property {(dateIndex: number, points: HeatmapPoints) => HeatmapGridAddResult | undefined} add
* @property {(col: number, row: number) => number} getValue
* @property {(col?: number) => number} getMaxValue
* @property {() => number} getMaxValue
* @property {(col: number) => HeatmapRange} getDateIndexRange
* @property {(row: number) => HeatmapRange} getYRange
*