use std::fmt;
use std::num::NonZeroUsize;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::Arc;
use std::time::{Duration, Instant};
use rand::{Rng, SeedableRng, rngs::SmallRng};
#[cfg(feature = "research")]
pub use crate::walnutpie::NonfinitePositionPolicy;
#[cfg(feature = "research")]
pub use crate::walnutpie::ReverseCoarseningOrder;
pub use crate::walnutpie::{
Cancellation, ChainOutput, Error, ErrorKind, PaperAdaptationConfig, RunMetadata, RunTelemetry,
StructuredBlockMass, StructuredCovarianceBlock, StructuredMetricRefresh,
StructuredRefreshConfig, StructuredRefreshUpdate, Target, TargetError, WarmupConfig,
WindowSummary,
};
use crate::walnutpie::{
ChainRescueConfig, DEFAULT_DIVERGENCE_THRESHOLD, DenseMass, DiagonalMass,
DiagonalMetricRegularization, ExhaustionRule, KernelOptions, KernelTuning, MultiChainOutput,
RunConfig, RunControl, TargetEvaluationAdmissionLimit, TargetEvaluationBudget, UTurnRule,
sample_chains_dense_with_control, sample_chains_dense_with_target_budget_and_control,
sample_chains_structured_refresh, sample_chains_structured_with_control,
sample_chains_with_control, sample_chains_with_target_budget_and_control,
};
#[non_exhaustive]
pub enum Metric {
Identity,
Diagonal {
adapt: bool,
initial: Option<Vec<f64>>,
},
Dense {
adapt: bool,
initial: Option<Vec<f64>>,
},
Structured(StructuredBlockMass),
StructuredRefresh {
initial: StructuredBlockMass,
refresh: Box<dyn StructuredMetricRefresh>,
config: StructuredRefreshConfig,
},
}
impl Metric {
pub fn diagonal() -> Self {
Self::Diagonal {
adapt: true,
initial: None,
}
}
pub fn fixed_diagonal(diagonal: Vec<f64>) -> Self {
Self::Diagonal {
adapt: false,
initial: Some(diagonal),
}
}
pub fn dense() -> Self {
Self::Dense {
adapt: true,
initial: None,
}
}
pub fn fixed_dense(matrix: Vec<f64>) -> Self {
Self::Dense {
adapt: false,
initial: Some(matrix),
}
}
pub fn structured_refresh(
initial: StructuredBlockMass,
refresh: impl StructuredMetricRefresh + 'static,
) -> Self {
Self::StructuredRefresh {
initial,
refresh: Box::new(refresh),
config: StructuredRefreshConfig::default(),
}
}
fn adapts_mass(&self) -> bool {
match self {
Self::Identity | Self::Structured(_) => false,
Self::Diagonal { adapt, .. } | Self::Dense { adapt, .. } => *adapt,
Self::StructuredRefresh { .. } => true,
}
}
fn supports_chain_rescue(&self) -> bool {
matches!(self, Self::Identity | Self::Diagonal { .. })
}
}
impl Default for Metric {
fn default() -> Self {
Self::diagonal()
}
}
impl fmt::Debug for Metric {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Identity => f.write_str("Identity"),
Self::Diagonal { adapt, initial } => f
.debug_struct("Diagonal")
.field("adapt", adapt)
.field("initial", initial)
.finish(),
Self::Dense { adapt, initial } => f
.debug_struct("Dense")
.field("adapt", adapt)
.field("initial", initial)
.finish(),
Self::Structured(mass) => f.debug_tuple("Structured").field(mass).finish(),
Self::StructuredRefresh {
initial, config, ..
} => f
.debug_struct("StructuredRefresh")
.field("initial", initial)
.field("config", config)
.finish_non_exhaustive(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum Adaptation {
None,
DualAveraging {
target_accept: f64,
},
Paper(PaperAdaptationConfig),
Custom(WarmupConfig),
}
impl Default for Adaptation {
fn default() -> Self {
Self::DualAveraging { target_accept: 0.8 }
}
}
pub const DEFAULT_WARMUP_EXHAUSTION: ExhaustionRule = ExhaustionRule::AcceptUnlessDivergent;
pub const DEFAULT_METRIC_REGULARIZATION: DiagonalMetricRegularization =
DiagonalMetricRegularization::Stan;
pub const DEFAULT_U_TURN_RULE: UTurnRule = UTurnRule::MomentumSum;
pub const DEFAULT_CHAIN_RESCUE: Option<ChainRescueConfig> = None;
impl Adaptation {
fn warmup_config(
&self,
adapt_mass: bool,
supports_chain_rescue: bool,
) -> Result<Option<WarmupConfig>, Error> {
let with_defaults = |warmup: WarmupConfig| {
let warmup = warmup
.with_mass_adaptation(adapt_mass)
.with_warmup_exhaustion_rule(DEFAULT_WARMUP_EXHAUSTION)
.with_metric_regularization(DEFAULT_METRIC_REGULARIZATION);
match (supports_chain_rescue, DEFAULT_CHAIN_RESCUE) {
(true, Some(rescue)) => warmup.with_chain_rescue(rescue),
_ => warmup,
}
};
Ok(match self {
Self::None => {
if adapt_mass {
return Err(Error::configuration(
"an adapting metric requires an adaptation mode",
));
}
None
}
Self::DualAveraging { target_accept } => {
Some(with_defaults(WarmupConfig::new(*target_accept)?))
}
Self::Paper(paper) => Some(with_defaults(
WarmupConfig::default().with_paper_adaptation(*paper),
)),
Self::Custom(warmup) => Some(warmup.clone().with_mass_adaptation(adapt_mass)),
})
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum Init {
Given(Vec<Vec<f64>>),
Uniform {
radius: f64,
max_attempts: usize,
},
}
pub const INIT_SEED_TAG: u64 = 0x5eed_1417_0000_0000_u64;
impl Init {
pub fn uniform() -> Self {
Self::Uniform {
radius: 2.0,
max_attempts: 100,
}
}
}
impl Default for Init {
fn default() -> Self {
Self::uniform()
}
}
pub fn uniform_starts<T: Target + ?Sized>(
target: &T,
chains: usize,
seed: u64,
radius: f64,
max_attempts: usize,
) -> Result<Vec<Vec<f64>>, Error> {
if !radius.is_finite() || radius <= 0.0 {
return Err(Error::configuration(
"initialisation radius must be finite and positive",
));
}
if max_attempts == 0 {
return Err(Error::configuration(
"initialisation must allow at least one attempt",
));
}
if chains == 0 {
return Err(Error::configuration("chain count must be nonzero"));
}
let dimension = catch_unwind(AssertUnwindSafe(|| target.dimension()))
.map_err(|_| Error::new(ErrorKind::Panic, "target dimension callback panicked"))?;
if dimension == 0 {
return Err(Error::configuration("target dimension must be nonzero"));
}
let mut rng = SmallRng::seed_from_u64(crate::walnutpie::splitmix64(seed ^ INIT_SEED_TAG));
let mut gradient = vec![0.0; dimension];
let mut starts = Vec::with_capacity(chains);
for chain in 0..chains {
let mut last_failure = String::new();
let mut found = None;
for _attempt in 0..max_attempts {
let candidate: Vec<f64> = (0..dimension)
.map(|_| rng.random_range(-radius..radius))
.collect();
gradient.iter_mut().for_each(|g| *g = f64::NAN);
let evaluated = catch_unwind(AssertUnwindSafe(|| {
target.log_density_gradient(&candidate, &mut gradient)
}))
.map_err(|_| Error::new(ErrorKind::Panic, "target callback panicked"))?;
match evaluated {
Ok(value) if value.is_finite() && gradient.iter().all(|g| g.is_finite()) => {
found = Some(candidate);
break;
}
Ok(value) => {
last_failure = if value.is_finite() {
String::from("gradient is not finite")
} else {
format!("log density is {value}")
};
}
Err(error) if error.kind() == crate::walnutpie::TargetErrorKind::Fatal => {
return Err(Error::new(
ErrorKind::Target,
format!(
"fatal target error while drawing the start of chain {chain}: {}",
error.message()
),
));
}
Err(error) => last_failure = error.message().to_owned(),
}
}
match found {
Some(start) => starts.push(start),
None => {
return Err(Error::new(
ErrorKind::Numerical,
format!(
"no evaluable start for chain {chain} after {max_attempts} uniform(-{radius}, \
{radius}) draws (last failure: {last_failure}); the log density and \
gradient must be finite at the start, check the model or supply starts"
),
));
}
}
}
Ok(starts)
}
#[derive(Clone, Debug, PartialEq)]
pub struct Tuning {
step_size: f64,
max_depth: usize,
min_micro_steps: usize,
max_refinement_levels: usize,
max_error: f64,
divergence_threshold: f64,
kernel_options: KernelOptions,
#[cfg(feature = "research")]
reverse_coarsening_order: ReverseCoarseningOrder,
#[cfg(feature = "research")]
nonfinite_position: NonfinitePositionPolicy,
}
impl Default for Tuning {
fn default() -> Self {
Self {
step_size: 0.5,
max_depth: 10,
min_micro_steps: 1,
max_refinement_levels: 8,
max_error: 1.0,
divergence_threshold: DEFAULT_DIVERGENCE_THRESHOLD,
kernel_options: KernelOptions {
u_turn: DEFAULT_U_TURN_RULE,
..KernelOptions::default()
},
#[cfg(feature = "research")]
reverse_coarsening_order: ReverseCoarseningOrder::FinestToCoarsest,
#[cfg(feature = "research")]
nonfinite_position: NonfinitePositionPolicy::Abort,
}
}
}
impl Tuning {
pub fn new() -> Self {
Self::default()
}
pub fn step_size(mut self, step_size: f64) -> Self {
self.step_size = step_size;
self
}
pub fn max_depth(mut self, max_depth: usize) -> Self {
self.max_depth = max_depth;
self
}
pub fn min_micro_steps(mut self, min_micro_steps: usize) -> Self {
self.min_micro_steps = min_micro_steps;
self
}
pub fn max_refinement_levels(mut self, levels: usize) -> Self {
self.max_refinement_levels = levels;
self
}
pub fn max_error(mut self, max_error: f64) -> Self {
self.max_error = max_error;
self
}
pub fn divergence_threshold(mut self, threshold: f64) -> Self {
self.divergence_threshold = threshold;
self
}
pub fn kernel_options(mut self, options: KernelOptions) -> Self {
self.kernel_options = options;
self
}
#[cfg(feature = "research")]
pub fn reverse_coarsening_order(mut self, order: ReverseCoarseningOrder) -> Self {
self.reverse_coarsening_order = order;
self
}
#[cfg(feature = "research")]
pub fn nonfinite_position(mut self, policy: NonfinitePositionPolicy) -> Self {
self.nonfinite_position = policy;
self
}
pub fn to_kernel(&self) -> Result<KernelTuning, Error> {
let nonzero = |value: usize, what: &str| {
NonZeroUsize::new(value)
.ok_or_else(|| Error::configuration(format!("{what} must be nonzero")))
};
let tuning = KernelTuning::new(
self.step_size,
nonzero(self.max_depth, "max_depth")?,
nonzero(self.min_micro_steps, "min_micro_steps")?,
nonzero(self.max_refinement_levels, "max_refinement_levels")?,
self.max_error,
)?
.with_divergence_threshold(self.divergence_threshold)
.map(|tuning| tuning.with_options(self.kernel_options))?;
#[cfg(feature = "research")]
let tuning = tuning.with_reverse_coarsening_order(self.reverse_coarsening_order);
#[cfg(feature = "research")]
let tuning = tuning.with_nonfinite_position(self.nonfinite_position);
Ok(tuning)
}
}
#[derive(Clone)]
pub struct Limits {
max_target_evaluations: Option<NonZeroUsize>,
admit_worst_case: bool,
deadline: Option<Instant>,
timeout: Option<Duration>,
cancellation: Option<Arc<dyn Cancellation>>,
max_depth_stops: Option<usize>,
}
impl Default for Limits {
fn default() -> Self {
Self {
max_target_evaluations: None,
admit_worst_case: true,
deadline: None,
timeout: None,
cancellation: None,
max_depth_stops: None,
}
}
}
impl Limits {
pub fn new() -> Self {
Self::default()
}
pub fn max_target_evaluations(mut self, evaluations: usize) -> Self {
self.max_target_evaluations = NonZeroUsize::new(evaluations);
self
}
pub fn admit_worst_case(mut self) -> Self {
self.admit_worst_case = true;
self
}
pub fn admit_conservative(mut self) -> Self {
self.admit_worst_case = false;
self
}
pub fn deadline(mut self, deadline: Instant) -> Self {
self.deadline = Some(deadline);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
pub fn cancellation(mut self, cancellation: Arc<dyn Cancellation>) -> Self {
self.cancellation = Some(cancellation);
self
}
pub fn max_depth_stops(mut self, stops: usize) -> Self {
self.max_depth_stops = Some(stops);
self
}
}
impl fmt::Debug for Limits {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Limits")
.field("max_target_evaluations", &self.max_target_evaluations)
.field("admit_worst_case", &self.admit_worst_case)
.field("deadline", &self.deadline)
.field("timeout", &self.timeout)
.field("cancellation", &self.cancellation.as_ref().map(|_| ".."))
.field("max_depth_stops", &self.max_depth_stops)
.finish()
}
}
pub const DEFAULT_RANDOM_START_CHAINS: usize = 4;
#[derive(Debug)]
pub struct Sampler {
warmup: usize,
draws: usize,
chains: Option<usize>,
seed: u64,
threads: Option<usize>,
metric: Metric,
adaptation: Adaptation,
tuning: Tuning,
limits: Limits,
cache_initial_evaluation: bool,
}
impl Default for Sampler {
fn default() -> Self {
Self {
warmup: 1_000,
draws: 1_000,
chains: None,
seed: 0,
threads: None,
metric: Metric::default(),
adaptation: Adaptation::default(),
tuning: Tuning::default(),
limits: Limits::default(),
cache_initial_evaluation: true,
}
}
}
impl Sampler {
pub fn new() -> Self {
Self::default()
}
pub fn warmup(mut self, transitions: usize) -> Self {
self.warmup = transitions;
self
}
pub fn draws(mut self, draws: usize) -> Self {
self.draws = draws;
self
}
pub fn chains(mut self, chains: usize) -> Self {
self.chains = Some(chains);
self
}
pub fn seed(mut self, seed: u64) -> Self {
self.seed = seed;
self
}
pub fn threads(mut self, threads: usize) -> Self {
self.threads = Some(threads);
self
}
pub fn metric(mut self, metric: Metric) -> Self {
self.metric = metric;
self
}
pub fn adaptation(mut self, adaptation: Adaptation) -> Self {
self.adaptation = adaptation;
self
}
pub fn tuning(mut self, tuning: Tuning) -> Self {
self.tuning = tuning;
self
}
pub fn limits(mut self, limits: Limits) -> Self {
self.limits = limits;
self
}
pub fn cache_initial_evaluation(mut self, enabled: bool) -> Self {
self.cache_initial_evaluation = enabled;
self
}
pub fn worst_case_target_evaluations(&self, chains: usize) -> Result<usize, Error> {
let chains = NonZeroUsize::new(chains)
.ok_or_else(|| Error::configuration("chain count must be nonzero"))?;
self.run_config()?.worst_case_target_evaluations(chains)
}
fn run_config(&self) -> Result<RunConfig, Error> {
let draws = NonZeroUsize::new(self.draws)
.ok_or_else(|| Error::configuration("draws must be nonzero"))?;
let mut config = RunConfig::new(self.warmup, draws, self.seed)
.with_tuning(self.tuning.to_kernel()?)
.with_cached_initial_evaluation(self.cache_initial_evaluation);
if let Some(warmup) = self.adaptation.warmup_config(
self.metric.adapts_mass(),
self.metric.supports_chain_rescue(),
)? {
config = config.with_warmup(warmup);
}
if let Some(limit) = self.limits.max_depth_stops {
config = config.with_maximum_depth_stop_limit(limit);
}
Ok(config)
}
fn admit_structured(
&self,
config: RunConfig,
budget: Option<NonZeroUsize>,
chains: NonZeroUsize,
) -> Result<RunConfig, Error> {
#[cfg(feature = "research")]
{
use crate::walnutpie::{
CONSERVATIVE_MAX_TARGET_EVALUATIONS, RESEARCH_MAX_TARGET_EVALUATIONS,
ResearchTargetEvaluationLimit,
};
if let (Metric::Structured(_) | Metric::StructuredRefresh { .. }, Some(budget)) =
(&self.metric, budget)
{
let worst = config.worst_case_target_evaluations(chains)?;
let ceiling = budget.get().min(RESEARCH_MAX_TARGET_EVALUATIONS);
if worst > CONSERVATIVE_MAX_TARGET_EVALUATIONS && worst <= ceiling {
let limit = ResearchTargetEvaluationLimit::new(
NonZeroUsize::new(ceiling).expect("ceiling above the conservative limit"),
)?;
return Ok(config.with_research_target_evaluation_limit(limit));
}
}
}
#[cfg(not(feature = "research"))]
let _ = (budget, chains);
Ok(config)
}
fn starts(&self, starts: &[Vec<f64>]) -> Result<Vec<Vec<f64>>, Error> {
match self.chains {
None => Ok(starts.to_vec()),
Some(chains) if chains == starts.len() => Ok(starts.to_vec()),
Some(chains) if starts.len() == 1 && chains > 0 => Ok(vec![starts[0].clone(); chains]),
Some(_) => Err(Error::configuration(
"chain count must equal the number of starts, or exactly one start must be given",
)),
}
}
pub fn run_with_init<T: Target>(&self, target: &T, init: &Init) -> Result<Posterior, Error> {
match init {
Init::Given(starts) => self.run(target, starts),
Init::Uniform {
radius,
max_attempts,
} => {
let chains = self.chains.unwrap_or(DEFAULT_RANDOM_START_CHAINS);
let starts = uniform_starts(target, chains, self.seed, *radius, *max_attempts)?;
self.run(target, &starts)
}
}
}
pub fn run_from_random_starts<T: Target>(&self, target: &T) -> Result<Posterior, Error> {
self.run_with_init(target, &Init::uniform())
}
pub fn run<T: Target>(&self, target: &T, starts: &[Vec<f64>]) -> Result<Posterior, Error> {
self.run_with_control_value(target, starts, RunControl::new())
}
#[cfg(feature = "research")]
pub fn run_with_comparison_observers<T: Target>(
&self,
target: &T,
starts: &[Vec<f64>],
proposals: &crate::walnutpie::ProposalObservationControl<'_>,
comparisons: &dyn crate::walnutpie::ComparisonObserver,
) -> Result<Posterior, Error> {
self.run_with_control_value(
target,
starts,
RunControl::new()
.with_proposal_observations(proposals)
.with_comparison_observer(comparisons),
)
}
fn run_with_control_value<'a, T: Target>(
&self,
target: &T,
starts: &[Vec<f64>],
base_control: RunControl<'a>,
) -> Result<Posterior, Error> {
let config = self.run_config()?;
let starts = self.starts(starts)?;
let chains = NonZeroUsize::new(starts.len())
.ok_or_else(|| Error::configuration("at least one start is required"))?;
let threads = NonZeroUsize::new(self.threads.unwrap_or(chains.get()))
.ok_or_else(|| Error::configuration("thread count must be nonzero"))?;
let dimension = catch_unwind(AssertUnwindSafe(|| target.dimension()))
.map_err(|_| Error::new(ErrorKind::Panic, "target dimension callback panicked"))?;
let dimension = NonZeroUsize::new(dimension)
.ok_or_else(|| Error::configuration("target dimension must be nonzero"))?;
let mut control = base_control;
if let Some(cancellation) = &self.limits.cancellation {
control = control.with_cancellation(&**cancellation);
}
if let Some(deadline) = self.limits.deadline {
control = control.with_deadline(deadline);
}
if let Some(timeout) = self.limits.timeout {
control = control.with_timeout(timeout)?;
}
let budget_size = match (
self.limits.max_target_evaluations,
self.limits.admit_worst_case,
) {
(Some(evaluations), _) => Some(evaluations),
(None, true) => Some(
NonZeroUsize::new(config.worst_case_target_evaluations(chains)?)
.ok_or_else(|| Error::configuration("worst case is zero"))?,
),
(None, false) => None,
};
let budget = budget_size.map(TargetEvaluationBudget::new);
let config = self.admit_structured(config, budget_size, chains)?;
let output = match &self.metric {
Metric::Identity => {
let mass = DiagonalMass::identity(dimension);
run_diagonal(target, &starts, &mass, &config, threads, &control, budget)?
}
Metric::Diagonal { initial, .. } => {
let mass = match initial {
Some(diagonal) => DiagonalMass::from_diagonal(diagonal.clone())?,
None => DiagonalMass::identity(dimension),
};
run_diagonal(target, &starts, &mass, &config, threads, &control, budget)?
}
Metric::Dense { initial, .. } => {
let mass = match initial {
Some(matrix) => DenseMass::from_matrix(matrix.clone(), dimension.get())?,
None => DenseMass::identity(dimension)?,
};
match &budget {
Some(budget) => sample_chains_dense_with_target_budget_and_control(
target,
&starts,
&mass,
&config,
threads,
TargetEvaluationAdmissionLimit::new(budget_size.expect("budgeted")),
budget,
&control,
)?,
None => sample_chains_dense_with_control(
target, &starts, &mass, &config, threads, &control,
)?,
}
}
Metric::Structured(mass) => match &budget {
Some(budget) => sample_chains_structured_with_control(
&budget.wrap(target),
&starts,
mass,
&config,
threads,
&control,
)?,
None => sample_chains_structured_with_control(
target, &starts, mass, &config, threads, &control,
)?,
},
Metric::StructuredRefresh {
initial,
refresh,
config: refresh_config,
} => {
let refreshed = match &budget {
Some(budget) => sample_chains_structured_refresh(
&budget.wrap(target),
&starts,
initial,
&**refresh,
refresh_config,
&config,
threads,
&control,
)?,
None => sample_chains_structured_refresh(
target,
&starts,
initial,
&**refresh,
refresh_config,
&config,
threads,
&control,
)?,
};
let (output, metric_updates, final_masses) = refreshed.into_parts();
return Ok(Posterior {
output,
metric_updates,
final_masses,
});
}
};
Ok(Posterior {
output,
metric_updates: Vec::new(),
final_masses: Vec::new(),
})
}
}
#[allow(clippy::too_many_arguments)]
fn run_diagonal<T: Target>(
target: &T,
starts: &[Vec<f64>],
mass: &DiagonalMass,
config: &RunConfig,
threads: NonZeroUsize,
control: &RunControl<'_>,
budget: Option<TargetEvaluationBudget>,
) -> Result<MultiChainOutput, Error> {
match budget {
Some(budget) => sample_chains_with_target_budget_and_control(
target,
starts,
mass,
config,
threads,
TargetEvaluationAdmissionLimit::new(
NonZeroUsize::new(budget.maximum()).expect("nonzero budget"),
),
&budget,
control,
),
None => sample_chains_with_control(target, starts, mass, config, threads, control),
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Posterior {
output: MultiChainOutput,
metric_updates: Vec<Vec<StructuredRefreshUpdate>>,
final_masses: Vec<StructuredBlockMass>,
}
impl Posterior {
pub fn chains(&self) -> &[ChainOutput] {
self.output.chains()
}
pub fn chain_count(&self) -> usize {
self.output.chains().len()
}
pub fn dimension(&self) -> usize {
self.output
.chains()
.first()
.map_or(0, ChainOutput::dimension)
}
pub fn draws_per_chain(&self) -> usize {
self.output
.chains()
.first()
.map_or(0, ChainOutput::retained)
}
pub fn draws(&self) -> impl Iterator<Item = &[f64]> + '_ {
self.output.chains().iter().flat_map(|chain| {
let dimension = chain.dimension().max(1);
chain.samples().chunks_exact(dimension)
})
}
pub fn chain_draws(&self, chain: usize) -> Option<&[f64]> {
self.output.chains().get(chain).map(ChainOutput::samples)
}
pub fn draw(&self, chain: usize, draw: usize) -> Option<&[f64]> {
self.output.chains().get(chain)?.sample(draw)
}
pub fn parameter(&self, index: usize) -> impl Iterator<Item = f64> + '_ {
self.draws().map(move |draw| draw[index])
}
pub fn telemetry(&self) -> impl Iterator<Item = &RunTelemetry> + '_ {
self.output.chains().iter().map(ChainOutput::telemetry)
}
pub fn metadata(&self) -> impl Iterator<Item = &RunMetadata> + '_ {
self.output.chains().iter().map(ChainOutput::metadata)
}
pub fn total_target_calls(&self) -> usize {
self.telemetry()
.map(|telemetry| telemetry.total().target_calls_total())
.sum()
}
pub fn seed(&self) -> u64 {
self.output.base_seed()
}
pub fn algorithm_revision(&self) -> &str {
self.output.algorithm_revision()
}
pub fn metric_updates(&self) -> &[Vec<StructuredRefreshUpdate>] {
&self.metric_updates
}
pub fn final_masses(&self) -> &[StructuredBlockMass] {
&self.final_masses
}
pub fn inner(&self) -> &MultiChainOutput {
&self.output
}
pub fn into_inner(self) -> MultiChainOutput {
self.output
}
}