Table of Contents

Universal functions (ufunc) basics

A universal function (ufunc) operates on an NDArray element by element, with broadcasting, type casting, and a small set of standard options. np.add, np.sqrt, np.exp, np.less, and the arithmetic/comparison operators are all ufuncs: each takes a fixed number of array inputs and produces a fixed number of array outputs, looping in fast C#/SIMD code rather than a C# for loop.

NumSharp implements the ufunc model - elementwise semantics, broadcasting, NEP 50 casting, out=/where=/dtype= - but exposes each ufunc as a direct np.* function (and operator), not as a NumPy-style ufunc object with .reduce/.accumulate/.at methods. This page covers the model and the porting differences.


Elementwise operation

The simplest ufunc is addition:

np.array([0, 2, 3, 4]) + np.array([1, 1, -1, 2]);   // [1 3 2 6]
np.add(np.array([0, 2, 3, 4]), np.array([1, 1, -1, 2]));  // same

Every arithmetic (+ - * / %, unary -), comparison (== != < > <= >=), and logical (& | !) operator is a ufunc, and each has a named function form:

Category Operators Named ufuncs
Arithmetic + - * / % add, subtract, multiply, divide/true_divide, mod/remainder, power, floor_divide
Comparison == != < > <= >= equal, not_equal, less, greater, less_equal, greater_equal
Logical / bitwise & \| ! logical_and/or/not/xor, bitwise_and/or/xor, invert
Unary math - sqrt, exp, log, sin, cos, abs, negative, square, sign, floor, ceil, …

Text ufuncs

Ufuncs are not limited to numbers. np.add concatenates text, and the six comparison ufuncs compare strings with broadcasting, out= and where=. Fixed-width S / U repetition needs a sized output when calling np.multiply directly; np.strings.multiply(a, i) sizes one for you. A StringDType (T) operand can allocate its variable-width result through np.multiply or *.

var names = np.array(new[] { "cat", "dog" });
var tagged = np.add(names, "!");                   // ['cat!' 'dog!']
var repeated = np.strings.multiply(names, 2);     // ['catcat' 'dogdog']
var text = names.astype(np.dtypes.StringDType());
var joined = np.sum(text);                        // 'catdog': concatenation, not numeric addition

The dtype determines which loops exist. np.sum(names) and np.max(names) refuse fixed-width strings as NumPy does, while their T forms compute. np.sqrt has no text loop. np.equal does not strip whitespace; the legacy np.@char.equal function does and has no ufunc out= / where= parameters. See Computing on text for the string functions and NA rules.


Broadcasting

Ufuncs apply broadcasting so inputs of different shapes still combine - a size-1 dimension is stepped with stride 0 (the same stored element feeds every position along that axis):

var a = np.ones((3, 4));
var b = np.array([1, 2, 3, 4]);   // (4,)
a + b;                                     // (3, 4) - b broadcast across rows

See Broadcasting for the full shape rules.


Type casting

When a ufunc's inputs have different dtypes, NumSharp picks a result dtype with NumPy 2.x's promotion rules (NEP 50): the inputs are cast to a common type, the loop runs there, and the result carries that dtype.

var i = np.array([1, 2, 3]);       // int32
var f = np.array([1.5, 2.5, 3.5]); // float64
(i + f).dtype;                             // float64  - int32 promoted

