Skip to content

NumSharp 0.80.0 - Strings Stable Release

Latest

Choose a tag to compare

@github-actions github-actions released this 08 Nov 16:59
d523eb4

We are at 95.2% NumPy API coverage, and everything except datetime, complex64 and other exotic dtypes matches NumPy byte for byte.
This release brings NumPy's text dtypes with numpy.strings and numpy.char, numpy.ma, numpy.polynomial, np.emath and 48 new np.* functions.
Integration rather than competition goes on, this time inside .NET: an NDArray now runs through ONNX Runtime models, serves as an ML.NET IDataView and shares its memory with System.Numerics.Tensors.

🧭 Overview

  • NumPy 2.x API coverage around 95.2%, everything except datetime, complex64 and other exotic dtypes is fully supported, with byte-to-byte equality and compatibility to NumPy.
    • Text - NumPy's fixed-width bytes_ (S) and str_ (U) dtypes and NEP 55's variable-width StringDType, with every computation NumPy runs on text, all 46 numpy.strings functions and numpy.char with chararray.
    • np.polynomial.* - the whole numpy.polynomial package, all 193 functions and the six series classes for all six bases, bit-exact.
    • np.* - +48 functions, the ten most used on GitHub: np.hypot, np.histogram, np.logspace, np.gradient, np.vectorize, np.hanning, np.geomspace, np.apply_along_axis, np.trapezoid and np.histogram2d.
    • np.random.* - every NumPy bit generator, the whole Generator distribution surface, and a legacy RandomState that is NumPy's own sampler code line for line, all byte-identical streams.
    • np.ma.* - the masked-array module; np.emath.* - the automatic-domain math module; np.matrix - numpy.matrix as the new NDMatrix type.
  • DType is NumPy's two-level dtype model - NDArray.dtype returns a numpy.dtype-shaped descriptor, with np.dtypes, byte order and datetime64 / timedelta64 descriptors, and NumPy's scalar classes (np.integer, np.floating, np.number, ...) with np.typing.
  • Reductions on NumPy's own schedules - max, min, ptp, nanmax, nanmin, argmax, the flat sum / mean and the fused np.evaluate reductions match NumPy bit for bit, down to the sign of a ±0 tie and a NaN's payload; mean, var, std and their NaN-aware forms are NumPy's own statements; and most of them run faster.
  • NumPy's structure for the engine - every computing np.* function is a TensorEngine member named after its NumPy source file, numpy.linalg and numpy.fft are replaceable sub-engines, and the default engine is settable.
  • The 2026-11-02 parity QA - 53 of its 55 findings fixed on the owner's decisions (one kept as a documented divergence, one deferred), and NumPy's own dot functions behind np.dot / np.vdot / np.vecdot.
  • 86 NumPy-parity, memory-safety and tooling fixes, 40 measured speedups and 44 breaking changes, across the release's 1,670 commits.
  • 3 new NuGet packages - NumSharp.Interop.System.Numerics.Tensors, NumSharp.Interop.OnnxRuntime and NumSharp.Interop.MLNet.
  • 1,119,305 NumPy 2.4.2 oracle test cases (0.70.0: 119,517) and 20,506 test methods (0.70.0: 14,416).

📦 New NuGet Packages

  • NumSharp.Interop.System.Numerics.Tensors - zero-copy interop between NDArray and the BCL's Tensor<T> / TensorSpan<T> (System.Numerics.Tensors), all 15 dtypes.
  • NumSharp.Interop.OnnxRuntime - run ONNX Runtime models on NDArrays, fed from the arrays' own memory, with the outputs read back as NDArrays.
  • NumSharp.Interop.MLNet - an NDArray as an ML.NET IDataView, and a transformer's output column back as an NDArray.

Detailed Breakdown

Show All

📦 New NuGet Packages

