Skip to content

Commit df449a2

Browse files
committed
Add new fp_accuracy param for LazyArray reductions (sum, prod et al)
1 parent a8bc280 commit df449a2

5 files changed

Lines changed: 170 additions & 41 deletions

File tree

doc/reference/reduction_functions.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ Reduction Functions
33

44
Contrarily to lazy functions, reduction functions are evaluated eagerly, and the result is always a NumPy array (although this can be converted internally into an :ref:`NDArray <NDArray>` if you pass any :func:`blosc2.empty` arguments in ``kwargs``).
55

6-
Reduction operations can be used with any of :ref:`NDArray <NDArray>`, :ref:`C2Array <C2Array>`, :ref:`NDField <NDField>` and :ref:`LazyExpr <LazyExpr>`. Again, although these can be part of a :ref:`LazyExpr <LazyExpr>`, you must be aware that they are not lazy, but will be evaluated eagerly during the construction of a LazyExpr instance (this might change in the future).
6+
Reduction operations can be used with any of :ref:`NDArray <NDArray>`, :ref:`C2Array <C2Array>`, :ref:`NDField <NDField>` and :ref:`LazyExpr <LazyExpr>`. Again, although these can be part of a :ref:`LazyExpr <LazyExpr>`, you must be aware that they are not lazy, but will be evaluated eagerly during the construction of a LazyExpr instance (this might change in the future). When the input is a :ref:`LazyExpr`, reductions accept ``fp_accuracy`` to control floating-point accuracy, and it is forwarded to :func:`LazyExpr.compute`.
77

88
.. currentmodule:: blosc2
99

src/blosc2/lazyexpr.py