A key NEP 50 subtlety: weak scalars (C# primitive literals) adopt the array's dtype rather than upcasting it, while strong operands (arrays, 0-d NDArray) promote normally:

var x = np.array([1, 2, 3], np.int8);
(x + 1).dtype;                             // int8  - weak scalar 1 does not upcast
(x + np.array([1])).dtype;         // int32 - the int32 array (strong) forces promotion

Numeric promotion and descriptor-based text promotion are described in Data types → Type promotion and NumPy Compliance. A text width or StringDType NA policy cannot be represented by the numeric 15×15 enum table.


out=, where=, and dtype=

The elementwise ufuncs take NumPy's three keyword-style options, exposed as one overload shaped like NumPy's signature - f(x[, x2], NDArray out = null, NDArray where = null, DType dtype = null):

var a = np.array([1.0, 2.0, 3.0, 4.0]);
var dst = np.zeros(4);

np.sqrt(a, @out: dst);                          // write into dst, return it
np.add(a, 10, @out: dst);                       // dst = a + 10
np.add(a, 10, @out: dst, where: a > 2);         // only where the mask is true; other slots keep prior contents
np.add(0.1, 0.2, dtype: np.float32);            // run the loop at float32 precision
  • out - write the result into an existing array (any compatible layout); it is returned. out may alias an input (overlap-safe). The result is cast to out's dtype under NumPy's same_kind rule.
  • where - a boolean mask that broadcasts to the output; positions where it is false keep their prior contents (so pair where with a meaningful out).
  • dtype - selects the loop dtype, i.e. the precision the computation runs at (np.add(0.1, 0.2, dtype: np.float32) computes in float32 even into a float64 out). It also gates which loops exist - a float-only ufunc rejects an integer dtype.

@out is spelled with the @ because out is a C# keyword. Pass where/dtype by name.


Reductions

A reduction collapses an axis with a binary ufunc (sum, product, min, max, …). NumSharp exposes these as direct functions with axis, dtype, and keepdims:

var x = np.arange(9).reshape(3, 3);
np.sum(x, axis: 1);                 // [3 12 21]  - reduce along axis 1
np.sum(x);                          // 36         - reduce all axes (no axis)
np.prod(x.astype(np.float64), axis: 0);
np.max(x, axis: 0, keepdims: true); // shape (1, 3)

Axis tuples. Every reduction takes NumPy's axis as an NDAxis: one axis, a tuple - np.sum(x, (0, 2)), np.max(x, new[] { 0, 1 }), x.mean((0, 2)) - or null for None. That covers np.sum, np.prod, np.mean, np.std, np.var, np.max / np.min (amax / amin), the nan reductions, np.all, np.any, np.count_nonzero, np.ptp, np.average and the median and quantile families (and np.flip, np.squeeze, np.expand_dims, np.moveaxis, np.roll, np.trim_zeros, np.size, np.gradient, np.apply_over_axes, np.fft.fftshift / ifftshift, np.linalg.norm / vector_norm). A tuple is ONE reduction over the whole axis set, as in NumPy - not one axis after another, which rounds floats differently. np.argmax / np.argmin / np.nanargmax / np.nanargmin take a single axis, and a tuple is NumPy's TypeError. Each of these functions also keeps an int and an int[] overload (an int? one for the single-axis functions) that forwards to the NDAxis form, so np.sum(x, 1), np.sum(x, new[] { 0, 2 }) and np.sum(x, null) bind them with the same answers; the one spelling that does not compile is np.sum(x, default) (ambiguous between int and int[]) - write null.

The reduce upcast rule

For sum/prod/cumsum/cumprod with no explicit dtype, an integer or boolean input smaller than the default integer is upcast to int64 - uint64 for unsigned inputs - to avoid overflow, matching NumPy:

var x = np.array([1, 2, 3], np.int32);
np.sum(x).dtype;        // int64  - accumulating reductions widen (uint64 for unsigned)
np.mean(x).dtype;       // float64
np.max(x).dtype;        // int32  - min/max/amax preserve the input dtype

abs, sign, min, max, and the comparisons preserve the dtype; only the accumulating reductions widen. This is NEP 50 alignment - see the DirectILKernelGenerator NEP50 table and Data types.

Pass an explicit dtype to control it: np.sum(x, dtype: np.float64).


NumSharp does not expose ufunc-object methods

NumPy attaches reduce, accumulate, reduceat, outer, and at to the ufunc object (np.add.reduce(x), np.multiply.accumulate(x), np.add.at(x, idx, v)). NumSharp has no ufunc-object surface - use the direct functions instead:

NumPy ufunc method NumSharp equivalent
np.add.reduce(x, axis) np.sum(x, axis)
np.multiply.reduce(x) np.prod(x)
np.maximum.reduce(x) / np.minimum.reduce(x) np.max(x) / np.min(x)
np.add.accumulate(x) np.cumsum(x)
np.multiply.accumulate(x) np.cumprod(x)
np.add.at(x, idx, v) (unbuffered scatter-add) np.add(x, v, @out: ...) / np.put/np.place (note: no accumulating scatter)
np.add.outer(a, b) a[Slice.All, np.newaxis] + b (broadcast; newaxis is an index object, not a slice-string token)

np.maximum/np.minimum (the elementwise pairwise ufuncs) exist as direct functions and are distinct from np.max/np.min (the reductions), exactly as in NumPy.


Fused expressions - np.evaluate (NumSharp extension)

Chaining ufuncs (a * b + c) normally allocates an intermediate per operation. NumSharp adds np.evaluate, which compiles an expression tree into one pass - every elementwise node runs inside a single inner loop, reading each operand once and allocating no intermediates:

NDArray r = np.evaluate((NDExpr)a * b + 2);                 // fused a*b+2, one pass
NDArray s = np.evaluate(NDExpr.Sum((NDExpr)a * b));         // fused sum(a*b), no temp

np.evaluate compiles numerical expressions. For text transformations, use np.strings and np.@char directly.

This is the NumSharp analog of numexpr.evaluate - measured 3–6× faster than the equivalent NumPy chain on large arrays. Per-node dtypes follow the same NEP 50 rules as the individual ufuncs. See NDIter and the API reference.


Common patterns

In-place elementwise transform

np.multiply(a, 2.0, @out: a);        // double a in place (out aliases input)

Masked update

np.add(a, 100, @out: a, where: a < 0);   // add 100 only to negative elements

Reduce with overflow safety

np.sum(np.array(new byte[] { 200, 200, 200 }));   // uint64 result (600) - no uint8 overflow

Troubleshooting

"np.add.reduce doesn't exist"

NumSharp has no ufunc-object methods. Use np.sum (see the table above).

"My int32 sum came back int64"

That's the reduce upcast rule (NEP 50) - accumulating reductions widen small integer inputs. min/max/abs preserve the dtype. Pass dtype: to override.

"out didn't change / masked positions had garbage"

With where, the false positions keep whatever out held before - initialize out (e.g. from a copy) so masked-off slots are meaningful.

"A weak scalar didn't upcast my array"

By design (NEP 50): int8_array + 1 stays int8. Use a strong operand (+ np.array([1])) or an explicit dtype to promote.


API reference

Feature Form
Elementwise ufunc np.add(a, b), np.sqrt(a), operators + - * /, == < >, & \| !
out= np.sqrt(a, @out: dst) - write into dst, returned; may alias an input
where= np.add(a, b, @out: dst, where: mask) - masked; false slots keep prior contents
dtype= np.add(a, b, dtype: np.float32) - selects the loop precision/loop
Reductions np.sum, np.prod, np.min, np.max, np.mean, np.std, np.var, np.cumsum, np.cumprod (with axis, dtype, keepdims)
Pairwise np.maximum, np.minimum, np.fmax, np.fmin
Fused np.evaluate((NDExpr)…), NDExpr.Sum/Prod/Min/Max/Mean