Three optional companion packages ship for the first time, co-versioned with NumSharp (each depends on NumSharp). None computes NumSharp operations: each converts arrays between NumSharp and another library, with no engine backend and no native asset of its own, so Core stays 100% managed without them.

  • NumSharp.Interop.System.Numerics.Tensors - zero-copy interop between NDArray and System.Numerics.Tensors (Tensor<T>, TensorSpan<T>, ReadOnlyTensorSpan<T>); all 15 NumSharp dtypes cross as their own CLR type, Half, Complex, decimal and char included.
    • Export - AsTensorSpan<T>() views an NDArray of any non-negative-stride layout (contiguous, sliced, transposed, strided, broadcast) as a TensorSpan<T> over the same unmanaged buffer, through a disposable handle that keeps the buffer pinned; ToTensor<T>() copies into a dense Tensor<T>.
    • Import - AsNDArray() views a Tensor<T> as an NDArray without copying (writes go through to the tensor, and the pinned array counts toward NumSharp's memory accounting until the last view is disposed); ToNDArray() copies a Tensor<T>, TensorSpan<T> or ReadOnlyTensorSpan<T>.
    • Text - S / U / StringDType arrays cross as a Tensor<string> copy in both directions (ToStringTensor() / ToNDArray(Tensor<string>)); the view verbs refuse text with one message, because a Tensor<string> holds managed strings.
    • The exported spans feed the BCL's SIMD TensorPrimitives directly, with no fill loop.
    • Limits - a negative-stride view is refused, because the BCL forbids negative strides (copy it first); the copies stop at the managed-array size limit; live export and import counters help leak checks.
    • Depends - System.Numerics.Tensors 10.x only, a managed BCL package. Its tensor types are experimental, so the bridge marks its own API experimental too and you opt in once. No engine backend and no native asset: Core stays 100% managed.
  • NumSharp.Interop.OnnxRuntime - feed an NDArray to an ONNX Runtime InferenceSession from the array's own memory and read the outputs back as NDArrays; no Python.
    • Modes - every entry that can share memory takes Auto (a view when ONNX Runtime can read the buffer in place, else a copy the handle owns), View (refuses to copy) or Copy.
    • Export - AsOrtValue / ToOrtValue, and AsDenseTensor / ToDenseTensor for ONNX Runtime's legacy tensor tier; import - ToNDArray copies (a legacy Tensor<T> too), AsNDArray views and can own the value.
    • Running a model - session.Run(...) binds the inputs by the model's names, checks each dtype with NumPy's casting rules and each declared shape, and refuses a missing required input (with onnxruntime's own message) or an unknown output name before the run; RunInto fills pre-allocated outputs; RunAsync / RunIntoAsync run ONNX Runtime's asynchronous inference (1.30.0 or later) and size a dynamic dimension (batch_size, N) from the fed inputs; the outputs of Run and RunAsync leave an NDScope (scope.ReturnsTask(session.RunAsync(...))).
    • Threads and disposal - every OrtValue the bridge touches is released once, after its last use: a Dispose from any thread, during a verb or while views of the value live, neither crashes nor hands out another value's data, and a disposed session or value is refused with ObjectDisposedException; NDInferenceSession, an InferenceSession with ONNX Runtime's constructors, releases a session once, after the bridge's runs on it.
    • Memory - ToOrtValue's copy and every view the bridge hands out count toward NumSharp's memory accounting, so forgotten ones get collected: 5,000 forgotten ToOrtValue copies of a 602 KB array peak at +68 MB instead of +2,898 MB.
    • Text and non-tensor values - fixed-width U / S arrays cross as string tensors (a copy; ReadStringTensor reads one as string[]), and a StringDType array is refused with its sized-U remedy; ToNDArrays, ToMap, ToMaps and GetMapKeys read sequence and map outputs.
    • Post-processing - Softmax, LogSoftmax, Sigmoid, Argmax, TopK.
    • Dtypes - 12 cross without a copy, float16 by bit reinterpretation; char exports as uint16 and decimal as float64 through a copy the handle owns; complex and bfloat16 are refused, each refusal naming the route that works.
    • Depends - Microsoft.ML.OnnxRuntime.Managed 1.16.0 or later, the C# API only: ONNX Runtime's native flavours exclude each other, so you reference the one you run. Verified on 1.16.0 and 1.31.1.
  • NumSharp.Interop.MLNet - expose an NDArray to an ML.NET pipeline as an IDataView and read a transformer's output column back as an NDArray, with no POCO class and no row-by-row fill.
    • Export - AsDataView is a lazy IDataView over the buffer's strides (one vector column, or one scalar column per feature, a repeated name refused); ToDataView takes a snapshot; ToVBuffer copies into a VBuffer<T>.
    • Tables - AsDataView of a dictionary of named arrays is one view with a column per array, each in its own dtype ({"Features": X, "Label": y}, as pandas' DataFrame), so a model trains and scores from arrays with no hstack and no conversion step.
    • Shuffling - a view reports CanShuffle and walks a full permutation of its rows when a trainer asks for a shuffled cursor, so SDCA trained on class-ordered rows passes 0.65 accuracy (ML.NET's pool shuffle left it at 0.50).
    • Import - ToNDArray reads a scalar column as (R,) and a fixed-size vector column as (R, C); AsNDArray views a dense VBuffer<T>'s array without copying, and the pinned array counts toward NumSharp's memory accounting until the last view is disposed.
    • Feeding a model - transformer.Transform(nd.AsDataView("Features")).ToNDArray("Score"): the IDataView is the whole bridge.
    • Post-processing - the ONNX Runtime bridge's Postprocess (Softmax, LogSoftmax, Sigmoid, Argmax, TopK), which this package references, so an app that uses both bridges has one.
    • Dtypes - 11 column types directly, char one way as uint16, float16 as float32 and decimal as float64 through a copy the view owns; complex and text arrays are refused, and a text, time or ragged column's refusal names the ML.NET route that reads it (GetColumn<string>, int64 ticks, GetColumn<T[]>).
    • Depends - Microsoft.ML.DataView 2.0.0 or later, and NumSharp.Interop.OnnxRuntime for Postprocess (bringing ONNX Runtime's C# API, which Postprocess never loads); the Microsoft.ML you reference brings the trainers and transforms. Verified on ML.NET 2.0.1 and 5.0.1.

📊 Dashboards & Docs

  • Supported Features Dashboard - now reports, for every API, whether its behavior is oracle-verified (committed NumPy 2.4.2 contracts resolve to it, directly or through a reviewed alias) next to whether it is available, and whether its C# call is NumPy's (one overload with exactly NumPy's parameters under NumPy's name). Headline 95.2% available (533/560, 4 more partial; 0.70.0: 87.1%, 488/560), 95.9% oracle-verified (537/560), 244 of 560 NumPy-identical calls.
    • An oracle filter in the API explorer and an evidence pane per API: contract and error-contract counts, dtypes, layouts, corpus files and host pins; a filter per kind of signature difference.
    • The catalog also covers what lies beyond the headline - every module NumPy lists as public (numpy.ma, numpy.emath, numpy.polynomial, numpy.char, numpy.strings, numpy.dtypes, numpy.exceptions, numpy.typing, ...), the members of its objects (nditer, dtype, finfo, the random classes), every Python protocol its classes define (ndarray.__add__, ndarray.__getitem__, nditer.__enter__, ...), the members of NumPy's scalar types, and every method of every ufunc (np.add.reduce, np.maximum.accumulate, np.add.at, ...): 2,017 of 4,844 APIs available in that expanded scope and 1,615 oracle-verified, the dashboard's default view.
  • Tests & Oracle Dashboard - 1,119,305 committed NumPy 2.4.2 oracle test cases over 1,448 operations (0.70.0: 119,517 over 372), and 20,506 test declarations (0.70.0: 14,416).
  • Benchmark Dashboard - benchmark evidence for 452 of 518 benchmarkable APIs (0.70.0: 455 of 471), and benchmark/strings: 442 text cells at geomean x6.61 NumPy's speed.
    • The 47 APIs this release makes benchmarkable have no benchmark yet, and np.remainder, np.random.ranf and np.random.sample, which 0.70.0 credited through np.mod and np.random.random_sample, are methods of their own now, with no benchmark of their own.
  • Documentation site - new guides, whose code samples run as tests:

✨ New APIs & Modules

  • NumPy's fixed-width string dtypes bytes_ (S) and str_ (U) - real dtypes whose descriptor carries the class and the width (<U5, |S3), byte-identical to NumPy 2.4.2.
    • np.array(string[]), "U5" / "S3" in every dtype parameter, NDArray.Scalar(string), a zero-copy np.frombuffer(bytes, "S3") - creation.
    • item(), tolist(), NDArray.AsStringArray, stores cut to the width; promotion, can_cast and the np.dtype grammar; views, joins, gathers and indexing in every layout; astype between widths and classes; printing and .npy - the dtype end to end.
    • A number written as text is its NumPy scalar's str(), a text read as a number is CPython's int() / float() / complex(), through every route that converts.
  • NEP 55 StringDType - np.dtypes.StringDType(na_object, coerce) and np.dtype("T"): NumPy's variable-width UTF-8 text in 16-byte cells over a per-buffer arena, with its NA object, coercion rules, promotion and casting table, NumPy's cells byte for byte, and np.ndarray(shape, StringDType, buffer:) views of another array's cells; a masked np.copyto into it converts only the elements the mask selects.
  • Computing on text - every computation NumPy runs on S / U / StringDType text computes with NumPy's results, NA rules and refusals in NumPy's order -
    • the six comparisons and their operators, the truth readers (astype(bool), np.any / np.all, np.nonzero, np.where, the logical ufuncs), concatenation (+) and repetition.
    • np.sort / np.argsort / np.partition (NumPy's order of equal strings for every kind), np.argmax / np.argmin, np.searchsorted / np.digitize, np.unique and the Array-API unique family, np.isin, the set routines, np.lexsort, the histograms and the element-picking quantiles.
    • np.sum / np.cumsum / np.max / np.min / np.trace of StringDType, reductions into a text out= or dtype=, np.pad over text, and numpy.ma over text.
  • np.strings - all 46 of numpy.strings' functions over S / U / StringDType with NumPy's arguments, results and errors: CPython 3.12's Unicode 15.0.0 tables for the predicates and case maps on every .NET, its codecs and error handlers for encode / decode.
    • str_len, find, rfind, index, rindex, count, startswith, endswith - lengths and searches.
    • strip, lstrip, rstrip, center, ljust, rjust, zfill, expandtabs, replace, partition, rpartition, slice - editing.
    • isalpha, isdigit, isspace, isalnum, islower, isupper, istitle, isdecimal, isnumeric - the predicates, with out= / where= / dtype=.
    • upper, lower, swapcase, capitalize, title, translate - case maps and translation.
    • add, multiply, mod, encode, decode, and equal ... greater_equal under their numpy.strings names.
  • np.@char - NumPy's numpy.char module (char is a C# keyword) with chararray as NDCharArray.
    • The comparisons that strip trailing whitespace, compare_chararrays, and every np.strings function re-exported.
    • chararray - construction, element access that strips as Python's rstrip() does, the + / * / % operators, its string members, and the np.* rules that keep, wrap or drop the class as NumPy's do.
    • split, rsplit, splitlines, join - the object path, whose results are NDListArray, NumPy's object array of lists with 65 of its 70 ndarray members.
  • Text I/O - np.loadtxt / np.savetxt / ndarray.tofile over StringDType with Python's codec names (encoding:), np.save / np.load of StringDType as NumPy's own pickle (allow_pickle), and np.fromstring(byte[]).
  • np.evaluate over text - S / U / StringDType arrays and a C# string are NDExpr leaves: the comparisons, truth readers, IsNaN, casts to and from text, NDExpr.StrLen / Find / StartsWith / the predicates, concatenation, Where with text branches and the root reductions compute as NumPy's statements do.
  • np.polynomial - the numpy.polynomial package for all six bases (polynomial, chebyshev, legendre, laguerre, hermite, hermite_e) plus polyutils: all 193 of its functions and the six series classes, bit-exact with NumPy 2.4.2 against generated oracles (179,118 cases, 0 excused); {p} below is poly / cheb / leg / lag / herm / herme.
    • {p}val, {p}val2d, {p}val3d, {p}grid2d, {p}grid3d, {p}valnd, polyvalfromroots - evaluation.
    • {p}add, {p}sub, {p}mulx, {p}mul, {p}div, {p}pow, {p}fromroots - series arithmetic.
    • {p}der, {p}int - calculus, with every m / k / lbnd / scl / axis form.
    • {p}vander, {p}vander2d, {p}vander3d - Vandermonde matrices.
    • {p}fit (every deg / rcond / full / w form) and chebinterpolate - fitting and interpolation.
    • {p}companion, {p}roots - companion matrices and roots (the roots compute through the OpenBLAS backend's eigvals).
    • chebweight ... hermeweight, chebpts1, chebpts2, chebgauss, leggauss, laggauss, hermgauss, hermegauss - weights, points and Gauss quadrature.
    • {p}trim, {p}line, {p}domain, {p}zero, {p}one, {p}x, the five X2poly / poly2X pairs - helpers, module constants and basis conversions.
    • as_series, trimseq, trimcoef, getdomain, mapparms, mapdomain, format_float, set_default_printstyle - polyutils and the printing helpers.
    • Polynomial, Chebyshev, Legendre, Laguerre, Hermite, HermiteE - the series classes (NDPolynomial ... NDHermiteE over NDPolyBase) under NumPy's names: construction, Python's operator protocol, evaluation and composition, fit / fromroots / roots / convert / integ / deriv and the rest, the printers and pickling.
    • Speed - x1.78-x37.8 NumPy's speed by benchmark geomean per family (evaluation x7.5, additive x8.5, calculus x7.6, series algebra x37.8, Vandermonde x10.2, roots x4.65, evaluation from roots x13.9, fitting x1.96, interpolation x7.20, points x3.37, Gauss quadrature x4.54, weights x1.78, printing x11.45, the series classes x8.22).
  • np.random bit-generator family - NumPy's engines, seeding and stream splitting, byte-identical to NumPy 2.4.2 and replayed overload by overload (493 members, 5 engines x 10 seeds).
    • PCG64DXSM, Philox, SFC64 - new bit generators; PCG64.advance, jumped, random_raw, state - engine controls; MT19937 is now a NumPy BitGenerator too.
    • SeedSequence.spawn, spawn_key, pool_size, state, BitGenerator.spawn, Generator.spawn, ISeedSequence - seeding and independent streams.
    • default_rng - every NumPy seed form (integers, BigInteger, arrays, SeedSequence, a bit generator, a Generator, a RandomState, null for fresh entropy).
    • np.random.PCG64(42), np.random.Generator(...), np.random.SeedSequence(...) and the other classes - NumPy's module spellings; get_bit_generator, set_bit_generator.
  • Generator distributions - NumPy's whole numpy.random.Generator surface on NumPy's modern algorithms, 29 samplers byte-identical to default_rng.
    • beta, chisquare, f, noncentral_chisquare, noncentral_f, standard_t, standard_cauchy, vonmises - the gamma family and friends.
    • pareto, weibull, power, laplace, gumbel, logistic, lognormal, rayleigh, wald, triangular - one transform per draw.
    • binomial, negative_binomial, poisson, zipf, geometric, hypergeometric, logseries - discrete.
    • multinomial, dirichlet, multivariate_normal (svd / eigh / cholesky), multivariate_hypergeometric (marginals / count) - multivariate.
    • pareto / power stay within 2 ULP / ceil(2/a)+3 ULP of NumPy, whose C runtime expm1 has no portable twin; multivariate_normal is byte-identical with the OpenBLAS backend and may pick the other eigenvector sign without it.
  • Array-valued distribution parameters - the 29 parameterized samplers of both RandomState and Generator take NDArray parameters, broadcast against size as NumPy does, byte-identical streams.
    • integers, randint - array bounds and BigInteger bounds, so g.integers(0, BigInteger.Pow(2, 64), dtype: np.uint64) ports verbatim.
  • Legacy RandomState gains NumPy's form over any bit generator (np.random.RandomState(new PCG64(42))), the dict get_state(legacy: false) / set_state, tomaxint, ranf and sample.
  • np.ma - NumPy's masked-array module on a new NDMaskedArray type, bit-exact vs NumPy 2.4.2 and gated by 109,961 differential cases in 13 tiers plus a 27,240-case surface tier.
    • The ufunc wrappers, masked reductions (mean / var / std / anom as numpy.ma's own statements), operators, creation, extrema and manipulation - the core.
    • average, median, dot, the set operations, sort, unique, cov, corrcoef, convolve, correlate, polyfit, apply_along_axis, apply_over_axes - the extras.
    • The masked indexer, hard masks and byte-exact masked str / repr; only the structured/record-dtype functions are missing.
  • np.emath - automatic-domain sqrt, log, log2, log10, logn, power, arccos, arcsin, arctanh (an out-of-domain input gives the complex result instead of NaN).
  • np.matrix - NumPy's numpy.matrix as the type NDMatrix (80 of its 83 members), held to NumPy 2.4.2's values, class and shape by two generated oracles (26,869 and 4,645 cases).
    • np.matrix, np.asmatrix, np.bmat - construction from arrays, sequences and "1 2; 3 4" literals.
    • T, H, I, A, A1 - the transposes, the inverse and the plain-array views.
    • *, ** - the matrix product and the matrix power; an index keeps rows and columns two-dimensional.
    • sum, prod, mean, var, std, any, all, max, min, argmax, argmin, ptp - reductions that keep two axes.
    • np.* functions return NumPy's class and shape for a matrix operand: np.sum(m, 0) is a 1 x N matrix, np.linalg.inv(m) a matrix, np.ravel(m) a plain array, and np.stack raises as in NumPy; np.max / np.min take the method's keyword and out= rules (np.max(m, initial=1) raises its TypeError), np.nanmax / np.nanmin NumPy's subclass route.
  • 48 new np.* functions, NumPy 2.4.2 parity, each gated by the differential-fuzz oracle -
    • hypot (correctly rounded: float32/float16 bit-exact, float64 bit-exact on ~91% of inputs and 1 ULP more accurate on the rest), divmod, fmod, remainder, float_power, heaviside, spacing, frexp, ldexp, gcd, lcm, fabs, signbit, fix - elementwise math ufuncs.
    • sinc, i0, real_if_close, unwrap - special functions and signal helpers.
    • bartlett, blackman, hamming, hanning, kaiser - window functions, bit-identical.
    • trapezoid, gradient - numerical integration and differentiation, bit-exact on every layout.
    • histogram, histogram_bin_edges, histogramdd, histogram2d - histograms.
    • packbits, unpackbits, bitwise_count - bit packing and population count.
    • piecewise, putmask, put_along_axis - indexed and conditional assignment.
    • vectorize, frompyfunc, apply_along_axis, apply_over_axes, broadcast_shapes - functional and shape helpers.
    • array_equiv, may_share_memory, shares_memory (with TooHardError) - array predicates.
    • logspace, geomspace - log-scale ranges, bit-exact.
    • binary_repr, base_repr, typename - string helpers.
  • Existing APIs gain NumPy's parameters -
    • maximum, minimum, fmax, fmin - out= / where=.
    • left_shift, right_shift, logical_and, logical_or, logical_xor, logical_not, modf - out= / where= / dtype=.
    • array_equal - equal_nan=.
    • indices, indices_sparse - long[] / Shape dimensions (np.indices(arr.shape)), and mgrid past 2^31 elements.
  • The max / min family's keywords - np.max / np.min / np.amax / np.amin / np.nanmax / np.nanmin and ndarray.max / ndarray.min take NumPy's out=, initial= and where= in NumPy's order, through a port of NumPy's reduction iterator: the loop is promote_types(out, a), initial is packed into it as NumPy packs a Python scalar, and a mask without initial raises NumPy's no-identity error; bit-exact over a 35,847-case minmax_kwargs oracle tier with zero excuses. Plain np.max / np.min of complex128 along a reversed axis and of a broadcast float input keep NumPy's first NaN and ±0, and an axis result over a permuted input keeps NumPy's layout (237 cells, geomean x3.98; np.max(bool, axis=1) at 10M x0.19 -> x2).
  • The reductions' keywords - np.sum / np.prod take initial= and where=, np.mean where=, and np.var / np.std where=, mean= and correction=, bit for bit with NumPy.
  • weights= for np.quantile / np.percentile / np.nanquantile / np.nanpercentile (inverted_cdf), over numbers and text.
  • np.evaluate / NDExpr - the fused-expression DSL covers most of the ufunc surface and every reduction, NumPy-typed and bit-exact (29,893-case oracle tier).
    • Maximum, Minimum, FMax, FMin, Fmod, CopySign, NextAfter, LogAddExp, LogAddExp2, Hypot, Heaviside, Gcd, Lcm, LeftShift, RightShift - binary nodes.
    • Positive, Conjugate, Fabs, Spacing, SignBit, IsPosInf, IsNegInf, BitwiseCount, Rint, Real, Imag, Angle, Cast, Round(decimals) - unary nodes.
    • LogicalAnd, LogicalOr, LogicalXor, Select, Clip, and the < / > / <= / >= operators on NDExpr - logic and selection.
    • Any, All, CountNonzero, NanSum, NanProd, Ptp, NanMin, NanMax, ArgMax, ArgMin, NanMean, Var, Std (ddof), the weighted Average, axis forms and keepdims - reductions.
    • dtype=, casting=, order=, where= - keywords; expr.Compile() returns a reusable CompiledExpression.
    • If, Switch, Mux, Saturate, NanTo, Coalesce, Relu, LeakyRelu, Elu, Sigmoid, Swish, Softplus, Gelu, Step, Nand, Implies, IsClose, Between, Bucketize, Lerp and 18 more - 38 decision and activation combinators that fuse like any node.
  • DType system - NumPy's NEP 40-50 two-level dtype model (dtype classes and dtype instances), the descriptor every dtype parameter and NDArray.dtype now use.
    • np.dtypes (Float64DType and the other dtype classes), np.datetime_data - the class module.
    • num, kind, char, itemsize, str, descr, byteorder, newbyteorder, isbuiltin, isnative, flags, type_num (NPY_TYPES) - the full numpy.dtype surface.
    • isalignedstruct, metadata, len(dtype) / dtype[key] (__len__ / __getitem__) and np.dtype(dtype, align, copy, metadata); the dtype classes under NumPy's own names (type, _abstract, _parametric, _legacy, _is_numeric, singleton, type_num, default_descr(), common_dtype(), ensure_canonical(), castingimpls with each class pair's strided loop (get_strided_loop), DTypeFlags.NPY_DT_*, DType.NPY_NATIVE and the other byte-order macros).
    • Non-native byte orders (np.dtype(">i4")) and datetime64 / timedelta64 descriptors that parse, print, promote and cast-check (no datetime storage yet).
    • promote_types, result_type, can_cast, issubdtype, isdtype - DType overloads with NEP 50 weak scalars.
  • np.integer, np.floating, np.number and the rest of NumPy's scalar-type hierarchy - the abstract classes as the new NDScalarType, the class behind every dtype (a.dtype.type) and np.typing, held to NumPy 2.4.2 by a 6,123-case oracle section (the most-used NumPy names NumSharp lacked: np.integer alone appears in ~203K GitHub files).
    • generic, number, integer, signedinteger, unsignedinteger, inexact, floating, complexfloating, flexible, character - the abstract classes.
    • np.issubdtype(a.dtype, np.integer), np.isdtype(dt, np.floating), np.dtype(cls), np.finfo(cls), np.iinfo(cls) - the type tests and conversions take classes, with NumPy's identity rules and error texts.
    • __name__, __mro__, __bases__, __subclasses__(), __instancecheck__ (isinstance), __subclasscheck__ (issubclass) - the class surface.
    • np.complex64 / np.csingle, np.object_, np.void - the NumPy classes NumSharp has no dtype for, in their NumPy places, and Python's built-in classes in every __mro__ (numpy.float64 derives from float).
    • np.longdouble, np.clongdouble, np.intc / np.uintc (Windows), np.longlong / np.ulonglong (Linux and macOS) - the C-type classes NumPy keeps beside the sized class of the same storage, classes of their own (np.issubdtype(np.float64, np.longdouble) is False, as in NumPy).
    • np.typecodes, np.sctypeDict, np.ScalarType, np.True_, np.False_ - the relatives.
    • np.typing.NDArray, ArrayLike, DTypeLike, NBitBase - each names the C# type that plays the alias's role (npt.NDArray[np.float64] is NDArray<double>).
  • TensorEngine.Linalg, TensorEngine.Fft, TensorEngine.Default - numpy.linalg and numpy.fft as sub-engines whose protected virtual kernels a native package overrides, and a settable default engine that every new array binds.
  • Typed iteration and .NET interop -
    • np.flat<T>(a) / a.flatiter.AsTyped<T>() - typed by-ref C-order flat iteration.
    • np.ndenumerate<T>(a).AsRef(), np.ndindex(...).AsSpans() - allocation-free enumeration.
    • nd.Unsafe.Span<T>(), Memory<T>(), Bytes(), Pointer<T>(), TryGetSpan<T>(), TryGetMemory<T>() - zero-copy BCL views of the buffer (they do not keep it alive: hold the NDArray); nd.AsEnumerable<T>() and the typed iterators' ToArray() / CopyTo(Span<T>) - safe copies.
  • Py.RegisterNumSharpCodec() and Py.RegisterNumSharpArrayAdapter(adapter) (NumSharp.Interop.pythonnet) - the one-call codec and array-adapter registrations, spelled on pythonnet's own Py (C# 14 extension members).
  • Text crosses NumSharp.Interop.pythonnet - S / U arrays share memory with numpy in both directions, StringDType crosses as an exact copy through numpy's NpyString C API, and an object array of str (a pandas text column) copies as StringDType.
  • NumSharp.Collections - ConcurrentOrderedDictionary<TKey,TValue> and ConcurrentOrderedCompactDictionary<TKey,TValue> (about a third of the memory): insertion-ordered, index-addressable concurrent dictionaries.
  • Extension points - the surface NumSharp's own subclasses, engines and bridges build on is open to a package of yours:
    • __array_priority__ and the protected internal subclass hooks __array_wrap__, __array_finalize__ and _collapse, under NumPy's names, which NDMatrix and NDCharArray override; the np.* functions route only those two classes through them, so a subclass of your own gets plain arrays back from them (np.nanmax / np.nanmin excepted: they dispatch virtually).
    • Public constructors for NDMatrix, NDCharArray, NDMaskedArray, NDMaskedConstant, NDPolyFitResult, NDArray<T>(Shape, bool, TensorEngine) and NDArray(DType, Shape, bool, TensorEngine).
    • BitGenerator is a public abstract base (NextUInt64, NextUInt32, the state hooks), and Generator, MT19937, PCG64, PCG64DXSM, Philox and SFC64 can be subclassed, as NumPy's can.
    • Shape's raw layout (dimensions, strides, offset, size, bufferSize) and its view constructor, TensorEngine.Evaluate(program, ...), DotFunction1D / VdotFunction1D, and the bridge surface: SizeBucketedBufferPool.BookForeign / ReturnForeign, ArraySlice.WrapShared and VStringAllocator with NumPy's NpyString C API.

🧩 ndarray surface

  • ToArray<T>() - a T other than the dtype converts like astype (it threw ArrayTypeMismatchException), and every layout copies in one pass (~5.8x faster than before on the measured set).
  • ToMuliDimArray<T>() / (Array)nd - one pass over any layout; float16, complex128, a T other than the dtype (converted like astype), 0-d and empty decimal arrays now work (they threw).
  • ToJaggedArray<T>() - any rank (rank 6 came back wrong, deeper ranks threw), any layout (a transposed view gave wrong values, a column slice read out of bounds) and a T other than the dtype; 10.7x faster with 7.7x less memory.

⚡ Performance

Ratios are NumPy ÷ NumSharp - higher is better (x2 = twice NumPy's speed); xLOW->xHIGH spans the worst→best measured cell across sizes and dtypes.

  • x0.70->x3.1 - np.max / np.min / np.ptp on NumPy's exact reduction schedules, slab reductions folding eight reduced indices per pass (axis cells on non-C layouts were x0.28-x1.0; L3-bound 4M cells of 1- and 4-byte lanes still trail).
  • x0.41->x4.7 - np.nanmax / np.nanmin (float32/float64 axis cells were x0.15-x0.50, flat C now x2.89 / float32 x3.33; float16/complex128 non-C axes run NumPy's scalar loop at x0.41-x0.69).
  • x0.77->x106 - np.argmax / np.argmin along an axis walk the strides instead of a per-output div/mod walk (axis 0: float64 x4.4-x8.8, integers x10-x16, bool x95-x106; memory-bound contiguous rows at 4M x0.77-x1.04), and the 64-bit flat argmax runs NumPy's single-pass tournament.
  • x0.89->x2.65 - np.mean / np.sum along an axis with a large output: the identity seed and the mean's division run as typed block passes (np.mean(a, axis=0) of 3x1M float32 was x0.29; mean(axis=1) over 3-wide rows stays below NumPy).
  • x1.43->x1.83 - np.convolve long products without the OpenBLAS backend (1000x1000 float64 was ~x0.9, complex128 x0.25).
  • x1.15->x5.9 - complex128 divide, multiply and integer-exponent power, now on NumPy's own formulas.
  • x1.26->x4.0 - np.modf plain, out= and promoted calls (out= at 10M was x0.40).
  • x1.0->x16 - np.array_equal / np.array_equiv fused early-exit compare (x15-x16 at N=1000, parity at 100K, instant on an early mismatch).
  • x1.05->x6.4 - every np.random bit generator fills through a bulk layer: all 105 engine x op x size cells beat NumPy.
  • x1.08->x5.58 - legacy RandomState samplers (hypergeometric was x0.96, zipf x1.12, multinomial x0.97; the samplers bound by the C runtime's pow / log / exp stay at x1.17-x1.49).
  • x0.65->x17.6 - np.evaluate comparison, logical, where and bool trees vectorize instead of running the whole tree scalar (a > 0.5 was x0.16).
  • x1.0->x22.2 - np.evaluate mixed-dtype trees vectorize through lane groups (i4*2+f8 x14.2 and f4*f8+f4 x22.2 at 100K; 1K cells sit at the per-call floor).
  • x0.86->x53 - np.evaluate reductions stream block by block instead of materializing their child, and Any / All stop at the deciding block (any(a > b, axis=0) was x0.37; count_nonzero(a > b) at 100K sits at the read-bandwidth ceiling).
  • x2.06 - a small np.evaluate call skips the iterator like NumPy's trivial loop: a*b+c at n=8 runs in 255 ns vs NumPy's 526 (out=: 75 ns).
  • x1.2->x10^6 - Generator.multivariate_hypergeometric(method='count') keeps a large, lightly sampled urn sparse instead of filling sum(colors) entries before the first draw as NumPy does, byte-identical: [10**9, 1] with one item 3.3 s -> 3 µs (it allocated 8 GB), a 1.05M-item urn sampled 10 items x 20 variates x81; dense urns x1.2-x1.45, unchanged.
  • x0.87->x5.5 - the comparisons == / != / < / <= / > / >= on every operand layout and dtype pair: a stepped, reversed, cast or broadcast chunk runs the contiguous SIMD kernels, a matrix against a row, a column or an outer product skips the iterator, an F operand against a C one is compared tile by tile, an array against a scalar runs a vector loop, and complex128 and int64-against-uint64 get vector kernels (1,279 cells: geomean x1.15 -> x2.12, cells under x1.0 445 -> 22, the lowest x0.03 -> x0.87; m > 0.75 on a 512x512 float64 matrix x0.25 -> x1.0); buffers of one page or more start on a cache line, which alone makes two-array comparisons of 100,000 elements 12-25 % faster.
  • x1.05->x8.8 - np.sum / np.mean on NumPy's add.reduce schedule, every result bit-exact (272 cells, geomean x3.73; was x1.54 with 87 cells under x1.0, and the four complex128 -> float64 dtype= cells crashed the process): the pairwise folds run SIMD per run kind (contiguous, reversed, stride 2) and a buffered fill is folded in place, never materialized.
  • x2.33->x79 - float16 np.max / np.min / np.nanmax / np.nanmin / np.ptp on every layout, flat and per axis, through bit-level SIMD walks of NumPy's sequential HALF loops (276 cells, geomean x26.1: x5.6 at 1K, x41 at 100K, x79 at 1M; was geomean x1.60 with 169 cells under x1.5).
  • x0.8->x41 - np.clip with array bounds, out= and where=: the ternary kernels gain broadcast-pattern stages and broadcast a stride-0 bound instead of gathering it (row / column bounds at 100K were x0.2-x0.7 and run x5.1-x19.9; where= with a row bound x19.6-x40.9 at 100K); the plain contiguous clip at 100K is L2-bound on both sides (x0.8-x1.4).
  • x1.51->x38 - masked np.copyto is a bit-copy kernel per element width (165 masked cells, geomean x12.4: a 0-d source x38, an array or row source x23, a row mask x10.6), which also lifts numpy.ma's masked reductions from geomean x2.83 to x4.60.
  • x1.55->x7.9 - float16 add / subtract / multiply / divide with a row, column, strided, reversed or outer-product operand, and into out=, run the SIMD widen-compute-narrow kernels (100K cells were x0.30-x0.50 and a broadcast F set x0.23 -> x1.99; numpy.ma's float16 anom x0.86 -> x5.97).
  • x0.71->x80 - moving S / U and StringDType text: copies, gathers, np.where, np.repeat, np.array and the casts (StringDType geomean x5.35).
  • x1.14->x221 - comparisons and truth values of S / U text (geomean x12.3), concatenation and repetition (x1.14-x44), text <-> number (x2.0-x7.9).
  • x0.66->x45 - sorting, searching, set routines and statistics over S / U text (family geomeans x3.05-x4.86; the low cells wait on memory on both sides).
  • x1.54->x47 - computing on and ordering StringDType text (geomeans x3.31 and x7.80, every cell over x1.5).
  • x1.65->x540 - np.strings: searches, strips, pads and replace x1.65-x15.5, predicates x3.99-x33, case maps x4.50-x237, translate x10.8-x540, mod x10.7-x39.7, encode / decode x30.7-x317.
  • x1.03->x459 - np.@char and chararray (the stripping comparisons x2.6-x10.5, the member families' geomeans x2.70-x57.1, the object path x4.6-x48) and NDListArray's members (x1.92-x459).
  • x1.4->x30.8 - the text readers and writers over every dtype (219 cells, geomean x3.81), and StringDType through np.save / np.load (x2.1-x9.5) and np.loadtxt / np.savetxt (x2.9-x8.1).
  • x1.52->x46 - the quantile family over numbers (70 cells, geomean x5.45) and weighted (x1.52-x24).
  • x0.42->x34.7 - np.isin over numbers, its table method in one pass (26 cells, geomean x4.21).
  • x0.91->x15.6 - np.argsort and np.unique of float16 and complex128 sort by radix in NumPy's stable order (float16 was x0.40-x1.02).
  • x0.79->x30.1 - np.logical_and / np.logical_or / np.logical_xor / np.logical_not in one pass (72 cells, geomean x3.04).
  • x1.84->x10.6 - np.evaluate trees whose dtypes share one element size vectorize (Where(int64 >= 0, float64, 0) x0.74 -> x4.73).
  • x0.88->x9.91 - np.indices of a dtype other than int64 (x7.01 at (1000, 1000)) and np.mgrid / np.ogrid of an imaginary step (x9.91).
  • x0.34->x2.40 - the sorts take their per-call scratch from the buffer pool (np.argsort int32 x1.73 -> x2.40).
  • x1.33->x4.34 - np.correlate / np.convolve of short and view operands (np.correlate of 3 by 8 values was x0.57).
  • x2.48->x7.0 - text -> float64 casts take Clinger's exact fast path.
  • x0.97->x1.92 - ndarray.byteswap(inplace) of a strided view (int32 was x0.17).
  • x0.88->x2.33 - pythonnet StringDType copies; every small pythonnet import runs 2.45x faster than before.
  • x1.07->x1.32 - np.dot of an N-D operand without the OpenBLAS backend (int64 was x0.026).

🎯 Parity & Fixes

  • np.max / np.min / np.ptp, flat and along an axis, run NumPy's own reduction schedules - the sign of a ±0 tie and a canonical-vs-payload NaN now match on every non-broadcast layout.
  • np.nanmax / np.nanmin are NumPy's fmax.reduce / fmin.reduce bit for bit: complex128 skips NaN (it propagated it), float16 on non-C layouts read the wrong elements, and an all-NaN float16 slice returns its first NaN.
  • complex128 arithmetic bit-exact with NumPy - divide (1 ULP off on ~33% of operands), multiply (NumPy's fused simd_cmul), power with an integer exponent (up to ~1e14 ULP off) and abs (NumPy's fused simd_cabsolute; 35.5% of values were off).
  • The quantile family - percentile / quantile / median are NumPy's two-branch _lerp in each dtype's arithmetic (median the mean of the middle pair, a view at a non-zero offset read from its own start), the arguments are checked in NumPy's order, q follows NumPy 2.4.2 (an int q picks the element, a 2-D q, arithmetic in q's dtype), the nan forms return NumPy's axis order and positive NaN, complex median of data holding a NaN returns the element NumPy's complex order puts last (_median_nancheck), and uint64 rounds once on .NET 8.
  • np.round / np.around with decimals != 0 port PyArray_Round - negative decimals threw, complex values stayed unrounded, float32 rounded in double, and the result dtypes follow NumPy.
  • sum / prod of a size-0 or size-1 array widen the accumulator by NEP 50 like any other size.
  • np.array_equal(a, a) with a NaN in a returned True; NumPy returns False unless equal_nan.
  • int64-vs-uint64 comparisons are exact on every path (they went through float64).
  • float16 mod / floor_divide / fmod by zero give ±inf like NumPy (they gave NaN), and float16 add / subtract / multiply / divide on the out= / where= / strided paths compute in float32 like NumPy's HALF loops (they double-rounded through double).
  • float16 signalling NaNs stay signalling across every float16 cast route, uint64 at or above 2^63 converts to float64/float32 with one rounding on .NET 8 (it rounded twice, inside mixed uint64/float arithmetic and comparisons too), and maximum / minimum of float16 with float32/float64 keep the NaN's bits.
  • np.linalg.eig / eigvals of a float32 matrix with complex eigenvalues return NumPy's complex64 values (they kept double precision).
  • Use-after-free fixed: np.real / np.imag / .real / .imag, view(dtype) and getfield did not keep their owner's buffer alive, so after the owner was disposed (as inside eigvals) the next allocation overwrote them; resize(refcheck: true) now sees such views.
  • np.convolve / np.correlate without the backend are byte-exact on float32 positions under 32 terms and complex128 positions under 8 (NumPy's scalar sdot / zdotu regimes), and complex products with inf/NaN follow NumPy (convolve([1+0j], [inf+0j]) is nan+nanj).
  • np.arctan2(complex) corrupted the heap and crashed the process; it raises NumPy's TypeError.
  • np.dtype(str) follows NumPy's parser where it differed: 'cdouble' and the intp letters 'n' / 'N' parse, a single control character is a type number ('\x0c' is float64), only C whitespace counts inside a sized code ('f\xa08' was accepted), and error texts quote the string like Python's repr (data type "a'b" not understood).
  • np.min_scalar_type of an array returned null; it answers the array's dtype, and a 0-d array by its value. np.common_type() with no array is float16, np.mintypecode raises NumPy's ValueError for a code outside its size table, np.result_type of one ulong past int64 is uint64, and None arguments to can_cast / promote_types raise NumPy's TypeErrors.
  • A race could make the first np.can_cast(float64, int32, 'no') of a process answer True: the cached cast safety was read in two loads.
  • np.dtype(null) is float64, NumPy's np.dtype(None) (it threw ArgumentNullException), and a null np.result_type operand is NumPy's None, a float64 operand (np.result_type((DType)null, np.int8) is float64).
  • np.find_common_type is NumPy 1.26.4's, the last NumPy that has it: each list's type is the first of ?bBhHiIlLqQefdgFDGO that every entry casts to safely, and a scalar type of a higher kind goes through _find_common_coerce. find_common_type([uint8], [int16]) is int16 (it was uint8), four or more types are all counted ([int8, int8, float64, int8] was int8), a decimal is no longer searched as int64, and the NDArray form no longer throws OverflowException for a uint16 array with an int16 0-d array.
  • np.asanyarray of mixed scalars promotes in element order like NumPy ([int8, uint8, float16] is float32; it was float16), and a float16 among other types converts (it threw InvalidCastException).
  • Complex input to cbrt, floor, ceil, trunc, deg2rad, rad2deg, floor_divide and mod raises NumPy's TypeError (it raised NotSupportedException, and a zero-size complex input returned an empty array); IncorrectTypeException now derives from TypeError.
  • np.modf accepts int, float16 and bool input like NumPy (it rejected them), float16 byte-exact.
  • np.trace of uint8/uint16/char accumulates in uint64 like NumPy; ndarray.flat of a 0-d array has shape (1,); np.take, ndarray.item() and len() of a 0-d array raise NumPy's verbatim texts.
  • np.argsort of an empty array with sort axis >= 2 overran the heap.
  • A boolean-mask selection owns its result (flags.owndata), like NumPy.
  • ! (logical not) on a non-contiguous array returned wrong values.
  • Memory leaks fixed in np.choose, tofile, tobytes, ogrid, poly1d, polyfit, multivariate_normal, apply_along_axis / apply_over_axes, flatiter slice/fancy access, cross-dtype SetData and the float16 sum / mean temporaries; NDArray.ReplaceData could free another array's buffer.
  • Generator parity (it shipped in 0.70.0).
    • integers with a high past the dtype returned constant data (integers(0, 257, dtype: np.uint8) was all zeros); it raises NumPy's error.
    • Concurrent draws on one bit generator tore its state (72-99% of 4-thread values belonged to no seed's stream); draws hold the generator's lock, as in NumPy.
    • choice without a size returns one element (it returned shape (1,)); permuted follows memory order and casts into out= safely; a -0.0 scale, shape or range is rejected; a () size keeps float32.
  • Legacy RandomState parity.
    • An unseeded RandomState() took its seed from Environment.TickCount, so instances made in the same millisecond were identical; it uses OS entropy.
    • choice ignored replace=False and searched the probability CDF from the wrong side; randint(dtype: bool) crashed.
    • Every sampler is NumPy's legacy-distributions.c line for line: gamma(shape<1), binomial, negative_binomial, poisson, f, pareto, power, standard_cauchy, multinomial, hypergeometric, zipf, geometric and vonmises used other algorithms.
  • SeedSequence - a typed null seed array seeded the empty sequence (the same stream on every run; now fresh entropy, NumPy's None), and SeedSequence(new[] { -1 }) silently seeded 0xFFFFFFFF instead of raising.
  • result_type of four or more operands dropped every other pair (result_type(int8, int8, float64, int8) was int8; it is float64).
  • np.evaluate - flat and axis Sum / Mean / Prod / Min / Max follow NumPy's own schedules bit for bit (they matched the value only), a flat Mean of an integer child is NumPy's buffered float64 sum, NEP 50 typing gaps are closed, two closures over one lambda or two literals that print alike (two NaN payloads; 2^-25 on .NET 10) no longer share a kernel, and results take NumPy's layout.
  • .NET 8: string slicing (nd["1:3"]) re-parsed its pattern on every call because the static Regex cache thrashed - 9-18 µs -> 0.6-2.8 µs per parse.
  • NDArray.Scalar stalled for seconds after a multi-threaded burst: the scalar pool's trim removed each freed slot with a linear search under its lock (7-12 s with a few hundred thousand slots).
  • np.clip is NumPy's _clip and its ufunc call: an integer bound outside the dtype no longer wraps (np.clip(uint8, 0, 256) returned all 0 and np.clip(int8, -1000, 1000) all -24; a Python-int bound at or past the dtype's limit is dropped, and a kept one that does not fit raises OverflowError), min= / max= / where= exist with fromnumeric's keyword rule, the bounds, out and where broadcast together (a bound larger than the array, a larger out), an out that overlaps an input is computed as if copied (it gave wrong values), the const float loop keeps NumPy's signed zeros (np.clip(-0.0, 0, 1) is -0.0) and a NaN bound its own bits, and a given out is returned itself; the 4,170-case clip_pyint and 2,302-case clip_ufunc oracle tiers.
  • A ufunc with where= and an out that overlaps an input no longer writes heap bytes into the masked-off positions (np.add(x[:-1], 1, out=x[1:], where=m)): the masked out is read-write as in NumPy, on every out= / where= route and in np.evaluate.
  • An array allocated from a view's Shape is laid out fresh, as NumPy's np.empty(shape) is: new NDArray(dtype, shape), np.empty / np.zeros / np.ones / np.full and every np.random size: kept a stepped, offset or broadcast view's strides over a buffer of size elements (default_rng(42).integers(0, 100, arange(20)["::4"].Shape) returned heap values and np.polyval(p, a["::4"]) crashed the CLR), and a transposed view's Shape put a sampler's stream in column order.
  • new NDArray(Array | IArraySlice, Shape, order) is NumPy's np.ndarray(shape, buffer=, order=) and reads only the shape's dimensions: a view's Shape read past the caller's buffer (stepped), backwards (reversed), in F order (transposed) or as a read-only repeated row (broadcast); 'A' / 'K' and lowercase orders are accepted, and an invalid order raises NumPy's ValueError.
  • A copy or cast of a stepped one- or two-byte view reads no element past the view: np.zeros(163840, np.uint8)["1::2"].copy() ended the process with an access violation, because seven SIMD deinterleave loops loaded one element beyond a view that ends at its buffer's last element.
  • NDArray<T>.reshape(Shape) reads only its argument's dimensions, as the untyped reshape does: the Shape of a view, a transposed or a broadcast array put that array's strides over this array's buffer, reading and writing outside it.
  • np.append(a, (object)v) and the six object-value np.insert overloads no longer dispose the caller's NDArray (its buffer went back to the pool, and a second call returned wrong values), and np.choose releases its cast choices and its out= temporary instead of leaving them to the finalizer (a 1K choose 3.33 -> 1.63 us).
  • np.add / np.multiply (+, *, np.evaluate) of two float NaNs return NumPy's NaN: the second operand's on every win-amd64 loop path and the broadcast operand's in its AVX2 scalar-broadcast blocks (NumSharp returned the first operand's); the 2,678-case nan_order tier compares NaN payloads raw.
  • float16 np.max / np.min / np.nanmax / np.nanmin / np.ptp return NumPy's exact bits on every layout, flat and per axis: the first NaN verbatim (payload, sign, signalling) and the first ±0 in NumPy's visiting order (a later NaN replaced an earlier one, and a strided flat walk returned .NET's canonical NaN); the raw-byte 5,900-case half_minmax tier.
  • np.sort writes NumPy's positive quiet NaN into a float NaN tail (it wrote .NET's negative NaN), the stable sort keeps NaN bits and the float sorts keep ±0 in input order; np.unique reads an offset view from its own start (it returned the parent's rows), writes NumPy's NaN and keeps NumPy's order among equal values; np.isin follows NumPy's three paths; np.searchsorted checks its sorter and reads offset views.
  • np.sum / np.mean start from the add identity, as NumPy's add.reduce does: a sum of only -0.0 is +0.0 on every route (one element, a length-1 axis; np.mean, np.average, np.nansum, np.nanmean, np.trapezoid, einsum's sums and np.evaluate too). The flat float sums and means are NumPy's add.reduce bit for bit (NpyIter's call schedule with np.getbufsize() fills and a pairwise sum per call; 106 of 268 benchmark results had other bits than NumPy's), a dtype= loop casts every element first (float -> int truncated the total, complex -> real crashed the process), a complex mean divides as NumPy's complex division does, an empty mean is NaN in the mean's dtype, and the nan reductions read a strided, reversed or transposed view's own elements; the zero-excuse 23,953-case sum_identity tier.
  • The statistics reductions are NumPy's Python functions statement for statement, bit-exact over zero-excuse oracle tiers -
    • np.var / np.std (_var / _std) - ddof= and dtype= work at every size: 8,284 of 22,110 probed cases were grossly wrong, and 936 empty dtype= calls wrote past the result buffer; the 16,108-case varstd tier, geomean x3.07.
    • np.nanvar / np.nanstd - a 1-D axis gives a 0-d result (it was (1,)), keepdims holds for every input, a ddof at or past the non-NaN count and an infinite element give NaN (they gave 0), float16 / float32 sum in their own dtype, a complex input is x * conj(x), and dtype= exists; the 13,382-case nanvarstd tier, geomean x3.71.
    • np.mean (_mean) - every dtype= loop casts each element first (the axis loops summed doubles), a complex sum is divided as NumPy's complex loop divides, float16 sums in float32, integer and bool loops wrap, an empty integer input no longer throws, and a flat decimal mean is decimal; the 6,550-case mean_loops tier, geomean x2.62 (a bool mean along axis 0 at 10M x0.08 -> x28).
    • np.nanmean - keepdims holds for integer inputs, float16 / float32 sum in their own dtype (float16 can overflow to inf), a complex sum divides as NumPy's complex loop does, and dtype= exists; the 5,017-case nanmean tier, geomean x5.82.
  • np.all / np.any with out= or where= run NumPy's reduce prologue before any work: a non-bool where= mask raises NumPy's safe-cast TypeError (it was read by truth value), a read-only out is reported first, then the mask, the axis and out's rank, and the reduction iterator's shape checks use NumPy's texts (a mask may not stretch the input or add axes); the reduce_kwargs tier starts here with 4,752 cases.
  • np.copyto is NumPy's array_copyto: an overlapping masked copy no longer reads values it already wrote (251 of 1,500 overlap probes gave silent wrong values), an overlapping slice assignment walks NumPy's raw order (a[3:19:2] = a[6:14]), a mask or a source that does not broadcast is refused, a source with leading unit axes and a self-copy of a read-only array are accepted, and the checks run in NumPy's order with its texts; the 13,462-case copyto_full tier.
  • np.power's float loops take NumPy's scalar-exponent fast path (1/x, 1, sqrt(x), x, x*x for -1, 0, 0.5, 1, 2) exactly where NumPy's calls see a zero exponent step: the out= / where= routes called pow for a 0-d exponent, (1,) ** (1,) took the fast path where NumPy calls pow, a column exponent never took it, and float16 power is powf with no fast path (7,161 of 1,114,112 values differed); the 7,902-case power_fast tier, fast-path cells geomean x6.
  • Two F-ordered operands where one broadcasts give the right values: np.add / subtract / multiply / true_divide, maximum / minimum / fmax / fmin, the bitwise functions and the C# operators paired the elements wrongly for every 3-D or 4-D pair with a broadcast axis (each SIMD dtype under 8,192 elements, float16 at any size: 824 of a 2,814-case probe); 3,074 new pp_fbcast_* cases in the binary oracle tiers.
  • np.random returns NumPy's kind of array: Generator.choice without replacement, RandomState.choice without replacement but with p, and multivariate_normal own their data (they were reshape views), Generator.permutation(x, axis > 0) is NumPy's fancy-index view (F-contiguous for 2-D), and the transpose of an empty array is a view; choice(1_000_000, 100_000, replace: false) x2.03 -> x5.04.
  • NpzFile is safe to share between threads: concurrent first loads of its members decoded each other's bytes; its ZipArchive reads run under one gate, with lock-free cache hits.
  • NDW012, the dropped-NDArray warning of the analyzer the NumSharp package ships, reads as a fix procedure in one short paragraph (611 characters, was 3,034): not a leak -> suppress it with a reason; every call already runs under a scope -> [NDScopedCovered]; an entry point that keeps no array past the call -> [NDScoped] / [NDScopedAsync]; otherwise dispose it. It no longer claims an array is "never scoped" (a caller's scope reclaims it), and where the build declares that NumSharp.Build is not weaving it never offers [NDScoped].
  • The analyzer and the NumSharp.Build weaver agree on which interface member a method implements (the CLR's interface map): the analyzer missed every override below a base that lists an interface without declaring the slot, and the weaver wove a public virtual beside an explicit implementation of the same signature (its scope disposed the arrays that method keeps); a class over a k-deep generic interface chain cost the weaver 2^k searches per method (56 s at 26 levels), a type of 50,000 auto-properties 12 s (now 1 s), and NDW012 costs a project that never names NDArray one memoized lookup per operation.
  • Build cost - the three analyzers share one fact set per compilation and skip every body and type that cannot involve an NDArray (the holder analysis of a 2,500-file project that never names NDArray 13 s -> under 0.01 s, NDW012 over NumSharp's own 1,016 files 3.5 s -> 1.5 s), and the NumSharp.Build weaver reads property and event accessors in linear time instead of Cecil's quadratic scan (a 50,000-property type with a scoped method 8.2 s -> 1.8 s).
  • C# literals are NumPy's weak Python scalars (NEP 50): a float32 array compares with 0.1 in float32, an out-of-range integer literal raises OverflowError in a binary ufunc and in every store (it wrapped), and only a literal's 0-d array is weak (an element read or np.array(x) promotes as a NumPy scalar).
  • Converts speaks culture-invariant NumPy text (under de-DE "1.5" read as 15), and every internal promotion is NumPy's order-independent result_type.
  • Indexing is NumPy's prepare_index and array_assign_subscript: a float entry is refused (it was rounded), a Python bool is a 0-d mask, a 0-d integer array a view index, the advanced block lands where NumPy puts it, errors come in NumPy's order, a store converts its value with the array's own descriptor (a list item by item), None stores NumPy's value, and the key array is never written.
  • View assignment and np.insert follow PyArray_AssignArray and NumPy's shifted positions; a zero slice step raises when used; the element accessors check every coordinate and convert another element type with NumPy's cast (in Release they read and wrote past the buffer).
  • Axis arguments are NumPy's normalize_axis_index (34 functions raised other exceptions, eight wrapped -ndim - 1, a negative axis of a 0-d input never returned).
  • np.put / np.take are PyArray_PutTo / PyArray_TakeFrom (a values view read slots the put had written), np.argwhere / np.flatnonzero / np.unravel_index return NumPy's views, and np.place converts its values as a Python list.
  • np.nditer - NumPy's descriptors end to end, its buffered iteration ported (a buffered reduction lost partial sums with a short buffer), its checks in NumPy's order, element views that do not own their data, and NumPy's store in the setter.
  • Reductions with dtype= and out= are NumPy's loops: np.cumsum / np.cumprod / np.prod cast the input first (a float16 or complex128 dtype= crashed or read past the buffer), an out= reduces in promote_types(out, a), np.cumsum / np.cumprod store into every out= NumPy's accumulate accepts (more trailing axes, an extent-1 input against a longer out, an empty input or out returned untouched), dtype=char runs uint16's loop and dtype=bool the logical one, a reversed or broadcast axis adds in NumPy's order, and a 0-d result is NumPy's read-only scalar.
  • np.nansum / np.nanprod are np.sum / np.prod over _replace_nan (float16 was several ULP off) and no longer read freed memory or divide by zero over an empty axis; complex np.prod / np.cumprod and float16 np.prod multiply in NumPy's forms.
  • np.dot / np.vdot / np.vecdot compute with NumPy's dot functions (models of scipy-openblas' sdot / ddot / zdotu / zdotc, NaN priority included): an N-D product read every element as a double (a complex one lost its imaginary parts) and an np.split child from its parent's start; the matmul family checks calls, casts and out= in NumPy's order and returns NumPy's output object; float16 products are NumPy's HALF loops.
  • np.correlate / np.convolve compute complex128 positions with NumPy's zdotu and read mode with NumPy's converter.
  • np.linalg runs NumPy's statements in norm / vector_norm / matrix_norm, cond, tensorsolve, tensorinv, cross, the hermitian=True forms, qr's modes, matrix_power, solve, multi_dot and the scalars of det / slogdet / lstsq, and answers empty operands without the OpenBLAS backend.
  • Ufuncs - a fresh result takes NumPy's K-order layout, out= / where= are checked before the loop with NumPy's shape errors, an empty operand gives NumPy's empty result, NDArray.negative() wraps unsigned values, unary + of bool raises, and char reads as uint16.
  • NumPy's bits in more ufuncs: np.fmax / np.fmin at NumPy's SIMD and C-library positions, float16 arctan2 / log1p / expm1, float64 hypot, models of the UCRT's log1p / expm1 / logaddexp, complex maximum / fmax ties and the two-bound complex np.clip.
  • Creation - np.arange is PyArray_ArangeObj (a float ramp in the dtype's own arithmetic, exact long bounds), np.linspace takes NumPy's edge cases and text dtypes, np.full / np.eye / *_like / np.indices / np.mgrid / np.ogrid / np.r_ / np.c_ / np.ix_ follow NumPy's arithmetic and checks, a C# object list or jagged array reads as a Python list (a ragged one raises), and np.frombuffer is PyArray_FromBuffer.
  • np.pad, np.delete, np.select, np.where, np.choose, np.compress, np.fill_diagonal and the broadcast family check and convert as NumPy does, with its texts (np.where read an offset view from its buffer's start; np.broadcast_arrays made a read-only operand writeable).
  • np.average is NumPy's statements (2,998 of 3,000 random cases byte-identical, 1,482 before; an empty axis crashed the process), and np.cov / np.corrcoef, np.einsum and the legacy np.poly* functions answer text, integer dtype=, out= and N-D operands as NumPy does.
  • Text I/O - np.fromstring / np.fromfile are NumPy's C scanners (630 of 2,000 random cases differed), np.loadtxt reads Python's float() and NumPy's DataSource, np.savetxt / ndarray.tofile format with CPython's %, files open with Python's share mode, and a binary np.fromfile past 2 GB reads.
  • Printing - the array printer is a linear-time walk (it overflowed the stack from about 2,000 dimensions), np.format_float_positional / np.format_float_scientific are NumPy's dragon4.c, a float64 prints NumPy's digits on .NET 10, and a text repr escapes as CPython 3.12 does.
  • Memory safety - an operand stays alive until the call that reads it returns (28 entry points; the NumSharp.Build weaver roots them), and tolist() / AsStringArray of a temporary, np.unique past Array.MaxLength elements and a float16 element at the end of mapped memory on .NET 8 no longer read freed or unmapped memory.
  • NumSharp.Interop.pythonnet - a view of a numpy buffer counts toward NumSharp's memory accounting until its last view is released, instead of one GC.AddMemoryPressure per view: 5,000 views disposed as they go ran 8-15 full collections, now none.
  • From the parity QA - (bool) of a one-element array is NumPy's truth test, a leading boolean mask over trailing extents of 1 selects rows, np.isclose takes an infinite tolerance, view / getfield refuse a non-native descriptor, np.interp's complex NaN is nan+0j, np.nan_to_num refuses a text fill, ndarray.astype checks its order and casting first, np.mintypecode reads arrays, and exception texts and the np.finfo / np.iinfo reprs read the same under every culture, NumPy's reprs byte for byte (a number in a message took the culture's minus sign or decimal comma, finfo printed min=-3.402823E+038 and uint64's iinfo max long.MaxValue).
  • np.einsum / np.nested_iters check their arguments in NumPy's order: np.nested_iters converts flags / order / casting before it reads axes or the operands, and np.einsum refuses an out= of the wrong extent before any cast check and plans an explicit path first (an incomplete one raises NumPy's RuntimeError: Invalid einsum_path is specified; it was contracted anyway).
  • np.polyval takes its first product in x's dtype, as NumPy's zeros_like(x) start does: np.polyval([1+1j], [inf]) is nan+1j (was nan+nanj) and np.polyval([-0.0], int [-3]) is +0.0 (was -0.0).

🧰 Testing & Tooling

  • Differential-fuzz oracle 117,917 -> 836,054 corpus cases: new tiers for every numpy.polynomial unit (179,118 cases), np.ma (109,961), np.evaluate (29,893), np.emath, ndarray instance members, and complex128 / Decimal coverage, with a dtype-spread gate (at least 4 dtypes per op) and an applicability gate.
  • The array-object, relayout and surface tiers - 9,677 NumPy 2.4.2 cases gate the object an API returns (a view, a copy or the operand itself, out= identity, the C / F / OWNDATA / WRITEABLE / ALIGNED flags, byte strides, every C# form of a shape argument), 16,862 replay every corpus cell in the layouts it never saw (F, permuted, stepped and reversed views at an offset, broadcast, 0-d, empty), and 25,729 + 27,240 gate the members, attributes and numpy.ma functions the operand corpus cannot express; every gap they found is carved under an id.
  • The random-API oracle replays every public np.random overload against NumPy 2.4.2 by its exact C# signature (493 members, 5 engines x 10 seeds, the full post-call state), with coverage gates and a nightly soak under 10 fresh seeds.
  • The coverage join credits each API from the oracle contracts that name it, strictly: a new op key or corpus file that resolves to nothing fails the docs build.
  • The coverage inventory lists every numpy.ufunc method (reduce, accumulate, reduceat, outer, at, resolve_dtypes, read from the type) for every ufunc, including those of numpy.strings and numpy.char, and the methods numpy.ma's masked ufunc wrappers implement: 323 more API contracts.
  • The coverage inventory credits the numpy.ma.MaskedArray and numpy.vectorize members to their NumSharp owners NDMaskedArray and Vectorized (79 rows had read missing).
  • The coverage inventory catalogs every module NumPy lists as public and every name its documentation indexes: numpy.dtypes, numpy.exceptions, numpy.typing, numpy.version and numpy.ma.mrecords had no rows, and neither had the constants and classes of the scanned modules (numpy.ma.masked, the polynomial domain constants), so 66 NumSharp APIs read as NumSharp-only. Two CI guards now hold the catalog to NumPy's own public-module list and documentation index.
  • The coverage inventory detects every public NumPy API but numpy.testing and numpy.lib: the 1,793 Python protocols NumPy's classes define (a + b, a[i], casts, iteration, with, pickling; 158 provided, by members of the protocol's name, C# operators, indexers and casts, or reviewed adapters such as ToString() for str()), the 923 members of NumPy's scalar types and the members of the remaining classes, with numpy.ctypeslib, numpy.__array_namespace_info__ and numpy.lib.NumpyVersion now shown. NumPy 1.x compatibility APIs NumPy itself deprecated (numpy.matlib, numpy.typing.NBitBase) are left out, and two CI guards resolve every row on NumPy 2.4.2 and fail on any API NumPy deprecates.
  • The numpy.dtype / dtype-class oracle replays 9,566 / 8,526 NumPy 2.4.2 records (Windows / Linux NumPy, one file per C long model): the member names (the C slots included), every attribute of 1,483 / 1,351 descriptors, comparisons, promotions with each result's class, the dtype classes (each model's C-type classes included), 2,993 / 2,439 casting resolutions and 2,012 dtype strings through np.dtype(str) - comma strings included, whose errors compare type and text strictly - and holds the C enums NumSharp mirrors (NPY_TYPES, NPY_TYPECHAR, ...) to the members, values and order of NumPy's header.
  • np.find_common_type replays 11,291 NumPy 1.26.4 cases, and np.asanyarray of mixed scalars 2,917 NumPy 2.4.2 orderings.
  • NumSharp.Tests runs in parallel, one test per worker: 108 s -> 22-27 s on net10.0 and 117 s -> 22-28 s on net8.0 (Release, a 32-thread desktop; 48 s on 4 CPUs); tests that touch process-wide state are [DoNotParallelize], and VSTest's result batch is 1000 (NumSharp.Tests.runsettings).
  • The string oracle - 281,297 NumPy 2.4.2 cases in 66 sections over S / U / StringDType, strict (no case passes as a deferral), with a randomized soak and mutation passes over the text families.
  • benchmark/strings - the sixth subsystem of run_benchmark.py: 442 cells over the text dtypes at 1K / 100K / 1M, geomean x6.61, min x1.44.
  • The text I/O oracle replays 54,741 Linux NumPy 2.4.2 cases on every host.
  • The coverage inventory compares every credited API's C# overloads with NumPy's signature and joins the API diff gap map onto its rows.
  • tools/facade-move/snapshot.cs records NumSharp's own outcome for every overload of the named functions over 18 dtypes x 13 layouts, so a refactor can be shown to change nothing; two ratchets keep dtype logic out of the public facades.
  • Analyzer rule NDW020 keeps exception texts culture-independent: a number, object or type parameter formatted without a format provider inside an exception message (an interpolation hole, a + operand, ToString(), string.Format / Join, a StringBuilder append, an ArgumentOutOfRangeException actual value, and the locals, : base(...) calls and helpers a message reads) is a build error in NumSharp.Core whose text lists the fixes in order; it ships in the NumSharp package off by default (dotnet_diagnostic.NDW020.severity = error in an .editorconfig turns it on).
  • Build warning NDW021 - a project that references NumSharp with PublishAot=true (the default of a dotnet run app.cs file-based app) is told at build time that NumSharp generates its kernels at run time and does not support Native AOT, with the one-line fix (#:property PublishAot=false), instead of building clean and throwing PlatformNotSupportedException at the first array arithmetic; NumSharpDisableAotWarning=true silences it.
  • Packages publish to nuget.org through Trusted Publishing: the release workflow trades its GitHub OIDC token for a one-hour API key (NuGet/login), so no long-lived API key is stored.

💥 Breaking Changes

  • DType replaces System.Type as the dtype currency.
    • NDArray.dtype returns DType (was Type), and np.float64, np.int32 and the rest are DType descriptors (were Type); DType -> Type is explicit (a.dtype.type), and a.dtype.Name is a.dtype.name.
    • Each dtype parameter takes one DType: the Type / NPTypeCode twins are gone (a Type, NPTypeCode or dtype string still converts implicitly), and linspace / nancumsum's typeCode: is dtype:.
    • The promotion family (result_type, promote_types, find_common_type, common_type, ...) returns DType, and isdtype rejects issubdtype's vocabulary ("floating"), as NumPy does.
  • finfo.dtype / iinfo.dtype return DType (were NPTypeCode), and the pythonnet dtype maps take DType.
  • np.issubdtype's string argument is a dtype string, as in NumPy: the category names are gone (np.issubdtype(x, "integer") raises data type 'integer' not understood, and np.issubdtype(np.float32, "float") is False because "float" is float64); pass the class, np.issubdtype(x, np.integer).
  • Casting-rule strings are case-sensitive like NumPy ("Safe" raises ValueError).
  • A byte-order prefix before a dtype NAME is refused, as in NumPy, which looks names up by the whole string: np.dtype("<float64"), np.dtype("|bool") and NumSharp's own "<Int32" raise data type '<float64' not understood (they were accepted). Before a code the prefix still works ("<f8", ">i4").
  • On Windows, np.int32 / np.int64 report the local NumPy's LLP64 type numbers and chars (7 / 'l' and 9 / 'q'; they were hard-coded to Linux's 5 / 'i' and 7 / 'l').
  • NPY_TYPECHAR is NumPy 2.4.2's: NPY_STRINGLTR2 is NPY_DEPRECATED_STRINGLTR2, NPY_INTPLTR / NPY_UINTPLTR are gone (cast the character, (NPY_TYPECHAR)'n'), and NPY_VSTRINGLTR ('T') is new. NPTypeCode.ToTypeCode(NPY_TYPECHAR) reads a character as np.dtype does: 'D' is complex128 (it was float64), 'l' is int32 on Windows (it was int64), and 'F', 'S', 'U', 'V', 'c' and the kind letter 'u' are refused (they read as float32, char, char, Empty, complex and uint64). ToTYPECHAR gives the dtype's own character ('D' for complex128, it was 'c'; 'q' for int64 on Windows) and refuses decimal and char (they gave 'q' and 'c'), and their dtype.char is 'A' / 'u', characters of their own that np.dtype refuses as NumPy refuses a user dtype's (they were int64's 'q' and S1's 'c', so np.dtype(d.char) built another dtype).
  • A write to a read-only array raises NumPy's ValueError (was NumSharpException); it derives from ArgumentException, so catch (NumSharpException) no longer catches it.
  • np.maximum / np.minimum / np.fmax / np.fmin take out as the third positional argument (NumPy's order), not dtype; pass dtype: by name.
  • np.logical_and / logical_or / logical_xor / logical_not return NDArray (was NDArray<bool>), and np.modf's tuple field Intergral is Integral.
  • ndarray.shape = ... raises NumPy's AttributeError when the layout cannot be reshaped in place (it silently copied); use reshape.
  • np.nanmax / np.nanmin of an empty reduction raise NumPy's "zero-size array to reduction operation fmax which has no identity" (they returned a 0-d NaN).
  • new MT19937(42) seeds like NumPy's np.random.MT19937(42) (through SeedSequence), not like RandomState(42); mt._legacy_seeding(42) is the old stream. MT19937's non-NumPy members (NextUInt32, NextDouble, NextInt, NextLong, Next, NextBytes, Seed, SeedByArray, SetState, Key, Pos, Clone) are gone, and Pcg64StateData is PCG64.State.
  • Seeded legacy RandomState streams of gamma(shape<1), binomial, negative_binomial, poisson, f, pareto, power, standard_cauchy, multinomial, hypergeometric, zipf, geometric, vonmises (and gumbel / logistic / rayleigh at scale 0) change to NumPy's values; binomial(int n) is binomial(long n); inputs NumPy accepts (dirichlet([]), a NaN alpha, negative_binomial(p: 0)) no longer throw.
  • Legacy integers are NumPy's LP64 C long: randint defaults to int64 (was int32), random_integers / permutation / choice / multinomial return int64, NumPyRandom.Seed is a uint (was int), and SeedSequence.generate_state returns an NDArray (was Array).
  • NumPy's parameter names: Generator(bit_generator) (was bitGenerator), PCG64(seed) and the other bit generators (was seedSeq, now any ISeedSequence), and default_rng(seed) (was bitGenerator / generator) - named-argument calls must be renamed.
  • The non-NumPy NumPyRandom.uniform(NDArray low, NDArray high, DType dtype) is removed; two-array calls bind NumPy's broadcasting overload (float64; .astype() the result for another dtype).
  • np.asmatrix / np.bmat return NDMatrix (was a plain 2-D NDArray): * is the matrix product, an index keeps two axes, and np.* functions return NumPy's matrix results for it (np.sum(m, 0) has shape (1, N)); take .A for the plain array.
  • An index-array assignment follows NumPy's value rule: a value that does not fit raises ValueError "shape mismatch: value array of shape (1,4) could not be broadcast to indexing result of shape (2,)" before anything is written (was IncorrectShapeException), and a (1, N) value now fits an (N,) selection.
  • A boolean-mask assignment takes a 0-d or 1-d value: a 2-d value raises NumPy's TypeError, and a 1-d value whose length is neither 1 nor the number of true elements raises NumPy's ValueError.
  • Shape is immutable and the shape accessors return copies, as NumPy's tuples are: the Shape indexer has no setter (s[i] = v no longer compiles; build a new Shape), a.shape, Shape.Dimensions and Shape.Strides return a copy on every read, and new Shape(long[]), (Shape)long[] and every public method taking a long[] shape copy the caller's array. A write through any of them changed an array's dimensions or strides without its size or buffer, so later reads went past the buffer; a read in a hot loop now allocates (hoist it, or use shape[i] / NDim), and new params ReadOnlySpan<long> overloads of reshape / reshape_unsafe / resize keep inline calls at one allocation. Shape(long[] dims, char order = 'C', bool copy = true) and Shape(long[] dims, long[] strides, bool copy = true) replace the two-parameter constructors (recompile: the old signatures are gone), and copy: false, NumPy's keyword passed by name, keeps an array nothing will write into again instead of copying it.
  • np.ndarray(shape, dtype, buffer, offset, strides, order) is NumPy's ndarray.__new__: float64 and C order by default (they were float32 and 'F'), order is honored and validated without a buffer, the buffer is any bytes-like object whose bytes are read as dtype (its element type was the dtype), offset and strides count bytes, and the fourth positional argument is offset (a char there is a compile error: name order:). The wrap constructors read a larger buffer's prefix and raise TypeError for a smaller one (both raised ArgumentException), and flags.aligned is false for misaligned memory (np.frombuffer / getfield at odd offsets too), so setflags(align: true) can raise and np.require(..., 'A') can copy; 167 of 185 probe cases match NumPy (21 did), gated by a 5,919-case ndarray_ctor tier.
  • np.var / np.std / np.nanvar / np.nanstd / np.mean / np.nanmean return NumPy's dtypes, shapes and values for every loop: a 1-D-axis nanvar / nanstd is 0-d (it was (1,)), float16 / float32 inputs are summed in their own dtype (a float16 nanmean can overflow to inf), an integer or bool mean loop wraps, and a flat decimal mean is decimal (it was float64).
  • np.median into an integer or bool out= stores NumPy's value, cast unsafely as NumPy's mean(out=out) does ([1, 5, 9] for arange(12.).reshape(3, 4) along axis 1; a nonzero median is True), and np.ptp of a float input into an integer out raises subtract's TypeError: Cannot cast ufunc 'subtract' output from dtype('float64') to dtype('int64') with casting rule 'same_kind' with the maxima already in the out (both raised np.copyto's InvalidCastException); integer np.ptp wraps in its dtype and widens exactly (np.ptp(int8 [127, -128], out=int64) is 255; it was -1).
  • np.float_power(x, 2.0) is pow, no longer x*x; float16 np.power is powf (-0.0 ** 0.5 is +0.0 and -inf ** 0.5 is +inf, as in NumPy); and a one-element operand with more dimensions than the other gives the broadcast shape in every binary ufunc ((7,) + (1, 1) is (1, 7); it lost the leading axes).
  • On C# 12, the default language of a net8.0 project, np.random.seed(null) and new PCG64(null) no longer compile (CS0121; both compiled against 0.70.0): the overload that takes a null seed is chosen by [OverloadResolutionPriority], which only C# 13 and later honor. Call np.random.seed() / new PCG64(), or set <LangVersion>13</LangVersion>; net9.0 and later projects are not affected.
  • A C# string is NumPy's str: the implicit string -> NDArray conversion and np.array(string) give a 0-d U array (the text was parsed as numbers; parse with astype(np.float64)), a .npy <U1 loads as U1 (it loaded as Char), and np.@char is the numpy.char module (it still converts to the char dtype).
  • C# number literals are weak Python scalars: a float16 / float32 array compared with a double literal compares in its own precision, an out-of-range integer literal raises OverflowException in a binary ufunc and in every store (a[0] = 1000 on int8, np.full(3, -1, dtype: np.uint8), np.copyto(int8, 1000); they wrapped), a NaN literal into an integer array raises, a Complex one into a real array TypeError, 'equiv' refuses an integer into int32, and a 0-d array that is not a literal promotes as a NumPy scalar (uint8 + int64[1] is int64).
  • NumPy's exception types: AxisError for an out-of-range axis, IndexError from np.put / np.take / np.choose, the indexers and np.einsum_path (an ellipsis without its operand, an explicit path past the operands), TypeError for a ufunc cast a casting rule refuses, a dtype string NumPy rejects, a numeric cast of a non-0-d array, np.ediff1d's refused ends and a null stored into an integer array, ValueError from np.nditer's construction errors and np.nested_iters without an operand, a zero slice step and np.nonzero of a 0-d array (they were ArgumentException, IndexOutOfRangeException, ArgumentOutOfRangeException, NotSupportedException, InvalidOperationException or IncorrectShapeException); AxisError.ActualValue is the axis as invariant text ("-3"; AxisError.Axis keeps the number).
  • One NumPy signature: np.sum / np.prod / np.mean / np.var / np.std and the max / min family (and their ndarray methods) have one overload each in NumPy's order, so a positional third argument is dtype / out (name keepdims:); the generic np.amax<T> / a.max<T>() and NDArray.amax / amin are gone; np.evaluate(expr, x) passes x as an operand (name @out:).
  • Each array class has one indexer, NumPy's __getitem__ / __setitem__: NDArray's other indexers bind this[params object[]] with the same results, an int[] key to __getitem__ is an index array, and NDArray<T>'s indexer returns NDArray<T> (its typed scalar indexers are gone: (double)a[0], or a.GetValue<double>(0) in a hot loop).
  • TensorEngine subclasses implement the new abstract members that carry each computing function; np.sum / np.mean call ReduceAdd / ReduceMean; the LAPACK members (Cholesky ... Svd) are DefaultLinalgEngine's kernels under TensorEngine.Linalg; BackendFactory.GetEngine() returns TensorEngine.Default, results and views bind the engine that made them, and a null array operand raises ArgumentNullException when the call starts.
  • A fresh ufunc result takes NumPy's K-order layout (its strides and flags change for permuted, F-ordered and flipped operands), and np.fft keeps the input's memory order.
  • The print options and np.setbufsize are context-local under Python 3.12's ContextVar rules: a new thread starts at the defaults, Task.Run and await continuations see the caller's.
  • NDScope.Dispose on a thread other than the one that opened the scope throws InvalidOperationException in every build (only a Debug assert guarded it): a hand-written scope held across an await was reused on the continuation's thread, where its close freed the opening thread's later arrays. Close the scope before the await and open another after it, or mark the async method [NDScopedAsync]; a refused scope's arrays are left to the finalizer.
  • Text I/O follows NumPy's encodings and CPython's formatter: np.loadtxt(Stream) / np.savetxt(Stream) default to Latin-1 (was UTF-8), a named file no longer detects a BOM, encoding: takes Python's codec names, np.savetxt / ndarray.tofile raise CPython's errors where they were lenient, and np.fromstring / np.fromfile read text as NumPy does.
  • a[true] returns a copy (it was a view), and (bool) of a one-element array answers its truth value (it threw).
  • Creation: np.arange raises NumPy's errors (a zero step DivideByZeroException, an out-of-range bound OverflowException; it wrapped), np.asanyarray of a C# object list reads integers as int64 and floats as float64, np.where / np.select read C# literals as Python scalars (np.where(c, 7, 7) is int64), and np.frombuffer's range checks raise ValueError.
  • np.pad reads its widths before its mode (a null mode is ValueError; one int[] entry per axis is refused, write an int[,]), a value that does not fit a view assignment or np.insert raises ValueError (was IncorrectShapeException), and np.average / np.isin(assume_unique: true) answer as NumPy does.
  • np.complex64 / np.csingle are scalar classes (np.zeros(3, np.complex64) no longer compiles), and np.longdouble / np.clongdouble / np.intc / np.longlong and their unsigned twins are classes of their own (equal to their twin, not the same object).
  • np.sort / np.argsort validate kind and take order / stable, np.argsort's first parameter is a (was nd), and np.format_float_positional / np.format_float_scientific take object x and a string trim.
  • pythonnet: numpy's <U1 imports as a U1 view (was a Char copy), np.bytes_ as a 0-d S<n>, and FromNumpyDtypeStr / FromBufferFormat return DType.
  • np.polyder is NumPy's recursion: a 2-D p is differentiated along its first axis (np.polyder(np.ones((3, 2))) is [[2, 1], [2, 1]]; it raised), and a 0-d p raises TypeError: len() of unsized object at every order (m = 0 returned it).
  • np.isscalar answers as NumPy does: a Type is not a scalar (np.isscalar(typeof(int)) was True), and Half, BigInteger, Char8 and DateTime64 are (they were False).

Install via NuGet

dotnet add package NumSharp --version 0.80.0
dotnet add package NumSharp.Bitmap --version 0.80.0
dotnet add package NumSharp.Interop.MLNet --version 0.80.0
dotnet add package NumSharp.Interop.OnnxRuntime --version 0.80.0
dotnet add package NumSharp.Interop.OpenBLAS --version 0.80.0
dotnet add package NumSharp.Interop.System.Numerics.Tensors --version 0.80.0
dotnet add package NumSharp.Interop.pythonnet --version 0.80.0
dotnet add package NumSharp.Build --version 0.80.0

Packages

Package NuGet
NumSharp NuGet
NumSharp.Bitmap NuGet
NumSharp.Interop.MLNet NuGet
NumSharp.Interop.OnnxRuntime NuGet
NumSharp.Interop.OpenBLAS NuGet
NumSharp.Interop.System.Numerics.Tensors NuGet
NumSharp.Interop.pythonnet NuGet
NumSharp.Build NuGet