use std::any::Any;
use vortex_error::VortexExpect;
use vortex_error::VortexResult;
use vortex_error::vortex_bail;
use vortex_error::vortex_ensure;
use vortex_error::vortex_ensure_eq;
use crate::ArrayRef;
use crate::Columnar;
use crate::ExecutionCtx;
use crate::aggregate_fn::AggregateDTypes;
use crate::aggregate_fn::AggregateFn;
use crate::aggregate_fn::AggregateFnRef;
use crate::aggregate_fn::AggregateFnVTable;
use crate::aggregate_fn::session::AggregateFnSessionExt;
use crate::columnar::AnyColumnar;
use crate::dtype::DType;
use crate::executor::max_iterations;
use crate::expr::stats::Precision;
use crate::expr::stats::Stat;
use crate::expr::stats::StatsProvider;
use crate::scalar::Scalar;
pub type AccumulatorRef = Box<dyn DynAccumulator>;
pub struct Accumulator<V: AggregateFnVTable> {
vtable: V,
options: V::Options,
aggregate_fn: AggregateFnRef,
dtypes: AggregateDTypes,
partial: Option<V::Partial>,
}
impl<V: AggregateFnVTable> Accumulator<V> {
pub fn try_new(vtable: V, options: V::Options, dtype: DType) -> VortexResult<Self> {
let dtypes = AggregateDTypes::try_new(&vtable, &options, dtype)?;
Ok(Self::from_dtypes(vtable, options, dtypes))
}
pub fn from_dtypes(vtable: V, options: V::Options, dtypes: AggregateDTypes) -> Self {
let aggregate_fn = AggregateFn::new(vtable.clone(), options.clone()).erased();
Self {
vtable,
options,
aggregate_fn,
dtypes,
partial: None,
}
}
pub(crate) fn empty_partial(&self) -> VortexResult<V::Partial> {
self.vtable.empty_partial(self.dtypes.args(&self.options))
}
fn ensure_partial(&mut self) -> VortexResult<()> {
if self.partial.is_none() {
self.partial = Some(self.empty_partial()?);
}
Ok(())
}
pub(crate) fn fold_partial(&mut self, other: V::Partial) -> VortexResult<()> {
self.partial = Some(match self.partial.take() {
None => other,
Some(current) => {
self.vtable
.merge_partials(self.dtypes.args(&self.options), current, other)?
}
});
Ok(())
}
fn fold_partial_scalar(&mut self, scalar: &Scalar) -> VortexResult<()> {
let other = self
.vtable
.partial_from_scalar(self.dtypes.args(&self.options), scalar)?;
self.fold_partial(other)
}
}
pub trait DynAccumulator: 'static + Send {
fn accumulate(&mut self, batch: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<()>;
fn merge_from(&mut self, other: &mut dyn DynAccumulator) -> VortexResult<()>;
fn combine_partials(&mut self, partial: &Scalar) -> VortexResult<()>;
fn is_saturated(&self) -> bool;
fn reset(&mut self);
fn partial_scalar(&self) -> VortexResult<Scalar>;
fn final_scalar(&self) -> VortexResult<Scalar>;
fn flush(&mut self) -> VortexResult<Scalar>;
fn finish(&mut self) -> VortexResult<Scalar>;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
impl dyn DynAccumulator {
pub fn downcast_mut<V: AggregateFnVTable>(&mut self) -> Option<&mut Accumulator<V>> {
self.as_any_mut().downcast_mut()
}
}
impl<V: AggregateFnVTable> DynAccumulator for Accumulator<V> {
fn accumulate(&mut self, batch: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<()> {
if self.is_saturated() {
return Ok(());
}
vortex_ensure_eq!(batch.dtype(), &self.dtypes.dtype, "Input DType mismatch");
if let Some(stat) = Stat::from_aggregate_fn(&self.aggregate_fn)
&& let Precision::Exact(partial) = batch.statistics().get(stat)
{
let partial = if partial.dtype() == &self.dtypes.partial_dtype {
partial
} else {
vortex_ensure!(
partial
.dtype()
.eq_ignore_nullability(&self.dtypes.partial_dtype),
"Aggregate {} read legacy stat {} with dtype {}, expected {}",
self.aggregate_fn,
stat,
partial.dtype(),
self.dtypes.partial_dtype,
);
partial.cast(&self.dtypes.partial_dtype)?
};
self.fold_partial_scalar(&partial)?;
return Ok(());
}
let session = ctx.session().clone();
{
let kernel = session
.aggregate_fns()
.find_aggregate_kernel(batch.encoding_id(), self.aggregate_fn.id());
if let Some(kernel) = kernel
&& let Some(result) = kernel.aggregate(&self.aggregate_fn, batch, ctx)?
{
vortex_ensure_eq!(
result.dtype(),
&self.dtypes.partial_dtype,
"Aggregate kernel returned the wrong partial dtype",
);
self.fold_partial_scalar(&result)?;
return Ok(());
}
}
self.ensure_partial()?;
let partial = self.partial.as_mut().vortex_expect("partial materialized");
if self
.vtable
.try_accumulate(self.dtypes.args(&self.options), partial, batch, ctx)?
{
return Ok(());
}
let mut batch = batch.clone();
for _ in 0..max_iterations() {
if batch.is::<AnyColumnar>() {
break;
}
if let Some(kernel) = session
.aggregate_fns()
.find_aggregate_kernel(batch.encoding_id(), self.aggregate_fn.id())
&& let Some(result) = kernel.aggregate(&self.aggregate_fn, &batch, ctx)?
{
vortex_ensure_eq!(
result.dtype(),
&self.dtypes.partial_dtype,
"Aggregate kernel returned the wrong partial dtype",
);
self.fold_partial_scalar(&result)?;
return Ok(());
}
batch = batch.execute(ctx)?;
}
let columnar = batch.execute::<Columnar>(ctx)?;
self.ensure_partial()?;
let partial = self.partial.as_mut().vortex_expect("partial materialized");
self.vtable
.accumulate(self.dtypes.args(&self.options), partial, &columnar, ctx)
}
fn merge_from(&mut self, other: &mut dyn DynAccumulator) -> VortexResult<()> {
let Some(other) = other.downcast_mut::<V>() else {
vortex_bail!(
"Cannot merge into a {} accumulator from an accumulator of a different aggregate",
self.aggregate_fn,
);
};
vortex_ensure!(
other.options == self.options && other.dtypes.dtype == self.dtypes.dtype,
"Cannot merge {} accumulators with different options or input dtypes",
self.aggregate_fn,
);
match other.partial.take() {
Some(partial) => self.fold_partial(partial),
None => Ok(()),
}
}
fn combine_partials(&mut self, partial: &Scalar) -> VortexResult<()> {
vortex_ensure_eq!(
partial.dtype(),
&self.dtypes.partial_dtype,
"Partial DType mismatch for {}",
self.aggregate_fn,
);
self.fold_partial_scalar(partial)
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn is_saturated(&self) -> bool {
self.partial.as_ref().is_some_and(|partial| {
self.vtable
.is_saturated(self.dtypes.args(&self.options), partial)
})
}
fn reset(&mut self) {
self.partial = None;
}
fn partial_scalar(&self) -> VortexResult<Scalar> {
let args = self.dtypes.args(&self.options);
let partial = match &self.partial {
Some(partial) => self.vtable.to_scalar(args, partial)?,
None => self.vtable.to_scalar(args, &self.empty_partial()?)?,
};
#[cfg(debug_assertions)]
{
vortex_ensure_eq!(
partial.dtype(),
args.partial_dtype,
"Aggregate returned incorrect DType on partial_scalar",
);
}
Ok(partial)
}
fn final_scalar(&self) -> VortexResult<Scalar> {
let args = self.dtypes.args(&self.options);
let result = match &self.partial {
Some(partial) => self.vtable.finalize_scalar(args, partial)?,
None => self.vtable.finalize_scalar(args, &self.empty_partial()?)?,
};
vortex_ensure_eq!(
result.dtype(),
args.return_dtype,
"Aggregate returned incorrect DType on final_scalar",
);
Ok(result)
}
fn flush(&mut self) -> VortexResult<Scalar> {
let partial = self.partial_scalar()?;
self.reset();
Ok(partial)
}
fn finish(&mut self) -> VortexResult<Scalar> {
let result = self.final_scalar()?;
self.reset();
Ok(result)
}
}
#[cfg(test)]
mod tests {
use vortex_buffer::buffer;
use vortex_error::VortexResult;
use vortex_session::SessionExt;
use vortex_session::VortexSession;
use crate::ArrayRef;
use crate::ExecutionCtx;
use crate::IntoArray;
use crate::VortexSessionExecute;
use crate::aggregate_fn::Accumulator;
use crate::aggregate_fn::AccumulatorRef;
use crate::aggregate_fn::AggregateFnRef;
use crate::aggregate_fn::AggregateFnVTable;
use crate::aggregate_fn::DynAccumulator;
use crate::aggregate_fn::NumericalAggregateOpts;
use crate::aggregate_fn::combined::Combined;
use crate::aggregate_fn::combined::PairOptions;
use crate::aggregate_fn::fns::mean::Mean;
use crate::aggregate_fn::fns::min::Min;
use crate::aggregate_fn::fns::sum::Sum;
use crate::aggregate_fn::kernels::DynAggregateKernel;
use crate::aggregate_fn::session::AggregateFnSession;
use crate::array::VTable;
use crate::arrays::Dict;
use crate::arrays::DictArray;
use crate::dtype::DType;
use crate::dtype::Nullability;
use crate::dtype::PType;
use crate::expr::stats::Precision;
use crate::expr::stats::Stat;
use crate::scalar::Scalar;
use crate::scalar::ScalarValue;
#[derive(Debug)]
struct SentinelMeanPartialKernel;
impl DynAggregateKernel for SentinelMeanPartialKernel {
fn aggregate(
&self,
_aggregate_fn: &AggregateFnRef,
_batch: &ArrayRef,
_ctx: &mut ExecutionCtx,
) -> VortexResult<Option<Scalar>> {
Ok(Some(sentinel_partial()))
}
}
#[derive(Debug)]
struct DeclineKernel;
impl DynAggregateKernel for DeclineKernel {
fn aggregate(
&self,
_aggregate_fn: &AggregateFnRef,
_batch: &ArrayRef,
_ctx: &mut ExecutionCtx,
) -> VortexResult<Option<Scalar>> {
Ok(None)
}
}
#[derive(Debug)]
struct SentinelSumPartialKernel;
impl DynAggregateKernel for SentinelSumPartialKernel {
fn aggregate(
&self,
_aggregate_fn: &AggregateFnRef,
_batch: &ArrayRef,
_ctx: &mut ExecutionCtx,
) -> VortexResult<Option<Scalar>> {
Ok(Some(Scalar::primitive(42.0f64, Nullability::Nullable)))
}
}
fn fresh_session() -> VortexSession {
crate::array_session()
}
fn dict_of_seven() -> ArrayRef {
DictArray::try_new(buffer![0u32].into_array(), buffer![7.0f64].into_array())
.expect("valid dictionary")
.into_array()
}
fn mean_f64_accumulator() -> VortexResult<Accumulator<Combined<Mean>>> {
let dtype = DType::Primitive(PType::F64, Nullability::NonNullable);
Accumulator::try_new(
Mean::combined(),
PairOptions(
NumericalAggregateOpts::default(),
NumericalAggregateOpts::default(),
),
dtype,
)
}
fn sentinel_partial() -> Scalar {
let acc = mean_f64_accumulator().expect("build accumulator");
let sum = Scalar::primitive(42.0f64, Nullability::Nullable);
let count = Scalar::primitive(1u64, Nullability::NonNullable);
Scalar::struct_(acc.dtypes.partial_dtype, vec![sum, count])
}
#[test]
fn combined_kernel_fires() -> VortexResult<()> {
static KERNEL: SentinelMeanPartialKernel = SentinelMeanPartialKernel;
let session = fresh_session();
session
.get::<AggregateFnSession>()
.register_aggregate_kernel(Dict.id(), Some(Mean::combined().id()), &KERNEL);
let mut ctx = session.create_execution_ctx();
let mut acc = mean_f64_accumulator()?;
acc.accumulate(&dict_of_seven(), &mut ctx)?;
let partial = acc.flush()?;
let s = partial.as_struct();
assert_eq!(
s.field("sum").unwrap().as_primitive().as_::<f64>(),
Some(42.0)
);
assert_eq!(
s.field("count").unwrap().as_primitive().as_::<u64>(),
Some(1)
);
Ok(())
}
#[test]
fn fallback_when_kernel_declines() -> VortexResult<()> {
static KERNEL: DeclineKernel = DeclineKernel;
let session = fresh_session();
session
.get::<AggregateFnSession>()
.register_aggregate_kernel(Dict.id(), Some(Mean::combined().id()), &KERNEL);
let mut ctx = session.create_execution_ctx();
let mut acc = mean_f64_accumulator()?;
acc.accumulate(&dict_of_seven(), &mut ctx)?;
let partial = acc.flush()?;
let s = partial.as_struct();
assert_eq!(
s.field("sum").unwrap().as_primitive().as_::<f64>(),
Some(7.0)
);
assert_eq!(
s.field("count").unwrap().as_primitive().as_::<u64>(),
Some(1)
);
Ok(())
}
#[test]
fn child_kernel_fires_through_combined() -> VortexResult<()> {
static KERNEL: SentinelSumPartialKernel = SentinelSumPartialKernel;
let session = fresh_session();
session
.get::<AggregateFnSession>()
.register_aggregate_kernel(Dict.id(), Some(Sum.id()), &KERNEL);
let mut ctx = session.create_execution_ctx();
let mut acc = mean_f64_accumulator()?;
acc.accumulate(&dict_of_seven(), &mut ctx)?;
let partial = acc.flush()?;
let s = partial.as_struct();
assert_eq!(
s.field("sum").unwrap().as_primitive().as_::<f64>(),
Some(42.0)
);
assert_eq!(
s.field("count").unwrap().as_primitive().as_::<u64>(),
Some(1)
);
Ok(())
}
#[test]
fn cached_sum_precedes_encoding_kernel() -> VortexResult<()> {
static KERNEL: SentinelSumPartialKernel = SentinelSumPartialKernel;
let session = fresh_session();
session
.get::<AggregateFnSession>()
.register_aggregate_kernel(Dict.id(), Some(Sum.id()), &KERNEL);
let mut ctx = session.create_execution_ctx();
let batch = dict_of_seven();
batch
.statistics()
.set(Stat::Sum, Precision::Exact(ScalarValue::from(11.0f64)));
let dtype = DType::Primitive(PType::F64, Nullability::NonNullable);
let mut acc = Accumulator::try_new(Sum, NumericalAggregateOpts::default(), dtype)?;
acc.accumulate(&batch, &mut ctx)?;
assert_eq!(acc.finish()?.as_primitive().as_::<f64>(), Some(11.0));
Ok(())
}
fn sum_i32_accumulator(options: NumericalAggregateOpts) -> VortexResult<AccumulatorRef> {
let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
Ok(Box::new(Accumulator::try_new(Sum, options, dtype)?))
}
#[test]
fn merge_from_drains_other_accumulator() -> VortexResult<()> {
let mut ctx = fresh_session().create_execution_ctx();
let mut global = sum_i32_accumulator(NumericalAggregateOpts::default())?;
let mut local = sum_i32_accumulator(NumericalAggregateOpts::default())?;
global.accumulate(&buffer![10i32, 20].into_array(), &mut ctx)?;
local.accumulate(&buffer![5i32].into_array(), &mut ctx)?;
global.merge_from(local.as_mut())?;
assert_eq!(
global.finish()?,
Scalar::primitive(35i64, Nullability::Nullable)
);
assert_eq!(
local.finish()?,
Scalar::primitive(0i64, Nullability::Nullable)
);
Ok(())
}
#[test]
fn merge_from_rejects_a_different_aggregate() -> VortexResult<()> {
let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
let mut sum = sum_i32_accumulator(NumericalAggregateOpts::default())?;
let mut min: AccumulatorRef = Box::new(Accumulator::try_new(
Min,
NumericalAggregateOpts::default(),
dtype,
)?);
assert!(sum.merge_from(min.as_mut()).is_err());
Ok(())
}
#[test]
fn merge_from_rejects_mismatched_options() -> VortexResult<()> {
let mut skipping = sum_i32_accumulator(NumericalAggregateOpts::skip_nans())?;
let mut including = sum_i32_accumulator(NumericalAggregateOpts::include_nans())?;
assert!(skipping.merge_from(including.as_mut()).is_err());
Ok(())
}
#[test]
fn merge_from_combines_child_accumulators() -> VortexResult<()> {
let mut ctx = fresh_session().create_execution_ctx();
let mut global: AccumulatorRef = Box::new(mean_f64_accumulator()?);
let mut local: AccumulatorRef = Box::new(mean_f64_accumulator()?);
global.accumulate(&buffer![1.0f64, 2.0].into_array(), &mut ctx)?;
local.accumulate(&buffer![6.0f64].into_array(), &mut ctx)?;
global.merge_from(local.as_mut())?;
assert_eq!(global.finish()?.as_primitive().as_::<f64>(), Some(3.0));
Ok(())
}
}