use super::{context::SeriesOps, owned::Series};
#[cfg(any(test, feature = "test-hooks"))]
use crate::runtime::Runtime;
use crate::{
core::{
error::{self, Error, ErrorCode, ErrorKind, FACT_TYPE_ID, Result},
scalar::Scalar,
types::{CudfType, DataType, NullPolicy, TypeId},
},
ops::{
reduction::{ReduceAgg, ScanAgg, ScanType},
unary::UnaryOperator,
},
runtime::{BudgetedExecutionOutcome, Exec, ExecutionBudgetToken},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScalarReductionOutputShape {
output_type: DataType,
output_rows: usize,
}
impl ScalarReductionOutputShape {
fn new(output_type: DataType) -> Self {
Self { output_type, output_rows: 1 }
}
pub fn output_type(&self) -> DataType {
self.output_type
}
pub fn output_rows(&self) -> usize {
self.output_rows
}
}
pub struct ScalarReductionOutput {
scalar: Scalar,
}
impl ScalarReductionOutput {
fn new(scalar: Scalar) -> Self {
Self { scalar }
}
pub fn scalar(&self) -> &Scalar {
&self.scalar
}
pub fn into_scalar(self) -> Scalar {
self.scalar
}
pub fn output_rows(&self) -> usize {
1
}
}
pub struct SeriesPreparedScalarReduction<'a> {
input: &'a Series,
aggregation: ReduceAgg,
output_shape: ScalarReductionOutputShape,
}
impl<'a> SeriesPreparedScalarReduction<'a> {
fn new(input: &'a Series, aggregation: ReduceAgg, output_shape: ScalarReductionOutputShape) -> Self {
Self { input, aggregation, output_shape }
}
pub fn input(&self) -> &Series {
self.input
}
pub fn aggregation(&self) -> &ReduceAgg {
&self.aggregation
}
pub fn output_type(&self) -> DataType {
self.output_shape.output_type()
}
pub fn output_shape(&self) -> &ScalarReductionOutputShape {
&self.output_shape
}
pub fn execute(&self, exec: Exec<'_>) -> Result<ScalarReductionOutput> {
self.execute_in_context(exec, "series::prepared_scalar_reduction::execute", |error| error)
}
pub fn execute_with_budget(
&self,
token: &ExecutionBudgetToken,
) -> Result<BudgetedExecutionOutcome<ScalarReductionOutput>> {
let operation = "series::prepared_scalar_reduction::execute_with_budget";
let ctx = token.exec();
let output = self.execute_in_context(ctx, operation, |error| {
annotate_scalar_reduction_budget_execution_error(error, token, operation)
})?;
let report = token.reservation_enforced_report().with_output_rows(output.output_rows());
Ok(BudgetedExecutionOutcome::new(output, report))
}
fn execute_in_context<F>(
&self,
ctx: Exec<'_>,
operation: &'static str,
map_execution_error: F,
) -> Result<ScalarReductionOutput>
where
F: FnOnce(Error) -> Error,
{
self.input
.reduce_prepared_in_context(
&self.aggregation,
&self.output_shape.output_type,
ctx,
operation,
map_execution_error,
)
.map(ScalarReductionOutput::new)
.map_err(|error| error.reanchor_if_needed(operation))
}
}
pub(crate) fn infer_reduce_output_type(input: &DataType, agg: &ReduceAgg, operation: &'static str) -> Result<DataType> {
match agg {
ReduceAgg::Sum => match input.id() {
TypeId::Int8
| TypeId::Int16
| TypeId::Int32
| TypeId::Int64
| TypeId::UInt8
| TypeId::UInt16
| TypeId::UInt32
| TypeId::UInt64
| TypeId::Bool8 => Ok(DataType::new(TypeId::Int64)),
TypeId::Float32 => Ok(DataType::new(TypeId::Float32)),
TypeId::Float64 => Ok(DataType::new(TypeId::Float64)),
id if id.is_decimal() => Ok(*input),
_ => Err(Error::new_with(ErrorCode::UnsupportedDtype, operation, "unsupported type for sum reduction")
.with_fact(FACT_TYPE_ID, input.id())),
},
ReduceAgg::Min | ReduceAgg::Max | ReduceAgg::Product => Ok(*input),
ReduceAgg::Mean | ReduceAgg::Median | ReduceAgg::Std { .. } | ReduceAgg::Variance { .. } => {
Ok(DataType::new(TypeId::Float64))
},
ReduceAgg::Any | ReduceAgg::All => Ok(DataType::new(TypeId::Bool8)),
ReduceAgg::NUnique { .. } => Ok(DataType::new(TypeId::Int32)),
}
}
pub(crate) fn scalar_to_optional_value<T: CudfType>(scalar: Scalar) -> Result<Option<T>> {
if !scalar.is_valid()? {
return Ok(None);
}
scalar.value::<T>().map(Some)
}
fn scalar_to_usize_count(scalar: Scalar, operation: &'static str) -> Result<usize> {
if !scalar.is_valid().map_err(|e| e.reanchor(operation))? {
return Ok(0);
}
let count = scalar.value::<i32>().map_err(|e| e.reanchor(operation))?;
usize::try_from(count).map_err(|_| negative_distinct_count_error(operation, count))
}
fn negative_distinct_count_error(operation: &'static str, count: i32) -> Error {
error::foreign_invariant_violation(
operation,
"distinct count must be >= 0",
"distinct_count",
"negative_count",
"ensure the foreign reduction backend returns a non-negative distinct count",
)
.with_detail("distinct_count", count.to_string())
}
#[cfg(any(test, feature = "test-hooks"))]
pub(crate) fn trigger_nunique_negative_distinct_count_for_test() -> Result<()> {
let runtime = Runtime::new()?;
let scalar = runtime.scalar(-1i32)?;
scalar_to_usize_count(scalar, "series::nunique").map(|_| ())
}
impl Series {
pub fn prepare_scalar_reduction(&self, agg: ReduceAgg) -> Result<SeriesPreparedScalarReduction<'_>> {
let operation = "series::prepare_scalar_reduction";
let input_type = self.dtype().map_err(|error| error.reanchor_if_needed(operation))?;
let output_type = infer_reduce_output_type(&input_type, &agg, operation)?;
Ok(SeriesPreparedScalarReduction::new(self, agg, ScalarReductionOutputShape::new(output_type)))
}
pub(super) fn nunique_with_context(&self, ctx: Exec<'_>, operation: &'static str) -> Result<usize> {
let scalar = self.reduce_in_context(ReduceAgg::NUnique { null_policy: NullPolicy::Exclude }, ctx, operation)?;
scalar_to_usize_count(scalar, operation)
}
}
fn annotate_scalar_reduction_budget_execution_error(
error: Error,
token: &ExecutionBudgetToken,
operation: &'static str,
) -> Error {
match error.kind() {
ErrorKind::ResourceExhausted | ErrorKind::DependencyFailure => token.annotate_error(error, operation),
_ => error.reanchor_if_needed(operation),
}
}
impl SeriesOps<'_> {
pub fn sum(&self) -> Result<Scalar> {
self.series.reduce_in_context(ReduceAgg::Sum, self.ctx, "series::sum")
}
pub fn sum_value<T: CudfType>(&self) -> Result<T> {
self.sum()?.value::<T>()
}
pub fn min(&self) -> Result<Scalar> {
self.series.reduce_in_context(ReduceAgg::Min, self.ctx, "series::min")
}
pub fn min_value<T: CudfType>(&self) -> Result<Option<T>> {
scalar_to_optional_value(self.min()?)
}
pub fn max(&self) -> Result<Scalar> {
self.series.reduce_in_context(ReduceAgg::Max, self.ctx, "series::max")
}
pub fn max_value<T: CudfType>(&self) -> Result<Option<T>> {
scalar_to_optional_value(self.max()?)
}
pub fn mean(&self) -> Result<Scalar> {
self.series.reduce_in_context(ReduceAgg::Mean, self.ctx, "series::mean")
}
pub fn mean_value(&self) -> Result<Option<f64>> {
scalar_to_optional_value(self.mean()?)
}
pub fn median(&self) -> Result<Scalar> {
self.series.reduce_in_context(ReduceAgg::Median, self.ctx, "series::median")
}
pub fn median_value(&self) -> Result<Option<f64>> {
scalar_to_optional_value(self.median()?)
}
pub fn std(&self, ddof: u32) -> Result<Scalar> {
self.series.reduce_in_context(ReduceAgg::Std { ddof }, self.ctx, "series::std")
}
pub fn std_value(&self, ddof: u32) -> Result<Option<f64>> {
scalar_to_optional_value(self.std(ddof)?)
}
pub fn var(&self, ddof: u32) -> Result<Scalar> {
self.series.reduce_in_context(ReduceAgg::Variance { ddof }, self.ctx, "series::var")
}
pub fn var_value(&self, ddof: u32) -> Result<Option<f64>> {
scalar_to_optional_value(self.var(ddof)?)
}
pub fn any(&self) -> Result<Scalar> {
self.series.reduce_in_context(ReduceAgg::Any, self.ctx, "series::any")
}
pub fn any_value(&self) -> Result<bool> {
Ok(scalar_to_optional_value(self.any()?)?.unwrap_or(false))
}
pub fn all(&self) -> Result<Scalar> {
self.series.reduce_in_context(ReduceAgg::All, self.ctx, "series::all")
}
pub fn all_value(&self) -> Result<bool> {
Ok(scalar_to_optional_value(self.all()?)?.unwrap_or(true))
}
pub fn product(&self) -> Result<Scalar> {
self.series.reduce_in_context(ReduceAgg::Product, self.ctx, "series::product")
}
pub fn product_value<T: CudfType>(&self) -> Result<Option<T>> {
scalar_to_optional_value(self.product()?)
}
pub fn minmax(&self) -> Result<(Scalar, Scalar)> {
self.series.minmax_with_context(self.ctx, "series::minmax")
}
pub fn minmax_values<T: CudfType>(&self) -> Result<(Option<T>, Option<T>)> {
let (min, max) = self.minmax()?;
Ok((scalar_to_optional_value(min)?, scalar_to_optional_value(max)?))
}
pub fn cumsum(&self) -> Result<Series> {
self.series.scan_with_context(
ScanAgg::Sum,
ScanType::Inclusive,
NullPolicy::Exclude,
self.ctx,
"series::cumsum",
)
}
pub fn cummin(&self) -> Result<Series> {
self.series.scan_with_context(
ScanAgg::Min,
ScanType::Inclusive,
NullPolicy::Exclude,
self.ctx,
"series::cummin",
)
}
pub fn cummax(&self) -> Result<Series> {
self.series.scan_with_context(
ScanAgg::Max,
ScanType::Inclusive,
NullPolicy::Exclude,
self.ctx,
"series::cummax",
)
}
pub fn cumprod(&self) -> Result<Series> {
self.series.scan_with_context(
ScanAgg::Product,
ScanType::Inclusive,
NullPolicy::Exclude,
self.ctx,
"series::cumprod",
)
}
pub fn is_nan(&self) -> Result<Series> {
self.series.is_nan_with_context(self.ctx, "series::is_nan")
}
pub fn is_not_nan(&self) -> Result<Series> {
self.series.is_not_nan_with_context(self.ctx, "series::is_not_nan")
}
pub fn abs(&self) -> Result<Series> {
self.series.unary_with_context(UnaryOperator::Abs, self.ctx, "series::abs")
}
pub fn neg(&self) -> Result<Series> {
self.series.unary_with_context(UnaryOperator::Negate, self.ctx, "series::neg")
}
pub fn logical_not(&self) -> Result<Series> {
self.series.logical_not_with_context(self.ctx, "series::logical_not")
}
}