Lines changed: 125 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1248,6 +1248,7 @@ def fast_eval( # noqa: C901
12481248
ne_args: dict = kwargs.pop("_ne_args", {})
12491249
if ne_args is None:
12501250
ne_args = {}
1251+
fp_accuracy = kwargs.pop("fp_accuracy", blosc2.FPAccuracy.DEFAULT)
12511252
dtype = kwargs.pop("dtype", None)
12521253
where: dict | None = kwargs.pop("_where_args", None)
12531254
if where is not None:
@@ -1306,11 +1307,10 @@ def fast_eval( # noqa: C901
13061307
if use_miniexpr:
13071308
cparams = kwargs.pop("cparams", blosc2.CParams())
13081309
# All values will be overwritten, so we can use an uninitialized array
1309-
fp_accuracy = kwargs.pop("fp_accuracy", blosc2.FPAccuracy.DEFAULT)
13101310
res_eval = blosc2.uninit(shape, dtype, chunks=chunks, blocks=blocks, cparams=cparams, **kwargs)
13111311
try:
13121312
res_eval._set_pref_expr(expression, operands, fp_accuracy=fp_accuracy)
1313-
print("expr->miniexpr:", expression)
1313+
print("expr->miniexpr:", expression, fp_accuracy)
13141314
# Data to compress is fetched from operands, so it can be uninitialized here
13151315
data = np.empty(res_eval.schunk.chunksize, dtype=np.uint8)
13161316
# Exercise prefilter for each chunk
@@ -1522,7 +1522,10 @@ def slices_eval( # noqa: C901
15221522
# Typically, we enter here when using UDFs, and out is a NumPy array.
15231523
# Use operands to get the shape and chunks
15241524
# operand will be a 'fake' NDArray just to get the necessary chunking information
1525+
fp_accuracy = kwargs.pop("fp_accuracy", None)
15251526
temp = blosc2.empty(shape, dtype=dtype)
1527+
if fp_accuracy is not None:
1528+
kwargs["fp_accuracy"] = fp_accuracy
15261529
chunks = temp.chunks
15271530
del temp
15281531

@@ -1607,7 +1610,10 @@ def slices_eval( # noqa: C901
16071610
if "chunks" in kwargs and (where is not None and len(where) < 2 and len(shape_) > 1):
16081611
# Remove the chunks argument if the where condition is not a tuple with two elements
16091612
kwargs.pop("chunks")
1613+
fp_accuracy = kwargs.pop("fp_accuracy", None)
16101614
out = blosc2.empty(shape_, dtype=dtype_, **kwargs)
1615+
if fp_accuracy is not None:
1616+
kwargs["fp_accuracy"] = fp_accuracy
16111617
# Check if the in out partitions are well-behaved (i.e. no padding)
16121618
behaved = blosc2.are_partitions_behaved(out.shape, out.chunks, out.blocks)
16131619
# Evaluate the expression using chunks of operands
@@ -1892,6 +1898,7 @@ def reduce_slices( # noqa: C901
18921898
ne_args: dict = kwargs.pop("_ne_args", {})
18931899
if ne_args is None:
18941900
ne_args = {}
1901+
fp_accuracy = kwargs.pop("fp_accuracy", blosc2.FPAccuracy.DEFAULT)
18951902
where: dict | None = kwargs.pop("_where_args", None)
18961903
reduce_op = reduce_args.pop("op")
18971904
reduce_op_str = reduce_args.pop("op_str", None)
@@ -2014,7 +2021,6 @@ def reduce_slices( # noqa: C901
20142021
if use_miniexpr:
20152022
# Experiments say that not splitting is best (at least on Apple Silicon M4 Pro)
20162023
cparams = kwargs.pop("cparams", blosc2.CParams(splitmode=blosc2.SplitMode.NEVER_SPLIT))
2017-
fp_accuracy = kwargs.pop("fp_accuracy", blosc2.FPAccuracy.DEFAULT)
20182024
# Create a fake NDArray just to drive the miniexpr evaluation (values won't be used)
20192025
res_eval = blosc2.uninit(shape, dtype, chunks=chunks, blocks=blocks, cparams=cparams, **kwargs)
20202026
# Compute the number of blocks in the result
@@ -2044,7 +2050,7 @@ def reduce_slices( # noqa: C901
20442050
else:
20452051
expression_miniexpr = f"{reduce_op_str}({expression})"
20462052
res_eval._set_pref_expr(expression_miniexpr, operands, fp_accuracy, aux_reduc)
2047-
print("expr->miniexpr:", expression, reduce_op)
2053+
print("expr->miniexpr:", expression, reduce_op, fp_accuracy)
20482054
# Data won't even try to be compressed, so buffers can be unitialized and reused
20492055
data = np.empty(res_eval.schunk.chunksize, dtype=np.uint8)
20502056
chunk_data = np.empty(res_eval.schunk.chunksize + blosc2.MAX_OVERHEAD, dtype=np.uint8)
@@ -2849,25 +2855,39 @@ def where(self, value1=None, value2=None):
28492855
new_expr._dtype = dtype
28502856
return new_expr
28512857

2852-
def sum(self, axis=None, dtype=None, keepdims=False, **kwargs):
2858+
def sum(
2859+
self,
2860+
axis=None,
2861+
dtype=None,
2862+
keepdims=False,
2863+
fp_accuracy: blosc2.FPAccuracy = blosc2.FPAccuracy.DEFAULT,
2864+
**kwargs,
2865+
):
28532866
reduce_args = {
28542867
"op": ReduceOp.SUM,
28552868
"op_str": "sum",
28562869
"axis": axis,
28572870
"dtype": dtype,
28582871
"keepdims": keepdims,
28592872
}
2860-
return self.compute(_reduce_args=reduce_args, **kwargs)
2873+
return self.compute(_reduce_args=reduce_args, fp_accuracy=fp_accuracy, **kwargs)
28612874

2862-
def prod(self, axis=None, dtype=None, keepdims=False, **kwargs):
2875+
def prod(
2876+
self,
2877+
axis=None,
2878+
dtype=None,
2879+
keepdims=False,
2880+
fp_accuracy: blosc2.FPAccuracy = blosc2.FPAccuracy.DEFAULT,
2881+
**kwargs,
2882+
):
28632883
reduce_args = {
28642884
"op": ReduceOp.PROD,
28652885
"op_str": "prod",
28662886
"axis": axis,
28672887
"dtype": dtype,
28682888
"keepdims": keepdims,
28692889
}
2870-
return self.compute(_reduce_args=reduce_args, **kwargs)
2890+
return self.compute(_reduce_args=reduce_args, fp_accuracy=fp_accuracy, **kwargs)
28712891

28722892
def get_num_elements(self, axis, item):
28732893
if hasattr(self, "_where_args") and len(self._where_args) == 1:
@@ -2889,9 +2909,22 @@ def get_num_elements(self, axis, item):
28892909
axis = tuple(a if a >= 0 else a + len(shape) for a in axis) # handle negative indexing
28902910
return math.prod([shape[i] for i in axis])
28912911

2892-
def mean(self, axis=None, dtype=None, keepdims=False, **kwargs):
2912+
def mean(
2913+
self,
2914+
axis=None,
2915+
dtype=None,
2916+
keepdims=False,
2917+
fp_accuracy: blosc2.FPAccuracy = blosc2.FPAccuracy.DEFAULT,
2918+
**kwargs,
2919+
):
28932920
item = kwargs.pop("item", ())
2894-
total_sum = self.sum(axis=axis, dtype=dtype, keepdims=keepdims, item=item)
2921+
total_sum = self.sum(
2922+
axis=axis,
2923+
dtype=dtype,
2924+
keepdims=keepdims,
2925+
item=item,
2926+
fp_accuracy=fp_accuracy,
2927+
)
28952928
num_elements = self.get_num_elements(axis, item)
28962929
if num_elements == 0:
28972930
raise ValueError("mean of an empty array is not defined")
@@ -2904,17 +2937,25 @@ def mean(self, axis=None, dtype=None, keepdims=False, **kwargs):
29042937
out = blosc2.asarray(out, **kwargs)
29052938
return out
29062939

2907-
def std(self, axis=None, dtype=None, keepdims=False, ddof=0, **kwargs):
2940+
def std(
2941+
self,
2942+
axis=None,
2943+
dtype=None,
2944+
keepdims=False,
2945+
ddof=0,
2946+
fp_accuracy: blosc2.FPAccuracy = blosc2.FPAccuracy.DEFAULT,
2947+
**kwargs,
2948+
):
29082949
item = kwargs.pop("item", ())
29092950
if item == (): # fast path
2910-
mean_value = self.mean(axis=axis, dtype=dtype, keepdims=True)
2951+
mean_value = self.mean(axis=axis, dtype=dtype, keepdims=True, fp_accuracy=fp_accuracy)
29112952
expr = (self - mean_value) ** 2
29122953
else:
2913-
mean_value = self.mean(axis=axis, dtype=dtype, keepdims=True, item=item)
2954+
mean_value = self.mean(axis=axis, dtype=dtype, keepdims=True, item=item, fp_accuracy=fp_accuracy)
29142955
# TODO: Not optimal because we load the whole slice in memory. Would have to write
29152956
# a bespoke std function that executed within slice_eval to avoid this probably.
29162957
expr = (self.slice(item) - mean_value) ** 2
2917-
out = expr.mean(axis=axis, dtype=dtype, keepdims=keepdims)
2958+
out = expr.mean(axis=axis, dtype=dtype, keepdims=keepdims, fp_accuracy=fp_accuracy)
29182959
if ddof != 0:
29192960
num_elements = self.get_num_elements(axis, item)
29202961
out = np.sqrt(out * num_elements / (num_elements - ddof))
@@ -2928,17 +2969,25 @@ def std(self, axis=None, dtype=None, keepdims=False, ddof=0, **kwargs):
29282969
out = blosc2.asarray(out, **kwargs)
29292970
return out
29302971

2931-
def var(self, axis=None, dtype=None, keepdims=False, ddof=0, **kwargs):
2972+
def var(
2973+
self,
2974+
axis=None,
2975+
dtype=None,
2976+
keepdims=False,
2977+
ddof=0,
2978+
fp_accuracy: blosc2.FPAccuracy = blosc2.FPAccuracy.DEFAULT,
2979+
**kwargs,
2980+
):
29322981
item = kwargs.pop("item", ())
29332982
if item == (): # fast path
2934-
mean_value = self.mean(axis=axis, dtype=dtype, keepdims=True)
2983+
mean_value = self.mean(axis=axis, dtype=dtype, keepdims=True, fp_accuracy=fp_accuracy)
29352984
expr = (self - mean_value) ** 2
29362985
else:
2937-
mean_value = self.mean(axis=axis, dtype=dtype, keepdims=True, item=item)
2986+
mean_value = self.mean(axis=axis, dtype=dtype, keepdims=True, item=item, fp_accuracy=fp_accuracy)
29382987
# TODO: Not optimal because we load the whole slice in memory. Would have to write
29392988
# a bespoke var function that executed within slice_eval to avoid this probably.
29402989
expr = (self.slice(item) - mean_value) ** 2
2941-
out = expr.mean(axis=axis, dtype=dtype, keepdims=keepdims)
2990+
out = expr.mean(axis=axis, dtype=dtype, keepdims=keepdims, fp_accuracy=fp_accuracy)
29422991
if ddof != 0:
29432992
num_elements = self.get_num_elements(axis, item)
29442993
out = out * num_elements / (num_elements - ddof)
@@ -2950,57 +2999,93 @@ def var(self, axis=None, dtype=None, keepdims=False, ddof=0, **kwargs):
29502999
out = blosc2.asarray(out, **kwargs)
29513000
return out
29523001

2953-
def min(self, axis=None, keepdims=False, **kwargs):
3002+
def min(
3003+
self,
3004+
axis=None,
3005+
keepdims=False,
3006+
fp_accuracy: blosc2.FPAccuracy = blosc2.FPAccuracy.DEFAULT,
3007+
**kwargs,
3008+
):
29543009
reduce_args = {
29553010
"op": ReduceOp.MIN,
29563011
"op_str": "min",
29573012
"axis": axis,
29583013
"keepdims": keepdims,
29593014
}
2960-
return self.compute(_reduce_args=reduce_args, **kwargs)
3015+
return self.compute(_reduce_args=reduce_args, fp_accuracy=fp_accuracy, **kwargs)
29613016

2962-
def max(self, axis=None, keepdims=False, **kwargs):
3017+
def max(
3018+
self,
3019+
axis=None,
3020+
keepdims=False,
3021+
fp_accuracy: blosc2.FPAccuracy = blosc2.FPAccuracy.DEFAULT,
3022+
**kwargs,
3023+
):
29633024
reduce_args = {
29643025
"op": ReduceOp.MAX,
29653026
"op_str": "max",
29663027
"axis": axis,
29673028
"keepdims": keepdims,
29683029
}
2969-
return self.compute(_reduce_args=reduce_args, **kwargs)
3030+
return self.compute(_reduce_args=reduce_args, fp_accuracy=fp_accuracy, **kwargs)
29703031

2971-
def any(self, axis=None, keepdims=False, **kwargs):
3032+
def any(
3033+
self,
3034+
axis=None,
3035+
keepdims=False,
3036+
fp_accuracy: blosc2.FPAccuracy = blosc2.FPAccuracy.DEFAULT,
3037+
**kwargs,
3038+
):
29723039
reduce_args = {
29733040
"op": ReduceOp.ANY,
29743041
"op_str": "any",
29753042
"axis": axis,
29763043
"keepdims": keepdims,
29773044
}
2978-
return self.compute(_reduce_args=reduce_args, **kwargs)
3045+
return self.compute(_reduce_args=reduce_args, fp_accuracy=fp_accuracy, **kwargs)
29793046

2980-
def all(self, axis=None, keepdims=False, **kwargs):
3047+
def all(
3048+
self,
3049+
axis=None,
3050+
keepdims=False,
3051+
fp_accuracy: blosc2.FPAccuracy = blosc2.FPAccuracy.DEFAULT,
3052+
**kwargs,
3053+
):
29813054
reduce_args = {
29823055
"op": ReduceOp.ALL,
29833056
"op_str": "all",
29843057
"axis": axis,
29853058
"keepdims": keepdims,
29863059
}
2987-
return self.compute(_reduce_args=reduce_args, **kwargs)
3060+
return self.compute(_reduce_args=reduce_args, fp_accuracy=fp_accuracy, **kwargs)
29883061

2989-
def argmax(self, axis=None, keepdims=False, **kwargs):
3062+
def argmax(
3063+
self,
3064+
axis=None,
3065+
keepdims=False,
3066+
fp_accuracy: blosc2.FPAccuracy = blosc2.FPAccuracy.DEFAULT,
3067+
**kwargs,
3068+
):
29903069
reduce_args = {
29913070
"op": ReduceOp.ARGMAX,
29923071
"axis": axis,
29933072
"keepdims": keepdims,
29943073
}
2995-
return self.compute(_reduce_args=reduce_args, **kwargs)
3074+
return self.compute(_reduce_args=reduce_args, fp_accuracy=fp_accuracy, **kwargs)
29963075

2997-
def argmin(self, axis=None, keepdims=False, **kwargs):
3076+
def argmin(
3077+
self,
3078+
axis=None,
3079+
keepdims=False,
3080+
fp_accuracy: blosc2.FPAccuracy = blosc2.FPAccuracy.DEFAULT,
3081+
**kwargs,
3082+
):
29983083
reduce_args = {
29993084
"op": ReduceOp.ARGMIN,
30003085
"axis": axis,
30013086
"keepdims": keepdims,
30023087
}
3003-
return self.compute(_reduce_args=reduce_args, **kwargs)
3088+
return self.compute(_reduce_args=reduce_args, fp_accuracy=fp_accuracy, **kwargs)
30043089

30053090
def _eval_constructor(self, expression, constructor, operands):
30063091
"""Evaluate a constructor function inside a string expression."""
@@ -3174,6 +3259,7 @@ def compute(
31743259
kwargs["_ne_args"] = self._ne_args
31753260
if hasattr(self, "_where_args"):
31763261
kwargs["_where_args"] = self._where_args
3262+
kwargs.setdefault("fp_accuracy", fp_accuracy)
31773263
kwargs["dtype"] = self.dtype
31783264
kwargs["shape"] = self.shape
31793265
if hasattr(self, "_indices"):
@@ -3192,7 +3278,15 @@ def compute(
31923278
and not isinstance(result, blosc2.NDArray)
31933279
):
31943280
# Get rid of all the extra kwargs that are not accepted by blosc2.asarray
3195-
kwargs_not_accepted = {"_where_args", "_indices", "_order", "_ne_args", "dtype", "shape"}
3281+
kwargs_not_accepted = {
3282+
"_where_args",
3283+
"_indices",
3284+
"_order",
3285+
"_ne_args",
3286+
"dtype",
3287+
"shape",
3288+
"fp_accuracy",
3289+
}
31963290
kwargs = {key: value for key, value in kwargs.items() if key not in kwargs_not_accepted}
31973291
result = blosc2.asarray(result, **kwargs)
31983292
return result

src/blosc2/ndarray.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,9 @@ def sum(
505505
If set to True, the reduced axes are left in the result
506506
as dimensions with size one. With this option, the result will broadcast
507507
correctly against the input array.
508+
fp_accuracy: :ref:`blosc2.FPAccuracy`, optional
509+
Specifies the floating-point accuracy for reductions on :ref:`LazyExpr`.
510+
Passed to :func:`LazyExpr.compute` when :paramref:`ndarr` is a LazyExpr.
508511
kwargs: dict, optional
509512
Additional keyword arguments supported by the :func:`empty` constructor.
510513
@@ -600,6 +603,9 @@ def std(
600603
If set to True, the reduced axes are left in the result as
601604
dimensions with size one. This ensures that the result will broadcast correctly
602605
against the input array.
606+
fp_accuracy: :ref:`blosc2.FPAccuracy`, optional
607+
Specifies the floating-point accuracy for reductions on :ref:`LazyExpr`.
608+
Passed to :func:`LazyExpr.compute` when :paramref:`ndarr` is a LazyExpr.
603609
kwargs: dict, optional
604610
Additional keyword arguments that are supported by the :func:`empty` constructor.
605611
@@ -732,6 +738,9 @@ def min(
732738
If set to True, the axes which are reduced are left in the result as
733739
dimensions with size one. With this option, the result will broadcast correctly
734740
against the input array.
741+
fp_accuracy: :ref:`blosc2.FPAccuracy`, optional
742+
Specifies the floating-point accuracy for reductions on :ref:`LazyExpr`.
743+
Passed to :func:`LazyExpr.compute` when :paramref:`ndarr` is a LazyExpr.
735744
kwargs: dict, optional
736745
Keyword arguments that are supported by the :func:`empty` constructor.
737746
@@ -863,6 +872,9 @@ def argmin(
863872
864873
keepdims: bool
865874
If True, reduced axis included in the result as singleton dimension. Otherwise, axis not included in the result. Default: False.
875+
fp_accuracy: :ref:`blosc2.FPAccuracy`, optional
876+
Specifies the floating-point accuracy for reductions on :ref:`LazyExpr`.
877+
Passed to :func:`LazyExpr.compute` when :paramref:`ndarr` is a LazyExpr.
866878
867879
Returns
868880
-------
@@ -890,6 +902,9 @@ def argmax(
890902
891903
keepdims: bool
892904
If True, reduced axis included in the result as singleton dimension. Otherwise, axis not included in the result. Default: False.
905+
fp_accuracy: :ref:`blosc2.FPAccuracy`, optional
906+
Specifies the floating-point accuracy for reductions on :ref:`LazyExpr`.
907+
Passed to :func:`LazyExpr.compute` when :paramref:`ndarr` is a LazyExpr.
893908
894909
Returns
895910
-------

0 commit comments

Comments
 (0)