computer: make aggr p2a less brittle

This commit is contained in:
nym21
2025-12-03 00:30:02 +01:00
parent 60c73f5635
commit f48ad577d3
2 changed files with 133 additions and 109 deletions
+119 -109
View File
@@ -1457,15 +1457,32 @@ impl Vecs {
let prev_timestamp = chain_state.last().unwrap().timestamp; let prev_timestamp = chain_state.last().unwrap().timestamp;
let mut vecs = self // Extract all mutable references upfront to avoid borrow checker issues
.0 // Use a single destructuring to get non-overlapping mutable borrows
.age_range let UTXOGroups {
all,
term,
age_range,
..
} = &mut self.0;
let mut vecs = age_range
.iter_mut() .iter_mut()
.map(|v| (v.filter().clone(), &mut v.state)) .map(|v| (v.filter().clone(), &mut v.state))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut sth_p2a = self.0.term.short.price_to_amount.as_mut(); // Collect aggregate cohorts' filter and p2a for age transitions
let mut lth_p2a = self.0.term.long.price_to_amount.as_mut(); let mut aggregate_p2a: Vec<(Filter, Option<&mut crate::PriceToAmount>)> = vec![
(all.filter().clone(), all.price_to_amount.as_mut()),
(
term.short.filter().clone(),
term.short.price_to_amount.as_mut(),
),
(
term.long.filter().clone(),
term.long.price_to_amount.as_mut(),
),
];
let _ = chain_state let _ = chain_state
.iter() .iter()
@@ -1495,19 +1512,23 @@ impl Vecs {
} }
}); });
// Handle STH -> LTH transitions for price_to_amount // Handle age transitions for aggregate cohorts' price_to_amount
let prev_was_sth = prev_days_old < Term::THRESHOLD_DAYS; // Check which cohorts the UTXO was in vs is now in, and increment/decrement accordingly
let now_is_sth = days_old < Term::THRESHOLD_DAYS; if let Some(price) = block_state.price {
aggregate_p2a.iter_mut().for_each(|(filter, p2a)| {
let is = filter.contains_time(days_old);
let was = filter.contains_time(prev_days_old);
if prev_was_sth && !now_is_sth { if is && !was {
if let Some(price) = block_state.price { if let Some(p2a) = p2a.as_mut() {
if let Some(p2a) = sth_p2a.as_mut() { p2a.increment(price, &block_state.supply);
p2a.decrement(price, &block_state.supply); }
} else if was && !is {
if let Some(p2a) = p2a.as_mut() {
p2a.decrement(price, &block_state.supply);
}
} }
if let Some(p2a) = lth_p2a.as_mut() { });
p2a.increment(price, &block_state.supply);
}
}
} }
ControlFlow::Continue(()) ControlFlow::Continue(())
@@ -1519,12 +1540,31 @@ impl Vecs {
height_to_sent: FxHashMap<Height, Transacted>, height_to_sent: FxHashMap<Height, Transacted>,
chain_state: &mut [BlockState], chain_state: &mut [BlockState],
) { ) {
let mut time_based_vecs = self // Extract all mutable references upfront to avoid borrow checker issues
.0 let UTXOGroups {
.age_range all,
.iter_mut() term,
.chain(self.0.epoch.iter_mut()) age_range,
.collect::<Vec<_>>(); epoch,
type_,
amount_range,
..
} = &mut self.0;
let mut time_based_vecs = age_range.iter_mut().chain(epoch.iter_mut()).collect::<Vec<_>>();
// Collect aggregate cohorts' filter and p2a for iteration
let mut aggregate_p2a: Vec<(Filter, Option<&mut crate::PriceToAmount>)> = vec![
(all.filter().clone(), all.price_to_amount.as_mut()),
(
term.short.filter().clone(),
term.short.price_to_amount.as_mut(),
),
(
term.long.filter().clone(),
term.long.price_to_amount.as_mut(),
),
];
let last_timestamp = chain_state.last().unwrap().timestamp; let last_timestamp = chain_state.last().unwrap().timestamp;
let current_price = chain_state.last().unwrap().price; let current_price = chain_state.last().unwrap().price;
@@ -1573,8 +1613,7 @@ impl Vecs {
.spendable .spendable
.iter_typed() .iter_typed()
.for_each(|(output_type, supply_state)| { .for_each(|(output_type, supply_state)| {
self.0 type_
.type_
.get_mut(output_type) .get_mut(output_type)
.state .state
.as_mut() .as_mut()
@@ -1592,8 +1631,7 @@ impl Vecs {
sent.by_size_group sent.by_size_group
.iter_typed() .iter_typed()
.for_each(|(group, supply_state)| { .for_each(|(group, supply_state)| {
self.0 amount_range
.amount_range
.get_mut(group) .get_mut(group)
.state .state
.as_mut() .as_mut()
@@ -1608,23 +1646,17 @@ impl Vecs {
); );
}); });
// Update aggregate cohorts' price_to_amount // Update aggregate cohorts' price_to_amount using filter.contains_time()
// All sends decrement from "all", and either from "sth" or "lth" based on age
if let Some(prev_price) = prev_price { if let Some(prev_price) = prev_price {
let supply_state = &sent.spendable_supply; let supply_state = &sent.spendable_supply;
if supply_state.value.is_not_zero() { if supply_state.value.is_not_zero() {
const STH_THRESHOLD: usize = Term::THRESHOLD_DAYS; aggregate_p2a.iter_mut().for_each(|(filter, p2a)| {
if filter.contains_time(days_old) {
if let Some(p2a) = self.0.all.price_to_amount.as_mut() { if let Some(p2a) = p2a.as_mut() {
p2a.decrement(prev_price, supply_state); p2a.decrement(prev_price, supply_state);
} }
if days_old < STH_THRESHOLD {
if let Some(p2a) = self.0.term.short.price_to_amount.as_mut() {
p2a.decrement(prev_price, supply_state);
} }
} else if let Some(p2a) = self.0.term.long.price_to_amount.as_mut() { });
p2a.decrement(prev_price, supply_state);
}
} }
} }
}); });
@@ -1643,16 +1675,17 @@ impl Vecs {
}); });
// Update aggregate cohorts' price_to_amount // Update aggregate cohorts' price_to_amount
// New UTXOs are always part of "all" and are always < 150 days old (so part of "sth") // New UTXOs have days_old = 0, so use filter.contains_time(0) to check applicability
if let Some(price) = price if let Some(price) = price
&& supply_state.value.is_not_zero() && supply_state.value.is_not_zero()
{ {
if let Some(p2a) = self.0.all.price_to_amount.as_mut() { self.0.iter_aggregate_mut().for_each(|v| {
p2a.increment(price, &supply_state); if v.filter().contains_time(0) {
} if let Some(p2a) = v.price_to_amount.as_mut() {
if let Some(p2a) = self.0.term.short.price_to_amount.as_mut() { p2a.increment(price, &supply_state);
p2a.increment(price, &supply_state); }
} }
});
} }
self.type_.iter_mut().for_each(|vecs| { self.type_.iter_mut().for_each(|vecs| {
@@ -1791,81 +1824,58 @@ impl Vecs {
.try_for_each(|v| v.safe_flush_stateful_vecs(height, exit))?; .try_for_each(|v| v.safe_flush_stateful_vecs(height, exit))?;
// Flush aggregate cohorts' price_to_amount // Flush aggregate cohorts' price_to_amount
if let Some(p2a) = self.0.all.price_to_amount.as_mut() { self.0.iter_aggregate_mut().try_for_each(|v| {
p2a.flush(height)?; if let Some(p2a) = v.price_to_amount.as_mut() {
} p2a.flush(height)?;
if let Some(p2a) = self.0.term.short.price_to_amount.as_mut() { }
p2a.flush(height)?; Ok(())
} })
if let Some(p2a) = self.0.term.long.price_to_amount.as_mut() {
p2a.flush(height)?;
}
Ok(())
} }
/// Reset aggregate cohorts' price_to_amount when starting from scratch /// Reset aggregate cohorts' price_to_amount when starting from scratch
pub fn reset_aggregate_price_to_amount(&mut self) -> Result<()> { pub fn reset_aggregate_price_to_amount(&mut self) -> Result<()> {
if let Some(p2a) = self.0.all.price_to_amount.as_mut() { self.0.iter_aggregate_mut().try_for_each(|v| {
p2a.clean()?; if let Some(p2a) = v.price_to_amount.as_mut() {
p2a.init(); p2a.clean()?;
} p2a.init();
if let Some(p2a) = self.0.term.short.price_to_amount.as_mut() { }
p2a.clean()?; Ok(())
p2a.init(); })
}
if let Some(p2a) = self.0.term.long.price_to_amount.as_mut() {
p2a.clean()?;
p2a.init();
}
Ok(())
} }
/// Compute and push percentiles for aggregate cohorts (all, sth, lth). /// Compute and push percentiles for aggregate cohorts (all, sth, lth).
/// Must be called after receive()/send() when price_to_amount is up to date. /// Must be called after receive()/send() when price_to_amount is up to date.
pub fn truncate_push_aggregate_percentiles(&mut self, height: Height) -> Result<()> { pub fn truncate_push_aggregate_percentiles(&mut self, height: Height) -> Result<()> {
// Helper to compute supply by summing age_range cohorts matching a filter // First, compute supplies for each aggregate cohort by summing age_range sub-cohorts
let compute_supply = |filter: &Filter| -> Sats { let supplies: Vec<(Filter, Sats)> = self
self.0 .0
.age_range .iter_aggregate()
.iter() .map(|v| {
.filter(|v| filter.includes(v.filter())) let filter = v.filter().clone();
.map(|v| v.state.as_ref().unwrap().supply.value) let supply = self
.fold(Sats::ZERO, |acc, v| acc + v) .0
}; .age_range
.iter()
.filter(|sub| filter.includes(sub.filter()))
.map(|sub| sub.state.as_ref().unwrap().supply.value)
.fold(Sats::ZERO, |acc, v| acc + v);
(filter, supply)
})
.collect();
// Compute and push percentiles for "all" // Then, compute and push percentiles for each aggregate cohort
if self.0.all.price_to_amount.is_some() { for (filter, supply) in supplies {
let supply = compute_supply(self.0.all.filter()); let v = self
let percentiles = self.0.all.compute_percentile_prices_from_standalone(supply);
if let Some(pp) = self.0.all.inner.price_percentiles.as_mut() {
pp.truncate_push(height, &percentiles)?;
}
}
// Compute and push percentiles for "sth"
if self.0.term.short.price_to_amount.is_some() {
let supply = compute_supply(self.0.term.short.filter());
let percentiles = self
.0 .0
.term .iter_aggregate_mut()
.short .find(|v| v.filter() == &filter)
.compute_percentile_prices_from_standalone(supply); .unwrap();
if let Some(pp) = self.0.term.short.inner.price_percentiles.as_mut() {
pp.truncate_push(height, &percentiles)?;
}
}
// Compute and push percentiles for "lth" if v.price_to_amount.is_some() {
if self.0.term.long.price_to_amount.is_some() { let percentiles = v.compute_percentile_prices_from_standalone(supply);
let supply = compute_supply(self.0.term.long.filter()); if let Some(pp) = v.inner.price_percentiles.as_mut() {
let percentiles = self pp.truncate_push(height, &percentiles)?;
.0 }
.term
.long
.compute_percentile_prices_from_standalone(supply);
if let Some(pp) = self.0.term.long.inner.price_percentiles.as_mut() {
pp.truncate_push(height, &percentiles)?;
} }
} }
+14
View File
@@ -95,4 +95,18 @@ impl<T> UTXOGroups<T> {
.chain(self.lt_amount.iter_mut()) .chain(self.lt_amount.iter_mut())
.chain(self.ge_amount.iter_mut()) .chain(self.ge_amount.iter_mut())
} }
/// Iterator over aggregate cohorts (all, sth, lth) that compute values from sub-cohorts.
/// These are cohorts with StateLevel::PriceOnly that derive values from stateful sub-cohorts.
pub fn iter_aggregate(&self) -> impl Iterator<Item = &T> {
[&self.all].into_iter().chain(self.term.iter())
}
/// Iterator over aggregate cohorts (all, sth, lth) that compute values from sub-cohorts.
/// These are cohorts with StateLevel::PriceOnly that derive values from stateful sub-cohorts.
pub fn iter_aggregate_mut(&mut self) -> impl Iterator<Item = &mut T> {
[&mut self.all]
.into_iter()
.chain(self.term.iter_mut())
}
} }