Namespace NumSharp.Utilities
Classes
- ArrayConvert
Presents all possible combinations of array conversion of types supported by numpy.
- Converts<T>
Provides various methods related to Convert based on give
T.
- DecimalMath
Provides transcendental math functions for decimal type. .NET does not provide these natively since decimal is designed for financial calculations.
- Hashset<T>
Implementation notes: This uses an array-based implementation similar to Dictionary<T>, using a buckets array to map hash values to the Slots array. Items in the Slots array that hash to the same value are chained together through the "next" indices.
This implementation supports long indexing for collections exceeding int.MaxValue elements.
The capacity is always prime; so during resizing, the capacity is chosen as the next prime greater than double the last capacity (or 33% growth for very large sets above 1 billion elements).
The underlying data structures are lazily initialized. Because of the observation that, in practice, hashtables tend to contain only a few elements, the initial capacity is set very small (3 elements) unless the ctor with a collection is used.
The +/- 1 modifications in methods that add, check for containment, etc allow us to distinguish a hash code of 0 from an uninitialized bucket. This saves us from having to reset each bucket to -1 when resizing. See Contains, for example.
Set methods such as UnionWith, IntersectWith, ExceptWith, and SymmetricExceptWith modify this set.
Some operations can perform faster if we can assume "other" contains unique elements according to this equality comparer. The only times this is efficient to check is if other is a hashset. Note that checking that it's a hashset alone doesn't suffice; we also have to check that the hashset is using the same equality comparer. If other has a different equality comparer, it will have unique elements according to its own equality comparer, but not necessarily according to ours. Therefore, to go these optimized routes we check that other is a hashset using the same equality comparer.
A HashSet with no elements has the properties of the empty set. (See IsSubset, etc. for special empty set checks.)
A couple of methods have a special case if other is this (e.g. SymmetricExceptWith). If we didn't have these checks, we could be iterating over the set and modifying at the same time.
- InfoOf
Static utility methods for type information.
- InfoOf<T>
Provides a cache for properties of
Tthat requires computation.
- NDComplexMath
Complex-math helpers backing NumSharp's complex (
complex128) unary ufuncs. Each entry point reproduces NumPy 2.4.2 bit-for-bit (or within 3 ULP on the finite interior), verified by a 504-point bit-exact sweep and a layout sweep (contiguous / F-contiguous / strided / transposed / reversed / sliced / broadcast / 0-d). Most are direct ports of NumPy's own routines innpy_math_complex.c.src(the FreeBSD msun implementations) becauseSystem.Numerics.Complexdiverges on large magnitudes, the unit circle, tiny/subnormal values, branch cuts, and signed zeros. Every non-finite input additionally matches NumPy's exact NaN SIGN bit-for-bit (win-amd64 = MSVC UCRT complex functions): the "produce a NaN" slots emit the positiveNPY_NAN;csqrt/clog/cnc_log1p/cexpPROPAGATE an input NaN's sign;csinh/ccoshCANONICALISE to +NaN (socsin/ccosfollow the transform's negate), whilectanhpropagates; and a genuine0/0 · inf-inf · inf*0keeps its x86-negative sign on both engines. Gated by the complex-unary NaN-contract oracle tier (raw-byte NaN compare).Ported NumPy algorithms. Log(Complex) =
npy_clog(four-regime rescale incl. the near-|z|=1log1ppath; drives Log10(Complex) and the engine'slog2); Sinh(Complex)/ Cosh(Complex) = textbooksinh/cosh(x)·trig(y)with ay==0guard (so a huge real part doesn't becomeinf·0 = NaN) and the C99 Annex G non-finite tables; Tanh(Complex) = Kahan'snpy_ctanh(markedly more accurate than the BCL near±π/2) + the|x|≥22overflow-safe branch; Sin(Complex)/Cos(Complex)/ Tan(Complex) route through those exactly as NumPy definescsin/ccos/ctan; Atan(Complex) = the fullnpy_catanh(realatanh/atanon the axes, thelog1pinterior, and an exponent-classifiedreal_part_reciprocal); Exp(Complex) =npy_cexp; Sqrt(Complex) =npy_csqrt; Expm1(Complex) =nc_expm1with a Goldberg realexpm1; Square(Complex) =vfmaddsubz·z(matches NumPy's SIMD complex multiply overflow/cancellation AND NaN sign); Reciprocal(Complex) = theCDOUBLE_reciprocalufunc loop (division-form imaginary term, NaN-sign correct); Exp2(Complex)/Log1p(Complex) compose the above; Abs(Complex) =npy_cabs(C99hypot: an infinite component yields+infeven alongside a NaN — the .NET 8Complex.Absreturns NaN there).Still delegating to the BCL (at parity): Asin(Complex) and Acos(Complex) use Asin(Complex)/Acos(Complex) on the finite interior with signed-zero / branch-cut fixups, and the C99 non-finite tables otherwise.
Accepted residuals (pathological FINITE inputs only, beyond 3 ULP — NaN sign is NOT among them, it is byte-exact):
arccoswith a sub-DBL_MINimaginary part flushes the denormal real part to 0 where NumPy'scacoshard-work kernel keeps it (~5.8e-309);sinh/cosh(and thesin/costhat route through them) at the|x|∈[710,710.13]overflow edge differ because Windows' CRTsinhoverflows where .NET's stays finite.Perf: each public entry point is a tiny finite-path wrapper marked AggressiveInlining so the JIT folds it into the IL-emitted unary kernel (no per-element call frame); the rare non-finite / special-value tables live in cold helpers marked AggressiveOptimization (kept out-of-line so the hot wrapper stays inlineable, fully optimized when hit). A benchmark of an IL-inlined variant vs this
call-based form showed the per-element cost is dominated by the transcendental, so hand-emitting the formulas is not worth the duplication.
- NDDivision
floor-division and remainder helpers matching NumPy's
floor_div_@TYPE@(loops_arithmetic.dispatch.c.src), integerremainder(loops_modulo.dispatch.c.src), and the floating-pointnpy_floor_divide@c@/npy_remainder@c@(Python-divmod port innpy_math_internal.h.src).Semantics replicated exactly:
- Integer divide/modulo by zero returns
0(NumPy raises a RuntimeWarning but yields 0, never throwing — C#'s DivideByZeroException must not surface). - Signed integer floor-division rounds toward negative infinity (Python
//), not toward zero like C#/;MIN // -1wraps toMIN(overflow), matching NumPy'snpy_set_floatstatus_overflow(); return NPY_MIN. - Signed integer remainder uses the floored (Python) sign convention: the result has the
sign of the divisor;
MIN % -1 == 0. - Float floor-division/modulo follow CPython's
divmod(fmod, sign-fixup, snap-to-nearest-integer), soa // 0.0is±inf/nan(not forced NaN) and edge cases like0.7 // 0.1 == 6.0and-2.0 // inf == -1.0match.
- Integer divide/modulo by zero returns
- NDFloatMath
Transcendental helpers backing NumSharp's float unary ufuncs. Unlike the BCL's MathF/Math (which route to the platform libm and are only ~correctly rounded), each entry point here is a port of the kernel NumPy 2.4.2 actually runs, so the result is bit-identical to NumPy rather than merely within a couple of ULP.
Most of these are float32-only, because at float64 the platform libm already agrees with NumPy bit-for-bit on this host and there is nothing to port. Tanh(double) is the exception: NumPy ships its own kernel at both widths, so both diverged from the BCL and both are ported.
Exp(float) =
simd_exp_FLOAT, Log(float) =simd_log_FLOAT(numpy/_core/src/umath/loops_exponent_log.dispatch.c.src, theSIMD_AVX2_FMA3instantiation). NumPy's own algorithm: clamp/flag the overflow and underflow ends, Cody-Waite range reductiony = x - k·ln2withk = rint(x·log2(e)), evaluateexp(y)as the ratio of a 5th-order over a 2nd-order Remez minimax polynomial, then scale by2^k. It is NOT correctly rounded — NumPy documents a max error of 2.52 ULP (atx = 0xc2781e37) — so reproducing NumPy means reproducing that error, operation for operation, in the same order.Why this is lane-width independent. Every step is elementwise (no cross-lane reduction, shuffle or horizontal op), and the whole-vector
ifinside NumPy'sfma_scalef_psis followed by a per-lane blend whose non-denormal lanes are unchanged by the taken branch. This scalar entry point therefore yields the same bits the 8-lane__m256kernel yields — as do theVector{128,256,512}overloads inNDFloatMath.Simd.cs, which the IL kernels prefer when the host has FMA.The FMA contraction is part of the answer, not an optimization. NumPy writes the quadrant as
_mm256_mul_ps(x, log2e)followed by_mm256_add_ps(quadrant, magic), but the wheel's compiler (MSVC 19.44, the numpy==2.4.2 win-amd64 build) contracts that pair into a singlevfmadd. The difference is observable: atx = 0xc26d0e6cthe un-contracted product lands on the exact tie-85.5and the magic-constant rint takes it to-86(half-to-even), while the fused form keeps the extra bits of-85.4999987495703and rounds to-85— a 1-ULP difference in the result. Hence FusedMultiplyAdd(float, float, float) here, matching the binary NumPy ships.Specials (probed against 2.4.2, and a consequence of NumPy zeroing the NaN lanes before the range comparisons, which makes the three masks mutually exclusive): any NaN — quiet or signalling, either sign, any payload — returns the canonical
0x7fc00000;x ≥ 88.72283935546875(incl.+inf) returns+inf;x ≤ -103.97208404541015625(incl.-inf) returns+0;±0returns1. Results between those ends denormalize gracefully through the ScalefDenormal(float, float) path. NumPy additionally raises the FP overflow/underflow status flags (surfacing as aRuntimeWarning); NumSharp models no FP status word, so that signalling is absent — a pre-existing, engine-wide difference, not one this port introduces.Verified exhaustively: all 2^32 float32 bit patterns agree with NumPy 2.4.2 (chunked checksum sweep), not a sample. Perf: the finite path is AggressiveInlining so the JIT folds it into the IL-emitted unary kernel with no per-element call frame; the NaN/overflow/underflow ends and the denormal scale-back live in NoInlining cold helpers so the hot path stays inlineable.
- NDIntegerPower
Integer power helpers matching NumPy's
@TYPE@_powerloop inloops.c.src. Uses repeated-squaring with native dtype wraparound (e.g.uint8 ** 8 = 0).These helpers assume the exponent is non-negative. NumPy raises
ValueError("Integers to negative integer powers are not allowed.")for any negative integer exponent, regardless of base value; the caller is responsible for that pre-check (seeDefaultEngine.Power).
- NDLogAddExpMath
Scalar kernels backing NumSharp's
np.logaddexp/np.logaddexp2/np.nextafterbinary ufuncs. Each entry point reproduces the exact NumPy 2.4.2 algorithm (npy_math_internal.h.src/ieee754/halffloat.cpp), operation for operation, so the result matches NumPy on this platform to within the accuracy of the one primitive NumSharp cannot reproduce bit-for-bit — the CRT'slog1p.nextafter is BIT-EXACT. BitIncrement(double) / BitDecrement(double) (and the MathF twins) compose to exactly the ucrtbase
nextafter/nextafterfNumPy calls (verified 0-diff over 6M random pairs). Half is a straight port ofnpy_half_nextafteron the raw 16-bit pattern.logaddexp / logaddexp2 are ≤2 ULP. Exp(double), Exp2(double) and Exp(float) are already bit-identical to the ucrtbase
exp/exp2/expfNumPy calls; the only divergence is Log1p(double) — an fdlibm port that agrees with the (closed) ucrtbaselog1pto ≤1 ULP. So float64logaddexpis ≤1 ULP,logaddexp2≤2 ULP (theLOG2E·log1pproduct), float32logaddexpis bit-exact andlogaddexp2≤1 ULP. The catastrophic small-value case that a naivelog(1+exp(-tmp))loses (logaddexp(0,-50)= 1.93e-22, not 0) is fully recovered.Half is computed in float32 (
(Half)F((float)x,(float)y)), matching NumPy'see->ehalf loops which promote to float. Decimal (no NumPy analog) bridges through double.
- NonGenericConvert
Provides a way to convert boxed object from known time to specific type.
- TypelessConvert
Provides a way to convert boxed object from known input type to known output type. By making it receive and return object - It is suitable for a common delegate: see TypelessConvertDelegate
- UnmanagedBuffer
Provides low-level memory copy operations for unmanaged types.
- UnmanagedSpanExtensions
Extension methods for UnmanagedSpan{T} and ReadOnlyUnmanagedSpan{T}. These provide SIMD-accelerated operations where possible. All methods support full 64-bit indexing natively.
- py
Implements Python utility functions that are often used in connection with numpy
Structs
- ReadOnlyUnmanagedSpan<T>
ReadOnlyUnmanagedSpan represents a contiguous region of arbitrary memory. Unlike arrays, it can point to either managed or native memory, or to memory allocated on the stack. It is type-safe and memory-safe.
- ReadOnlyUnmanagedSpan<T>.Enumerator
Enumerates the elements of a ReadOnlyUnmanagedSpan<T>.
- UnmanagedSpan<T>
UnmanagedSpan represents a contiguous region of arbitrary memory. Unlike arrays, it can point to either managed or native memory, or to memory allocated on the stack. It is type-safe and memory-safe.
- UnmanagedSpan<T>.Enumerator
Enumerates the elements of a UnmanagedSpan<T>.