Conversation
…AS in the interop suite Adds byte-for-byte parity tests that compute np.linalg.cholesky / np.linalg.qr twice over the SAME memory — once by NumSharp's OpenBLAS backend, once by the embedded CPython's numpy.linalg reading NumSharp's ZERO-COPY export — and assert the two results are byte-identical. This is the strongest parity gate in the repo: no serialization step between the two stacks, numpy computing over NumSharp's actual buffer through the operand's own strides, and parity against the numpy actually installed rather than an offline snapshot. Suite wiring (shared infra): - NumSharp.Interop.UnitTests.csproj: reference NumSharp.Interop.OpenBLAS so the bundled scipy-openblas runtime asset flows to the output and np.dot/matmul/linalg go through it. - PythonSession.Start: OpenBlasEngine.TryEnable(threads: 1). TryEnable never throws (a bare image keeps the managed kernels and the LAPACK tests report Inconclusive via LapackAvailable); the 1-thread pin makes the small-matrix comparisons against live numpy deterministic. CholeskyQrLiveParityTests (19 cases, InteropTestBase + Python.np + ByteContract): - cholesky lower/upper value-rich; float32 (computed in double, cast back — matches numpy's "lite" linalg); complex Hermitian; all 9 widening dtypes (bool + ints → float64); batched (3-D, 4-D); every layout (C/F/transposed/reversed/strided/broadcast, exported as the VIEW); 1x1; empty; not-positive-definite raising on BOTH stacks with the identical message. - qr reduced/complete/r/raw over tall/wide/square; float32 and complex128 all modes; all widening dtypes; batched (3-D/4-D); every layout; degenerate shapes ((3,0) incl. complete→identity Q, (0,3), 1x1). raw's h differs in memory order (C-contig vs numpy's F-view) but ByteContract compares canonical C-order bytes, so the identical values are what is asserted. All 19 pass against the live NumPy 2.4.2 in-process (its OpenBLAS is byte-identical to the bundle). Why byte-exact holds and when: BLAS/LAPACK result bits depend on three levers — the OpenBLAS build, the thread count, and the dispatched DYNAMIC_ARCH micro-kernel. This suite pins all three (bundled == numpy 2.4.2's binary; threads:1; small matrices stay single-threaded; same process ⇒ same CPU), so the comparison is deterministic and identical. There is no tolerance fallback — a divergent host BLAS SHOULD go red. Documented in full in the new test/NumSharp.Interop.UnitTests/CLAUDE.md, which is the agent-facing guide to live-numpy byte-parity testing (PythonSession lifecycle, InteropTestBase, Python.np, NumpyExtensions, ByteContract, GIL discipline, the leak gate, the canonical recipe, error parity, the three levers, the Windows C-long reduction trap, and how to add a new op's live gate).
The ApiInventory tool reflected only np/NDArray/NumPyRandom, but the coverage
denominator counts numpy.linalg.* and numpy.fft.*. Those NumSharp functions do
not live on np: np.fft.* is on the FourierModule class (reached via the np.fft
property) and np.linalg.* is on the nested np.linalg static class. Reflecting
typeof(np) with DeclaredOnly sees the fft *property* and cannot see a nested type
at all, so the entire np.fft surface (18) and every linalg factorisation without
a top-level np.* twin (~23) were mis-reported as 'missing' -- fft 0.0%, linalg
19.4%, headline 60.4%.
Tool (Program.cs): additionally reflect typeof(FourierModule) (instance) and
typeof(np.linalg) (static) -> inventory keys fft/linalg. Captures all 18 fft
transforms and all 31 linalg.__all__ members incl. cross (which has no np.cross).
Generator (generate_coverage.py): add the two facade surfaces to the three
manual-linking tables that were hardcoded to {np,ndarray,random} --
SourceLocator.TYPE_PATTERNS (class FourierModule / class linalg, for source
links), member_maps (by_surface + definitions prefixes NumSharp.FourierModule /
NumSharp.np.linalg), and direct_target's prefix dict. Bump GENERATOR_VERSION
1.1.2 -> 1.2.0.
overrides.json: drop the now-dead numpy.linalg.matrix_power alias -- it resolves
directly to linalg.matrix_power once the nested class is reflected.
Regenerated coverage/generated/* (generate_coverage.py --check passes):
np.fft.* 0/18 -> 18/18 (100%)
np.linalg.* 6/31 -> 31/31 (100%)
headline 60.4% -> 76.8% (338 -> 430 / 560)
The +92 is the fft/linalg structural fix plus stale rows regeneration caught up
(conj, argpartition, einsum, diag, isin, ix_, lexsort, nanargmax, ...).
Facade audit (exhaustive): the only function-namespace facades are random
(already reflected), fft, and linalg. The c_/r_/mgrid/ogrid/s_/index_exp DSLs are
single objects NumPy exports one-each (already captured as single np properties);
no np.char/ma/testing/polynomial/emath exist in NumSharp. numpy.linalg.LinAlgError
is a class (out of default scope; NumSharp exposes it top-level) and does not
affect the headline.
…rix_rank/cond/norm{2,-2,nuc}/lstsq (gesdd/gelsd)
Completes the SVD/least-squares np.linalg surface through NumSharp.Interop.OpenBLAS, reusing
the LU/Cholesky/QR flow: the bundled scipy-openblas LAPACK gesdd (SVD) and gelsd (least
squares). Value-parity with NumPy 2.4.2; 39 unit tests in LapackSvdTests.cs (Inconclusive
without LAPACK, like the LU/matmul-parity gates). Full suite green modulo one pre-existing,
unrelated failure (AuditV2 T1_33, from 02fe0fb making AsNumpyDtypeName public while the test
reflects it NonPublic).
Engine (NumSharp.Interop.OpenBLAS) — builds on the gesdd/gelsd bindings + ILapackType
Gesdd/Gelsd already staged in OpenBlasNative.cs:
- ILapackType<T>.Abs2 (v*v / |z|^2) for lstsq's residual sum, in both DoubleLapack/ComplexLapack.
- OpenBlasEngine.Svd.cs: SvdCore<T,TOps> + TrySvd. JOBZ N/S/A (values / reduced / full),
column-major linearize through the operand's own strides (any layout), workspace query,
stacked outer loop, zero-K identity fill (NumPy's identity_matrix fallback), and
LinAlgError("SVD did not converge"). Singular values are always the real basetype; float32
computes in double and casts U/S/Vh back to single, exactly as NumPy's _commonType.
- OpenBlasEngine.Lstsq.cs: LstsqCore<T,TOps> + TryLstsq. gelsd with the B buffer at LDB=max(m,n),
work/iwork/(complex)rwork all queried; x = the first N rows, residuals = squared 2-norm of the
excess rows only when m>=n && rank==n (else NaN), rank int32, s = min(m,n) singular values.
- OpenBlasBackend: TrySvd/TryLstsq wired to the engine.
Wrappers (NumSharp.Core/LinearAlgebra/linalg):
- svd/svdvals now compute via the backend (no wrapper change was needed).
- pinv (np.linalg.inv.cs): conjugate -> reduced SVD -> cutoff (rcond * max s) -> reciprocate ->
Vh^T @ (s * U^T); empty-matrix short circuit returns (N,M) of the ORIGINAL dtype.
- matrix_rank (np.linalg.svd.cs): count singular values above max(M,N)*eps*max(s); the <2d
predicate short circuit is unchanged.
- cond (np.linalg.svd.cs): None/+-2 is the singular-value ratio smax/smin (+ NaN->inf unless the
matrix held a NaN, + the empty-array guard "cond is not defined on empty arrays"); the other
orders compose norm * norm(inv).
- norm 2/-2/nuc (np.linalg.norm.cs): _multi_svd_norm — moveaxis the two matrix axes to the end,
take svdvals, reduce with amax / amin / sum.
- lstsq (np.linalg.lstsq.cs): rewritten to NumPy 2.x semantics — 1-D b promotion + squeeze back,
default rcond eps*max(m,n) (was the stale pre-2.0 -1), nrhs==0 padding with trim, m==0 zeroing,
residuals discarded unless rank==n && m>n, and the real/result dtype coercion.
NumPy parity notes (verified, don't re-derive): linalg computes SVD/lstsq in double regardless
of input width, so only the d/z routines are ever called; lstsq's rank is int32 while
matrix_rank's is int64; U/Vh carry a per-column sign freedom, so the tests assert the singular
values, the reconstruction and the sign-invariant derived quantities (pinv/rank/cond/norm/lstsq)
rather than U/Vh entrywise. Performance is parity-bound (the same native OpenBLAS on both sides),
matching the LU backend. Still NSE-throwing: the eigenvalue family (eig/eigvals/eigh/eigvalsh)
and einsum.
…singular) composition orders
Adversarial completeness validation of svd/svdvals/pinv/matrix_rank/cond/norm{2,-2,nuc}/lstsq
against NumPy 2.4.2 across the DOD variation matrix. 22 tests in LapackSvdCompletenessTests.cs; full
suite green (the lone failure, AuditV2 T1_33_AsNumpyDtypeName, is pre-existing and unrelated —
02fe0fb made AsNumpyDtypeName public while that reflection test still expects it NonPublic).
One real gap found and fixed:
- cond(singular, p in {1,-1,+-inf,'fro'}) — NumPy calls the RAW inv gufunc, which nan-fills a
singular matrix rather than raising, so norm(inv) is nan and cond's nan->inf tail returns inf.
NumSharp's np.linalg.inv RAISES on a singular matrix, so a composition-order cond of a singular
2-D operand used to throw. It now catches the LinAlgError and routes through CondNanToInf(nan) ->
inf, matching NumPy. The svd-based orders (None/+-2) were already correct (the smallest singular
value is ~1e-16, not exactly 0, so the ratio is huge-but-finite). A singular element inside a
STACK still raises (per-element nan-fill would need a non-raising inv) — pinned [Misaligned].
Validation coverage (LapackSvdCompletenessTests.cs):
- Dtypes: bool + every integer width widen to float64; float32/float64/complex keep width (S always
real); Half/Decimal/Char raise the verbatim "array type X is unsupported in linalg".
- Layouts: svdvals is storage-independent — C / F / strided-view / transposed / negative-stride all
give the same values, and a broadcast (stride-0) batch reads the same matrix per element.
- Metamorphic (over np.random data, no oracle): reduced + full-matrices reconstruction, unitary
factors (U^T U = I, Vh Vh^T = I), Moore-Penrose A P A = A / P A P = P incl. a stacked pinv, the
least-squares normal equations A^T(Ax-b)=0 with residual = ||Ax-b||^2, and cross-function
consistency (nuc = sum(S), spectral = max(S), rank = count(S > tol)).
- Edges: svd(NaN) -> "SVD did not converge" (info != 0) while svd(inf) converges (info == 0) with
NaN singular values; cond(singular) svd-orders finite, composition-orders inf.
- Error taxonomy: sub-2-D -> "at least two-dimensional", the mutually-exclusive tol/rtol and
rcond/rtol ValueErrors, lstsq's "Incompatible dimensions" and 3-D-b rejection.
- Anchors: svdvals/pinv/matrix_rank/complex-lstsq/matrix_norm against NumPy 2.4.2 values.
Accepted limitations (documented): hermitian=true is ignored (routes to gesdd, not the unimplemented
eigh) but the VALUES are correct for hermitian inputs; tol/rtol/rcond are scalar only (NumPy also
accepts array_like for per-element stacked thresholds); the stacked-singular composition cond above.
…ce (gesdd/gelsd) Adds SvdLstsqLiveParityTests to NumSharp.Interop.UnitTests — the strongest gate in the repo: real CPython+numpy embedded in the test process (pythonnet) computes each op over NumSharp's OWN exported bytes (zero-copy) and asserts the two results are BYTE-FOR-BYTE identical. 16 tests, ~200 byte comparisons; the full interop suite (390 tests) stays green. Both stacks call the SAME single-threaded scipy-openblas gesdd/gelsd (numpy pins it, this package bundles the byte-identical copy), so the factor bytes are a deterministic function of the input — and U/S/Vh are byte-identical despite their per-column sign freedom, because the shared LAPACK routine resolves that sign the same way on both sides. numpy's linalg computes every factorisation in double/cdouble and casts back, so float32 operands and bool/integer widening are byte-identical too. Byte-exact coverage (via SameBytes over the zero-copy export, tuple returns indexed [0]/[1]/[2]): - svd U/S/Vh — every widening dtype + float32/float64/complex, every layout (C/F/transposed/reversed/ strided via the exported VIEW), tall/wide/square x full/reduced/compute_uv=False, stacked 3-D/4-D, 1x1 and empty (identity-fill). - svdvals, real pinv (shapes/dtypes/layouts/rcond/stacked), matrix_rank (default/tol/rtol/stacked — int64 width matches numpy's intp), cond (None/2/-2/1/-1/+-inf/'fro', complex, stacked), norm 2/-2/'nuc' (layouts, axis-tuple stack, keepdims), lstsq x/resids/rank/s (1-D/2-D b, over/under/ square/rank-deficient, int/single/double, complex — rank int32 matches the gufunc 'i'). - Error parity: sub-2-D, NaN non-convergence, lstsq incompatible dimensions raise on BOTH stacks. One documented non-byte-exact case: COMPLEX pinv. It ends in a complex reconstruction matmul, and OpenBlasBackend serves float32/float64 only, so NumSharp's complex matmul runs its managed GEMM while numpy calls zgemm — they agree to ~1 ULP but not byte-for-byte. Asserted with numpy's own allclose over NumSharp's exported result; the SVD factors feeding it ARE byte-exact (complex svd case above).
…h/acosh/atanh aliases), byte-exact vs NumPy 2.4.2
Adds NumPy's three inverse-hyperbolic ufuncs plus their NumPy-2.0 Array-API
aliases (np.asinh/acosh/atanh are the same ufunc), integrated through the exact
arcsin/sinh engine seam — no new kernel class, no per-dtype branching.
API surface (mirrors arcsin exactly):
np.arcsinh(x, out=None, where=True, dtype=None) + (x, NPTypeCode) / (x, Type)
np.arccosh(...) np.arctanh(...) + asinh/acosh/atanh aliases (same overloads)
Engine seam: TensorEngine.{ASinh,ACosh,ATanh} -> ExecuteUnaryOp(UnaryOp.{Asinh,
Acosh,Atanh}) -> DirectILKernelGenerator; dtype via ResolveUnaryFloatReturnType
(bool/i8/u8->f16, i16/u16/char->f32, i32+->f64; float/decimal/complex preserved).
PARITY (all verified against NumPy 2.4.2, differential byte-compare):
* Real float32/float64: BYTE-IDENTICAL. Math.Asinh/Acosh/Atanh and the MathF
twins call the same MSVC ucrtbase CRT as npy_asinh/acosh/atanh — 0 bit-diffs
over 4521 adversarial inputs at BOTH widths (domain boundaries, +-inf, NaN,
subnormals, +-0). Same platform-libm class as the exp2/log1p cells. Emitted
via EmitMathCall — no custom kernel needed.
* Integer/bool tiers + Char: byte-exact (compute-in-float-tier), like arcsin.
* float16: Half->double->Math.X->Half arm (the existing arc-trig f16 path);
<=1 ULP finite + NaN canonicalised (tokenised by the corpus), identical in
character to arcsin(f16).
* Decimal: decimal->double->Math.X->decimal bridge (house transcendental policy).
* Complex128: derived from the byte-exact Asin/Acos/Atan(=Catanh) ports through
NumPy's OWN msun involution I*conj(.), which is a pure component swap
((z.Im, z.Re), zero arithmetic, zero rounding):
asinh(z) = swap(asin(swap z)) [casin<->casinh involution]
atanh(z) = catanh(z) [already ported; drives np.arctan]
acosh(z) = cacosh_formula(acos(z)) [npy_cacosh: +-I*cacos, |Im|+copysign,
NaN/Inf special block ported verbatim]
The three identities are bit-exact INSIDE NumPy itself (verified 0 diffs over
20,036 inputs incl. every NaN/Inf corner), so the derived ops inherit the whole
complex-unary family's documented <=3 ULP envelope. arccosh inherits arccos's
single sub-DBL_MIN-imaginary pathological edge (Complex.Acos flushes the
denormal Re to 0), classified under MisalignedRegistry branch 7 alongside
arccos; arcsinh/arctanh are fully <=3 ULP (asin/atan have no such edge).
Files:
NDComplexMath.cs +Asinh/Acosh/Atanh (Atanh exposes the existing Catanh)
DirectILKernelGenerator {.Unary.Math (real/int), .Unary.Decimal (Decimal group,
Half group, Complex cases), .cs (CachedMethods.Complex*)}
KernelOp.cs +UnaryOp.{Asinh,Acosh,Atanh}
TensorEngine + Default.{ASinh,ACosh,ATanh}.cs, Math/np.{arcsinh,arccosh,arctanh}.cs
DefaultEngine.UfuncOut UfuncName -> canonical "arcsinh"/"arccosh"/"arctanh"
(even for the asinh alias, matching ufunc.__name__)
NDExpr(.Typing) +factory nodes + IsFloatPromoting (np.evaluate fusion)
Gates (green, net8.0 + net10.0):
Math/InverseHyperbolicTests.cs (16: real/complex/dtype-tiers/out/where/dtype=/
aliases/non-contiguous/evaluate-fusion) + NpApiOverloadTests_UnaryMath regions.
Fuzz oracle: OpRegistry + gen_oracle UNARY_EXTRA_OPS & SPECIAL_UNARY_OPS ->
unary_extra.jsonl / specials.jsonl regenerated (364*3 + 20*3 cases across all
14 dtypes x layouts x specials). FuzzMatrix UnaryExtra+Specials pass; the large
corpus line-diff is purely the id running-counter shifting (existing-op data is
byte-identical, id-stripped diff = 0). MisalignedRegistry branch 7 extended for
the complex arccosh denormal edge. No regressions (134 existing unary/math tests).
…f.Asinh (+ completeness validation) Follow-up to the inverse-hyperbolic feature. Completeness audit against every axis (all 15 dtypes, ~25 layouts, the out=/where=/dtype= matrix, error parity, and a NumPy benchmark) surfaced one improvable cell — float16 — now fixed, plus documents the performance ceiling for the rest. float16 improvement (correctness + speed): * WAS: Half -> double -> Math.X -> Half (the shared arc-trig f16 double bridge) — <=1 ULP finite (excused) and 0.68-0.80x NumPy. * NOW: native Half.Asinh/Acosh/Atanh, which compute in float32 — this is EXACTLY NumPy's float16 loop (generate_umath astype 'e'->'f', i.e. (Half)asinhf((float)h)). Verified BYTE-EXACT: 0 finite-diffs over ALL 65536 f16 values through the real np.* API (was 4/1/0), and faster (0.75-0.87x, up from 0.68-0.80x). Pulled ONLY these three ops out of the shared Half group case; sinh/arcsin/... keep the double bridge (unchanged). Gate: Float16_ByteExact pin + the unary_extra/specials f16 cases now pass byte-exact instead of via the ~ULP excuse. Completeness validation (all verified vs NumPy 2.4.2, no code change needed): * out= / where= / dtype= matrix: 21/21 byte-exact — same-instance return, strided-out base buffer, masked-slot preservation, dtype-loop precision, out-cast (int8->f8), compute-f32-store-f64. Error parity VERBATIM: "No loop matching ... ufunc arcsinh" (canonical name even for the asinh alias), where-cast, out shape/dtype — text matches; exception TYPE follows the house convention (IncorrectTypeException/ ArgumentException <-> TypeError/ValueError), as everywhere in the library. * Edge layouts byte-exact: 0-d, empty, 5-D, scalar-broadcast, partial-broadcast, newaxis (on top of the fuzz corpus's C/F/strided/transposed/neg-stride/sliced set). * Char == uint16 proxy (float32 tier). Decimal: valid-domain correct (15-sig-fig house policy); out-of-domain NaN/inf throws OverflowException on (decimal)NaN — a PRE-EXISTING bridge limitation shared by the WHOLE decimal transcendental family (arcsin(2m)/arccos(2m)/sqrt(-1m)/log(-1m)/log(0m) all throw identically), no NumPy decimal analog. Not introduced here; left consistent with the family. * Overload resolution: all 7 arg-shapes x 6 functions resolve unambiguously (Overloads_AllFormsResolve pin). Performance verdict — byte-exact PARITY, not >=1.5x, and why that is correct here: real f32/f64 emit scalar Math.Asinh (no Vector<double>.Asinh in the BCL), and NumPy on win-amd64 is ALSO scalar npy_asinh (the SVML asinh kernels are Linux/AVX-512-only, never compiled on Windows). So byte-exactness REQUIRES the scalar CRT on both sides -> ~1x (geomean alloc 0.99x / out 0.94x; f64 up to 1.41x at 10M). This is the same documented category as exp2/expm1/log1p (platform-libm-bound, byte-parity prioritised over a divergent SIMD polynomial). complex128 is competitive-to-faster (arctanh/arccosh 1.07-1.20x). Gates all green: 216 unary/math tests + full FuzzMatrix (104 tiers) on net8.0 and net10.0.
…nh performance The inverse-hyperbolic completeness+performance validation is concluded. This records the measured performance finding in the CLAUDE.md inverse-hyperbolic paragraph so the parity ceiling is not re-litigated. Benchmark (NPY/NS, Release, best-of-11 warm, out=, 10M f64 on byte-identical inputs loaded from a shared .npy so input distribution is eliminated as a variable): arcsinh 0.99x (NS 97.05 ms vs NumPy 96.21 ms) arccosh 1.00x (NS 65.63 ms vs NumPy 65.64 ms) arctanh 0.97x (NS 89.10 ms vs NumPy 86.43 ms) Two facts settle whether better performance is reachable: 1. The IL kernel is ALREADY faster than a naive managed Math.* scalar loop over the identical array (atanh 89.1 vs 98.0, asinh 97.1 vs 102.0, acosh 65.6 vs 70.7 ms/10M) — the emitted kernel's hoisted pointers + unrolling beat the straight loop, so there is no NumSharp-side overhead to reclaim. 2. NumSharp sits within ~3% of NumPy (0.97-1.00x). The whole runtime is the shared ucrtbase CRT transcendental call. An earlier uncontrolled run showed a 0.87x arctanh cell; that was input-distribution + run variance — on byte-identical inputs it is 0.97x. Why 1.5x is unreachable without breaking byte-parity: these three are the one arc-family for which NumPy has NO active SIMD kernel on win-amd64 (its SVML asinh/acosh/atanh are AVX-512/Linux-gated and never compiled into the 2.4.2 wheel). Unlike exp/log/sin/cos/tanh — which NumSharp ports bit-exactly and beats — there is nothing to port, and byte-exactness forces the scalar CRT on both sides. A divergent SIMD polynomial is the only faster route and it would break byte-parity, so it is rejected. Same principled category as the documented platform-libm cells exp2/expm1/log1p. Validation is otherwise complete and green (real f32/f64 + f16 byte-exact, complex <=3 ULP, out/where/dtype byte-exact, error parity verbatim, all layouts byte-exact, Char/Decimal consistent). Doc-only change; no behavior touched.
…r arcsinh/arccosh/arctanh Answers 'can we POC a SIMD+unrolling+other perf boost, or are we already doing it?' with measured evidence, and corrects a factual error in the prior perf note (the scalar-math unary loop is NOT unrolled). What the current kernel actually is: EmitUnaryScalarLoop emits a plain, non-unrolled i++ loop with a raw-pointer direct 'call Math.Asinh' and no bounds checks (Asinh/Acosh/Atanh fall through every CanUseUnarySimd branch by necessity — there is no Vector<double>.Asinh). The previous note's 'unroll' was wrong; the kernel's whole edge is pointer access + elided bounds checks. POC 1 — unrolling the CRT-call loop (4x, 8x): REJECTED. Wash-to-slightly-slower for asinh/atanh (unroll8 asinh 121 vs scalar 110 ms/10M), tiny help for acosh only. The ucrtbase call latency dominates and cannot be hidden by issuing independent calls; the extra body just adds instruction pressure. POC 2 — SIMD via System.Numerics.Tensors.TensorPrimitives (10.0.11), .NET's own production SIMD+FMA transcendental library: it is 0-ULP against NumPy over 10M f64 AND f32 inputs — which is the DIRECT PROOF that it does not vectorize these three ops (no correctly-rounded vector asinh/acosh/atanh exists, so it falls to the scalar CRT). A genuine vector kernel would therefore necessarily diverge from ucrtbase. And it is not even faster: in one warm process on the same 10M f64 array, NumSharp np.arcsinh IL kernel 97.3 ms (fastest) TensorPrimitives.Asinh 101.1 ms (1.04x slower) direct Math.Asinh loop 105.5 ms (1.08x slower) So NumSharp's existing kernel already beats .NET's own SIMD transcendental library for these ops. The only faster route is a divergent SIMD polynomial (SLEEF/Cephes, ~1-4 ULP off), which breaks byte-parity and is rejected under the project's paramount parity constraint — same principled ceiling as the documented platform-libm cells exp2/expm1/log1p. Doc-only change; no behavior touched.
Implement np.digitize(x, bins, right=False) as a NumPy-2.4.2-exact composition
over searchsorted, and fix the pre-existing searchsorted NaN-ordering bug it
surfaced.
## np.digitize (Sorting_Searching_Counting/np.digitize.cs)
Port of numpy.lib._function_base_impl.digitize: searchsorted + a monotonicity
check, mapping `side = 'left' if right else 'right'` and inverting decreasing
bins via `len(bins) - searchsorted(bins[::-1], x, side)`. Bit-exact vs NumPy
across the full variation matrix (verified in-process):
- increasing / decreasing bins x right={False,True}
- N-D x (result keeps x's shape), 0-d scalar x -> 0-d result
- empty / single / all-equal / duplicate bins
- out-of-range x -> 0 or len(bins)
- int/bool/uint x; return dtype int64 (intp)
Two correctness details ported deliberately:
- Promotion: NumSharp's searchsorted casts the keys to the sorted array's dtype,
which would truncate e.g. a negative float x into int bins. digitize promotes
`bins` to result_type(bins, x) first so x is widened, not truncated
(NegativeFloatX_IntBins_NoTruncation / Int64X_BeyondInt32Bins_Promotes).
- Monotonicity: BinsMonotonicity is a faithful port of check_array_monotonic
(compiled_base.c) run in double, non-strict, with the NaN quirk intact — the
strict </> after the first differing pair means a NaN pair never registers a
violation, so [1,2,nan,3] is reported increasing exactly as NumPy does. The
diff-based composition (all(diff>=0)) diverges here and was rejected.
Error taxonomy matches NumPy verbatim: complex x -> IncorrectTypeException
("x may not be complex"), >1-D bins -> "object too deep for desired array",
0-D bins -> "object of too small depth for desired array", non-monotonic bins
-> "bins must be monotonically increasing or decreasing".
## searchsorted NaN fix (DirectILKernelGenerator.Search.cs)
digitize passes x (arbitrary order, possibly NaN) as searchsorted keys, which
exposed that the kernel did not implement NumPy's NaN-as-largest total order.
NumPy (numpy_tag.h + binsearch.cpp) derives both side comparators from one
`less` functor — left = less(a,b), right = less_equal(a,b) = !less(b,a) — and
float `less` is NaN-aware ((a<b) | (a==a & b!=b), so NaN sorts last). NumSharp
used ordered `<` (left) and `!(a>b)` (right), so `NaN <= finite` was wrongly
true: a NaN key placed at index 0 on the left, and on the right it corrupted the
following key via the monotonic-bound carry (`[nan,1.0]` -> `[5,5]` not `[5,2]`).
EmitCmpForSide now reads both operands from locals and derives both sides from a
new NaN-aware EmitLess; integer/char/bool collapse to the ordered clt/clt.un
(unchanged, bit-identical), only float/half/complex-real gain the NaN term. The
change is perf-neutral (benchmarked: ~parity with NumPy on the random-key hot
path, unchanged vs the pre-fix kernel) and the whole comparison is a transcription
of NumPy's own functors, so the corpus stays bit-exact.
## Gates
- Unit: np.digitize.Test.cs (22), np_searchsorted_nan.cs (12 — both sides, carry
+ bisect, scalar-key + float32 regression pins).
- Fuzz: gen_digitize added to the sort tier (30 cases: dtypes x inc/dec x right,
a float-into-int-bins promotion lock, and NaN-in-x cases); OpRegistry wired.
FuzzMatrix green (104), sort corpus regenerated.
- No regression: 329 sort/searchsorted unit tests, full CI-filtered suite
(13281 passed; the 2 failures are pre-existing and unrelated — verified they
fail identically with these changes reverted).
… (pure composition)
Line-for-line port of numpy/lib/_function_base_impl.cov as a pure composition over
existing infrastructure (average/dot/concatenate/atleast_2d/conjugate/squeeze) — no new
kernel. cov is GEMM-bound: it inherits np.dot's performance (managed GEMM by default,
BLAS-fast and byte-identical once NumSharp.Interop.OpenBLAS is referenced).
Signature parity: cov(m, y=None, rowvar=True, bias=False, ddof=None, fweights=None,
aweights=None, dtype=None). ddof is int? (NumPy's 'ddof must be integer' ValueError is
enforced by the type). Result dtype = result_type(m[,y], float64) unless dtype is given;
crucially it is NOT force-cast at the end, so dtype=float32 WITH weights still yields
float64 (the float64 weights promote avg->dot) — matching NumPy exactly.
NumPy fidelity details reproduced deliberately (each caught by the differential harness):
- 'X -= avg[:, None]' is performed in the operand's dtype (in-place semantics), so
dtype=float32 without weights stays float32.
- fact = w_sum - ddof*sum(w*aw)/w_sum; fact<=0 (DoF<=0) => scale becomes +inf (1/0).
- For complex input NumPy's w_sum inherits complex128, so its '/w_sum' is Smith's-method
numer*(1/w_sum) — a reciprocal-multiply that rounds its real part 1 ULP off real
division and flips finite-vs-inf when fact~0; reproduced for complex data.
- Empty variables -> (0,0) float64 regardless of input dtype; single observation / scalar
-> nan; ddof too large -> inf.
Error taxonomy is verbatim NumPy:
- ValueError: 'm/y has more than 2 dimensions', '{f,a}weights cannot be negative'.
- TypeError: 'fweights must be integer'.
- RuntimeError (new house exception, mirrors TypeError/ValueError/AxisError/LinAlgError):
'cannot handle multidimensional {f,a}weights', 'incompatible numbers of samples and
{f,a}weights'.
- weights summing to zero -> DivideByZeroException (NumPy ZeroDivisionError), same message.
Verification:
- 2556-case differential vs NumPy 2.4.2 (5 dtypes x 8 shapes x rowvar x bias x
ddof{None,0,1,2} x weight combos{none,fw,aw,both} x y x layouts): 0 shape/dtype/value
failures; ~57% bit-exact (small observation counts), remainder within managed-GEMM
tolerance; degenerate DoF<=0 soup agrees (both non-finite).
- Non-contiguous inputs (transposed/sliced/negative-stride/F-order/broadcast) match.
- Statistics/np.cov.BattleTests.cs (34 tests) green on net8.0 and net10.0; full Statistics
suite (238) still green.
Files: src/NumSharp.Core/Statistics/np.cov.cs, src/NumSharp.Core/Exceptions/RuntimeError.cs,
test/NumSharp.UnitTest/Statistics/np.cov.BattleTests.cs, .claude/CLAUDE.md.
… + fix a latent double-engine pass7/pass11 bug Prove-then-fix of the documented "np.fft float32/float16 dtype divergence". The old claim — "dtype-only divergence, values are the correctly-rounded double result, not a compute gap" — was DISPROVEN: NumPy does NOT compute float32 fft in double. Its fft ufuncs carry a single (Ff->F/ff->F) and a double (Dd->D/dd->D) loop and pick between them by operand shape + fct dtype, so NumSharp's old "promote to double" produced values that differed from NumPy in BOTH dtype AND value (up to ~1e-7). This makes the values bit-identical (902/902) so only the result *dtype* diverges (complex128 vs complex64 — issue #569). NumPy's actual per-loop precision, now reproduced exactly: * a REAL float32/float16 input to fft/ifft/irfft promotes real->complex128 => the DOUBLE loop => double transform rounded to the complex64/float32/float16 output (PocketFFTDriver.RoundInPlace); * rfft of a float32 real input, and fft/ifft/irfft of a complex64-precision N-D intermediate, hit a SINGLE loop -- but ONLY when the norm factor is a float (ortho/forward); backward/None's int fct=1 resolves NumPy to the double loop. Mirrored via a genuine single-precision pocketfft engine (PocketFFT.{Complex,Real,Bluestein,Plan}.Single.cs, CmplxF, TwiddleF.At -- the double twiddle table narrowed at lookup, matching sincos_2pibyn<float>'s Thigh==double), gated in Execute on `floatPrec && !effNormUnity && (complex operand || rfft-float32)`; * the norm factor is computed in the input's real_dtype (RawFft.FftFct: float32/float16), matching NumPy's reciprocal(sqrt(n, dtype=real_dtype)). floatPrec is threaded through the N-D compositions. Bonus (pre-existing bug found while doing this): the double Cfftp (and the new CfftpF) pass7/pass11 codelets applied special_mul to ca/cb instead of ca+-cb (the PM) in their ido>1 branch -- so np.fft.fft(float64, n) was SILENTLY WRONG for any n with a radix-7/11 factor at ido>1 (49, 98, 121, 143, 259, ...), latent because the fft.jsonl n-sweep was only {4,12,13}. Corrected to match pocketfft's PARTSTEP7/PARTSTEP11; float64 fft(n=49/98/...) now bit-exact again. Gate: * fft.jsonl regenerated 1796 -> 1864 (n-sweep now includes 49 and 121 to cover pass7/pass11 ido>1); * the fuzz harness now VALUE-verifies the float32/float16 cells instead of only excusing the dtype (FuzzCorpusTests.Kinds.IsFftFloatCell / FftFloatValuesMatch up-cast NumPy's complex64/float32/ float16 and bit-compare) -- teeth-checked: forcing a mismatch reds 540 cells; * new Fourier/np.fft.Float32Parity.Test.cs pins the pass7/pass11 fix against a codelet-independent direct DFT + the float32 dtype/round-trip invariants. * FuzzMatrix 104/104 tiers, 330 Fourier unit tests, fft.jsonl double bit-exactness all green. Docs updated: FFT_PARITY.md §6/§7 and CLAUDE.md (values now bit-exact; mechanism; pass7/pass11 fix) and the MisalignedRegistry F1 rationale (the previously-false "correctly-rounded double result" claim).
…s hardening) Validation of the float32/float16 FFT work confirmed pass7/pass11 were the ONLY codelets with the ca/cb-vs-ca±cb (PM) bug — pass3/4/5/8/passg and the real radfg/radbg are bit-exact at ido>1 (swept n=9/16/25/49/64/121/169/289 in double + float32/16). But that bug class (a codelet's ido>1 branch untested) is worth gating permanently: the differential corpus n-sweep now adds the perfect squares 9(pass3)/25(pass5)/49(pass7)/64(pass8)/121(pass11)/169(passg ido>1) — pass4 ido>1 is already hit by 12=4·3. fft.jsonl 1864 -> 2000 cases; every codelet ido>1 branch is now bit-exact-gated for all dtypes (float32/16 value-verified). FuzzMatrix 104/104, Fourier 341, all green. Docs counts refreshed.
…6x (2.9 -> 43 GFLOP/s) The managed float64 GEMM had no blocked/packed kernel: SimdMatMul.Double.cs carried only MatMulDoubleSimpleStrided, whose bStride1 != 1 branch (any transposed B — i.e. every dot(X, X.T), the exact product np.cov computes) is a pure SCALAR triple loop. Measured on 500x2000 @ 2000x500 (Release, best-of-5): transposed-B 362 ms (2.8 GFLOP/s) vs contiguous 58 ms (17.1 GFLOP/s) — while the float path, which has the BLIS-style blocked kernel with panel packing (SimdMatMul.Strided.cs), holds ~66-70 GFLOP/s in every layout. The double file's own header said the intended fix: "mirror SimdMatMul.Strided to add a full blocked double kernel; the packer design transfers 1:1". np.cov made transposed double matmul a hot path (413 ms for a 500x2000 input where NumPy takes 4.85 ms), so this commit does exactly that. Design — a 1:1 port of the float strided GEBP with Vector256<double>: - Micro-kernel Microkernel8x8Packed: MR=8 rows x NR_D=8 cols (2 vectors of 4 doubles) with 16 Vector256<double> accumulators + 4x k-unroll — the same register pressure the float 8x16 kernel (16 Vector256<float> accumulators) already proves works on AVX2. FMA when Fma.IsSupported, mul+add fallback. - Packers PackADoublePanelsStrided / PackBDoublePanelsStrided absorb all stride variation into contiguous [kc][MR] / [kc][NR_D] panels, with the same three fast paths as the float packers: PackA aStride0==1 (transposed-contig A, two 4-wide loads per k), PackB bStride1==1 (row-contig B, two 4-wide loads), PackB bStride0==1 (transposed-contig B, contiguous K-long column reads + scalar scatter). Everything else falls to scalar element access. Packing is O(M*K + K*N) vs O(M*N*K) GEMM — <3% of total work at size. - MicrokernelGenericDoublePacked handles partial edge panels (mr<8 / nr<8) with a 4-wide SIMD body and scalar remainder, mirroring the float generic. - Entry MatMulDouble now routes: all dims <= BLOCKING_THRESHOLD (128) -> the unchanged MatMulDoubleSimpleStrided (preserving the bit-exact accumulation order the small-products fuzz tier pins), else -> MatMulDoubleBlockedStrided. Blocking constants MC=64 / KC=256 / MR=8 shared with the float kernels; NR_D=8 is new (NR=16 is the float micro-kernel width). Numerics: the blocked accumulation order differs from the IKJ/scalar order — expected and accepted (the managed GEMM is already documented as not bit-exact vs NumPy's BLAS for large K; byte-parity at scale is the OpenBLAS interop package's job). Prototyped standalone and proven before landing: max relative error <= 2.8e-14 vs a naive scalar reference across 22 shape/layout cells — contiguous, transposed-A, transposed-B, both-transposed, general strided (::2 columns both sides) x shapes (137,129,311)/(256,256,256)/(8,8,1000)/ (3,5,200)/(500,500,2000) — exercising every packer branch, partial MR/NR/KC edge panels, and the k-remainder loop. Measured (Release, best-of-5, 500x2000 @ 2000x500, this host, AVX2+FMA): transposed B (the A@A.T / cov pattern): 362.3 ms -> 23.2 ms (15.6x, 43.1 GFLOP/s) contiguous: 58.3 ms -> 23.0 ms ( 2.5x, 43.4 GFLOP/s) transposed A: 55.3 ms -> 22.5 ms ( 2.5x, 44.5 GFLOP/s) End-to-end: np.cov on a 500x2000 float64 input drops 413 ms -> 37.9 ms (10.9x). NumPy's 4.85 ms remains faster (NPY/NS 0.13, up from 0.012) — the residual is multi-threaded hand-tuned BLAS, which NumSharp.Interop.OpenBLAS buys back (4.49 ms measured); this commit closes the managed single-threaded gap. Verification: - MatMul/Dot/linalg/Cov suites: 720/720 green; FuzzMatrix gate: 104/104 green (the small-exact products.jsonl cases stay on the untouched simple path). - Full CI-style suite (net10.0, no OpenBugs/HighMemory): 13,349 passed; the 2 failures (convolve buffer-release sweep, Char AsNumpyDtypeName audit) were re-run against the pre-change kernel and fail identically — pre-existing on this WIP tree, unrelated to this change. - New regression pins in MatMulStridedTests: double blocked-path view-vs-copy bit-equality for transposed-A (PackA fast path), contig@transposed-B (PackB bStride0==1, the cov pattern), offset 2D slice (general PackA + offset), and an independent exact-value oracle for large contiguous double GEMM against the INumber<long> integer kernel (small integer values, everything < 2^53, so the two disjoint code paths must agree bit-for-bit).
…vice + creation device=
Implements NumPy 2.x's Array-API `device` surface. NumSharp, like NumPy, is
single-device (CPU-only), so `device` is a validation/identity shim that lets
Array-API-generic code (`xp.zeros(shape, device=x.device)`) port unchanged.
Behavior probed 1-to-1 against NumPy 2.4.2.
ndarray surface (Backends/NDArray.cs, beside `mT`):
- `device => "cpu"` (plain string, matching NumPy's `L["cpu"]` stub).
- `to_device(string device, object stream = null)` returns `this` (identity, no
copy — NumPy `Py_INCREF(self)`) for "cpu". null device -> ArgumentNullException
(NumPy TypeError); non-null stream -> ArgumentException; other device ->
ArgumentException. NumPy's check order (null -> stream -> device value) is kept.
Creation `device=` on the canonical dtype-taking overload of the Array-API-blessed
constructors, via the new `np.ValidateDevice` helper (Creation/np.ValidateDevice.cs):
asanyarray, zeros, ones, empty, full, zeros_like, ones_like, empty_like, full_like,
arange, linspace, eye. (`asarray` already carried it.)
Two verbatim NumPy messages, DELIBERATELY quoted differently (ported from
array_api_standard.c vs conversion_utils.c):
- to_device : `Unsupported device: {d}. Only 'cpu' is accepted.` (single quotes)
- creation : `Device not understood. Only "cpu" is allowed, but received: {d}` (double)
Matched NumPy asymmetries (intentional — a ported call fails exactly where NumPy's
does): device= is NOT added to `array`, `ascontiguousarray`, `asfortranarray`,
`identity`, `frombuffer`, `fromfunction`. `__array_namespace__` is intentionally
omitted (no meaningful C# analog for the duck-typed Python-module mechanism).
Design notes:
- `device` rides only the dtype-taking overload per function (as a trailing
`string device = null`), never the bare `(Shape)` overload — adding a `string`
sibling there makes `zeros(shape, null)` ambiguous. Consequence: on
zeros/ones/empty the dtype must be passed to reach device.
- The pre-existing inline device check in `np.asarray` is left untouched; the new
sites use `np.ValidateDevice` with a byte-identical message.
- Additive only: existing call sites pass device=null, and ValidateDevice(null) is
a no-op, so no existing behavior changes.
Gate: test/NumSharp.UnitTest/Creation/np.device.Test.cs (14 tests) — .device=="cpu"
(incl. views), to_device identity/reject/stream/null ordering, and creation
device= accept/reject across every blessed constructor with the verbatim messages.
All green; full-solution build clean (no overload ambiguity at existing call sites).
…identical) Three per-element complex kernels in NDComplexMath.cs carried their NaN/Inf special-value if-chains INLINE on the finite fast path, burdening every ordinary (finite) input with dead branches and, in Acosh, re-evaluating the same predicate several times. Each is now the file's established hot-wrapper + cold-`*Special` idiom (as Exp/ExpSpecial, Cosh/Sinh/Tanh, Asin/Acos already are): one combined finite guard, with the rare corners hoisted into a NoInlining helper so the hot wrapper stays small and inlinable into the emitted IL unary kernels (these are invoked as direct per-element `call`s via ComplexAcosh/etc. MethodInfos in DirectILKernelGenerator, so callee branch layout is on the hot path). - Acosh: the original ran THREE sequential ifs that evaluated IsNaN(rx) twice and IsNaN(ry) twice on every call. Now a single `IsNaN(rx) | IsNaN(ry)` guard → cold AcoshSpecial; the finite path is one predictable branch + the general cacosh result. - Hypot (private; called per element inside complex Log/Sqrt/Log1p): replaced the leading 2x IsInfinity + 2x IsNaN guards with one `!IsFinite(x) | !IsFinite(y)` test (IsFinite is a single exponent-field compare) → cold HypotNonFinite, which keeps C99 hypot(+-inf,*)=+inf (even alongside NaN) else NaN. - Sqrt (npy_csqrt): the finite non-zero path walked four special branches (both-zero, IsInfinity(b), IsNaN(a), IsInfinity(a)) before CsqrtCore. Now one `IsFinite(a) & !IsInfinity(b)` guard (NaN b still flows into CsqrtCore -> (NaN, NaN) as before) with the both-zero singularity nested, else cold SqrtSpecial. SqrtSpecial also drops one branch: once IsInfinity(b) and IsNaN(a) are ruled out, entry required !IsFinite(a), so a is necessarily +-Inf -- the original's `if (IsInfinity(a))` was provably always true there. Behaviour is bit-for-bit identical to the prior logic (NaN sign bits included), verified by fully independent reimplementations of the ORIGINAL code (own Hypot + CsqrtCore, sharing nothing with the library) comparing raw DoubleToInt64Bits of both components across corner grids + wide random sweeps incl. THRESH-rescale huge, subnormal, +-Inf and NaN: Acosh 2,000,576 cases -> 0 mismatches Sqrt 3,000,729 cases -> 0 mismatches (exercises new Hypot via CsqrtCore) Log 3,000,441 cases -> 0 mismatches (isolates new Hypot via a second entry) Gates green: Core builds clean; 114 complex + inverse-hyperbolic unit tests and the fuzz specials/unary/unary_extra/aliasing/params tiers all pass. No numeric, API, or dtype behaviour changes -- pure hot-path restructuring.
… 8x unroll (measured) Follow-up to the blocked double GEBP kernel, answering two questions: does deeper k-unrolling help, and how do the remaining dispatch/packer paths perform. 8x k-unroll: REJECTED by measurement. A genuine straight-line 8x variant of Microkernel8x8Packed (two constant-offset 4-groups per iteration, verified correct to 2.8e-14 first) is 25% SLOWER than the committed 4x at every shape/layout tried — 500x2000x500 contig/transB/transA and 1024^3 contig/transB all land at 0.73-0.76x of 4x speed (43.8 -> 32.6 GF/s on contig). The loop overhead 8x would eliminate is already <2% of a 64-FMA body, while doubling the live b-vector/a-pointer window forces the register allocator to spill accumulators (16 YMM already fully committed) and doubles the loop-body I-cache footprint. 4x is the sweet spot, same as the float kernel. Recorded in the file header so it is not re-derived. Path sweep of the blocked kernel (500x2000x500, Release, best-of-7, this host): every packer combination is within ~4% of contiguous — contig 33.9, A@Bt 33.3, At@B 33.8, At@Bt 33.6, A[::2cols]@b 34.1, A@B[::2cols] 33.2, both-strided 33.3 GF/s; edge-panel-heavy 501x1999x503 holds 32.3-32.9 GF/s. The packing stage genuinely absorbs stride variation. Skinny shapes are lower-GF/s but bound by traffic, not dispatch: 8x2000x2000 ~9 GF/s (one MR panel reuses a 32 MB packed B), 2000x5x2000 6.7 GF/s (nr=5 generic micro-kernel only), 2000x2000x8 22 GF/s (C-traffic dominated) — absolute times 3-7 ms, acceptable. The sweep exposed one genuinely bad cell: mid-size transposed-B on the simple path. All dims <= BLOCKING_THRESHOLD (128) with bStride1 != 1 ran the simple path's SCALAR inner loop: 128^3 A@Bt at 4.6 GF/s (0.91 ms) while 129^3 — one step over the threshold — ran blocked at 31.6 GF/s (0.14 ms), a ~7x cliff. Crossover measurement (simple-scalar vs always-blocked, transposed-B, best-of-many): blocked wins from 16^3 up — 3.0x at 16^3 (4096 MACs), 4.8x at 24^3, 5.6-7.0x at 32^3-64^3, 5.9x at 128^3; worst mid-size cell 8x128x128 still 1.04x. Below ~4096 MACs the two pack-buffer AlignedAllocs (~0.5 us) dominate and simple wins (micro-dots, e.g. batched 4x4 stacks, stay allocation-free). Fix: MatMulDouble now takes the simple path only when (all dims <= BLOCKING_THRESHOLD) && (bStride1 == 1 || M*N*K < SCALAR_FALLBACK_MAX_WORK=4096). bStride1 == 1 mid-size keeps the simple SIMD path unchanged (no allocs, and no accumulation-order change for the committed corpus at those sizes). Result: 128^3 A@Bt 0.911 -> 0.126 ms (7.2x, 33.3 GF/s), 64^3 A@Bt 0.098 -> 0.017 ms (5.8x); all other cells unchanged. Corpus-safety note: every green float64 matmul corpus case is order-invariant by construction (its expectation is NumPy's output computed with a different summation order than any NumSharp path), and the prior commit already re-ordered every dim>128 case with the gate staying green; the sub-4096 floor additionally keeps every tiny corpus product on its committed path. Verified empirically: FuzzMatrix 104/104 green. New regression pin: Dot_Double_MidSizeTransposedB_BlockedReroute_ ExactVsInt64Oracle — 100^3 transposed-B (all dims <= 128, product >= 4096, so it rides the new route) with small integer values, asserted bit-equal to the independent INumber<long> kernel through the public np.dot API. Verification: MatMul/Dot/linalg/Cov suites + MatMulStridedTests (33) + FuzzMatrix — 826/826 green.
Introduces a full NumPy-aligned `np.correlate` API and refactors `convolve` to use a shared sliding dot engine that handles dtype promotion, mode parsing, complex conjugation/reversal rules, and contiguous materialization consistently. Adds `np.bincount` with unweighted and weighted paths, minlength support, integer/bool input validation, and NumPy-matching error behavior. Expands validation with new unit tests for correlate and bincount, updates fuzz op dispatch and corpora (`groupa`, `sort`), extends oracle generation for bincount cases, and adds interop LAPACK differential tests plus CLAUDE.md guidance updates.
Implements the eigenvalue family of np.linalg in NumSharp.Interop.OpenBLAS, reusing the existing LAPACK factorisation flow (Linearize/Delinearize into column-major buffers, per-batch loop, workspace hoisting) shared with solve/inv/det/svd/cholesky/qr. Verified BYTE-EXACT against NumPy 2.4.2. Routes: - eigh / eigvalsh -> LAPACK syevd (real symmetric) / heevd (complex Hermitian) - eig / eigvals -> LAPACK geev Design (mirrors NumPy's umath_linalg.cpp + _linalg.py split): - eigh mirrors svd: the engine returns the FINAL dtype (W = real basetype double, V = compute dtype; float32 computed in double then cast back), so the np.linalg.eigh/eigvalsh wrapper stays thin. - eig ALWAYS returns complex128 at the engine seam (a real matrix can carry a complex-conjugate eigenpair, so the result dtype is data-dependent). The real-output collapse (`not isComplexType(t) and all(w.imag == 0)` -> real, a GLOBAL reduction) and the result-width cast live in the wrapper (CollapseEig), exactly as NumPy's Python layer does. _assert_finite (AssertFinite, new) is eig/eigvals-only -- eigh/eigvalsh do not reject inf/NaN. - Real dgeev writes split real parts (WR/WI/VRR); the always-complex W/VR is assembled in managed code (AssembleGeevEigenvectors) exactly as NumPy's mk_complex_array / mk_geev_complex_eigenvectors, incl. the conjugate-pair reconstruction. zgeev writes complex W/VR directly (real rwork). jobvl='N' always; VL aliases VR's buffer as NumPy does. Native bindings (OpenBlasNative): dsyevd/zheevd/dgeev/zgeev added to the all-or-nothing BindLapack set + Unload clear + width-marshalled wrappers, ILP64 and LP64. Two char args (jobz+uplo / jobvl+jobvr) passed as byte*, hidden gfortran length args omitted (safe under cdecl, LSAME reads one char; same as potrf/gesdd). Trap: dgeev's LP64 signature has THREE doubles (wr,wi,vl) before the first int (ldvl), unlike zgeev's two. Completeness-audited against the full DOD variation matrix (differential vs NumPy 2.4.2): all 15 dtypes (int/bool -> float64, uint64>2^53 rounds identically, float16/decimal/char rejected with NumPy's verbatim "array type X is unsupported in linalg", error ORDER Half+NaN -> AssertFinite before CommonType); all layouts (C/F/strided/transposed/reversed/broadcast-batch/ newaxis/rank-4/simple-slice); edge matrices (0-d & 1-d errors, non-square, 1x1, 0x0, (0,3,3), (3,0,0), singular, defective [[1,1],[0,1]] with its 2.22e-16 eigenvector artifact bit-exact, repeated eigs, near-real docstring collapse, symmetric-through-eig, eigh(nan)->[0,-0] bit-exact, non-Hermitian one-triangle read, complex imaginary-diagonal ignored); 5x5 workspace scaling; eig==eigvals. ONE inherent non-invariant (not a bug): the complex-Hermitian eigenvector PHASE from zheevd is not reproducible ACROSS PROCESSES -- the same scipy-openblas binary returns +/- the same eigenvector column for the same input in different process invocations; numpy.linalg.eigh equals a raw zheevd only WITHIN a process. NumSharp returns raw zheevd output (byte-exact same-process, verified in the pythonnet harness). Eigenvalues are always exact and eigenvectors always valid. The interop gate therefore checks complex-Hermitian eigenvectors by the phase-invariant reconstruction A.V == V.diag(w) (as SVD checks its sign-ambiguous U/Vh); real-symmetric (syevd sign) and eig (geev canonical phase) ARE reproducible and are byte-compared. Documented dtype divergence: eig of a float32 operand with COMPLEX eigenvalues yields complex128 (NumSharp has no complex64); values identical. Perf is parity-bound (same native LAPACK both sides); the 1.5x DOD rule is for NumSharp's own kernels. Gates: LapackEigTests.cs (22, Inconclusive without LAPACK, reconstruction-based eigenvector checks) + EigLiveParityTests.cs (12 live pythonnet, byte-exact eigenvalues + reproducible eigenvectors, reconstruction for complex-Hermitian). Note: OpenBlasNative.cs is co-edited with a parallel Level-3 BLAS effort (trmm/trsm/symm/syr2k infra); this commit carries those native bindings as a side effect of the shared file. The Level-3 IBlasType surface (OpenBlasEngine.cs) and its gate are committed separately.
… over np.cov
Implement np.corrcoef with full NumPy 2.4.2 API parity. It is a faithful,
line-for-line port of numpy/lib/_function_base_impl.corrcoef — a thin wrapper
over the (dot/BLAS-powered) np.cov:
R_ij = C_ij / sqrt(C_ii * C_jj), then clip real (and, for complex input,
imaginary) parts to [-1, 1].
Signature: corrcoef(NDArray x, NDArray y = null, bool rowvar = true,
NPTypeCode? dtype = null) — matches NumPy 2.x exactly, which exposes ONLY
x/y/rowvar/dtype. The long-deprecated bias/ddof parameters were removed from
NumPy years ago and are deliberately NOT offered here.
Algorithm (port of NumPy's structure):
- c = cov(x, y, rowvar, dtype=dtype). Delegating inherits cov's validation and
its verbatim error taxonomy (ValueError "m/y has more than 2 dimensions"),
and its entire dtype/promotion behaviour — so corrcoef needs no dtype logic
of its own. Notably float32/float16 input (no dtype) yields float64 because
cov promotes via result_type(m, float64); an explicit dtype is honoured.
- Scalar-covariance branch: a single-variable 1-D input makes cov return a 0-d
scalar; np.diag raises on 0-d (NumPy's ValueError, NumSharp's ArgumentException),
and NumPy's `return c / c` gives 1 for a good value, nan for nan/inf/0. Ported
via try/catch around np.diag to mirror NumPy's control flow exactly.
- stddev = sqrt(diag(c).real); c /= stddev[:,None]; c /= stddev[None,:].
- Clip is done IN PLACE via the lane views, exactly as NumPy does: np.real(c) is
c itself for a real array and a write-through float64 lane view for a complex
array, so the clipped values land back in c either way.
One NEP50 subtlety handled: the clip bounds are cast to the lane's dtype. A C#
`-1`/`1` literal is a STRONG int32 scalar, which under NEP50 would promote a
float32 lane to float64 and then fail clip's same_kind out-cast; NumPy's Python
`-1`/`1` are WEAK ints that adopt the operand dtype — reproduced by casting, and
since ±1 is exactly representable in every float type the values are unchanged.
Parity: differential-tested against NumPy 2.4.2 across the variation matrix
(C/F/transposed/strided/negative-stride/sliced layouts, rowvar, y-combinations,
the scalar and constant-variable nan paths, empty (0,0) input, int/float32/
float16/float64/complex128 dtypes, and the [-1,1] clip). Bit-exact for
float64/complex128 within the managed GEMM's rounding (~1e-16), and within that
GEMM's ULP tolerance for float32/float16 (1-1.5 ULP) — the SAME parity profile
as np.cov, which corrcoef adds only elementwise diag/sqrt/divide/clip on top of.
Perf: GEMM-bound, inherited from cov (the wrapper's O(N^2) work over the N×N
matrix is negligible — measured ~1.00-1.33x cov). Like cov it is byte-identical
and BLAS-fast once NumSharp.Interop.OpenBLAS is referenced. Being GEMM-bound it
follows cov's gating precedent: a battle-test file, not the differential-fuzz
corpus (neither cov nor corrcoef is in OpRegistry).
Gate: Statistics/np.corrcoef.Test.cs (20 tests, from probed NumPy 2.4.2 output),
green on net8.0 and net10.0; no regression in the 34 np.cov battle tests.
Extend OpenBlasEngine's internal BLAS interface with trmm, trsm, symm, and syr2k for both float and double, wiring each call to the native OpenBLAS entry points. Add comprehensive Level3BlasBindingTests to validate these routines numerically against managed references across side/uplo/transpose/diag combinations, row/column-major layouts, padded leading dimensions, and degenerate quick-return cases, with inconclusive behavior when BLAS is unavailable.
Port of numpy.kron (numpy/lib/_shape_base_impl.py). Composes the same expand/broadcast-multiply/reshape structure NumPy uses, with a dispatched fast-path selection so NumSharp beats NumPy across the practical size range. Semantics (all probed against NumPy 2.4.2, bit-exact): - kron(a,b)[k0..kN] = a[i0..iN]*b[j0..jN], kt = it*st + jt; result shape (r0*s0, ..., rN*sN). Ranks need not match — the smaller is treated as if prepended with size-1 axes (NumPy's ndmin promotion). - b 0-d takes NumPy's multiply shortcut (a * scalar); both-0-d -> 0-d. - Result dtype follows multiply/NEP50 (int32*float64->float64, bool*int8->int16, complex/half/decimal preserved); result is a fresh, writeable, C-contiguous array. - Non-contiguous inputs (transposed / strided / F-order) are read C-order via reshape materialisation, exactly as NumPy does; empty and mixed-rank shapes match. Performance (NPY/NS, Release, best-of-warm): the direct broadcast-multiply's inner SIMD run equals the interleaved layout's coalesced contiguous stretch (b's trailing dims). For short runs on cache-resident outputs it loses its SIMD edge, so those route to a TILE fast-path (materialise repeat-a and tile-b, one SimdFull multiply) — bit-identical, and 2.2-2.6x on the common kron(big, 2x2) tensor-product pattern where direct alone sits ~1.4x. Tile is bounded to SIMD dtypes (Half/Decimal/Complex have no vector multiply, so it only adds passes) and to ~64 MB of result bytes (past cache its 3x traffic loses to direct's single pass). Measured: SIMD dtypes up to ~8M elements 1.9-6.5x; 1-D and small-a large-b 5-8x; very large short-run and scalar-path dtypes are memory/overhead bound and land nearer parity (still faster than NumPy). Verified bit-identical to NumPy 2.4.2 over a 38-case rank/layout/dispatch differential (ranks 1-4, C/F/transposed/reversed/strided, empty, both paths) plus scalar, complex, half, and NEP50 promotion checks. Tests: test/NumSharp.UnitTest/LinearAlgebra/np.kron.Test.cs (23).
Wires np.kron into the NumPy-oracle differential gate alongside the sibling
product ops. gen_products emits 80 kron cases — every product dtype (bool..complex128)
x {1d, 2d, 1d_2d, 2d_1d (ndmin promotion), 3d_2d, 0-d scalar_b} + an F-layout
non-contiguous operand — with valueclass "products". OpRegistry maps "kron" to
np.kron(ops[0], ops[1]).
kron values are plain a[i]*b[j], so the clean _mm_fill ramps map straight through
and are bit-exact for every dtype (integer/half products don't overflow at these
sizes). The Products [FuzzMatrix] gate passes: 367 cases total (was 287). Small
corpus sizes take the direct dispatch path; the tile fast-path is bit-identical and
pinned at size by the unit tests.
Committed index-only for gen_oracle.py so a concurrent session's unrelated
matmul-parity edits in the same file remain uncommitted; the corpus non-kron cases
are byte-identical to HEAD (verified, modulo the id renumbering from mid-stream
insertion).
…ty via OpenBLAS Widen the NumSharp.Interop.OpenBLAS parity backend from float32/float64 to complex128, and implement the five CBLAS product-gufunc seams (inner/vdot/vecdot/matvec/vecmat). Every product NumPy 2.4.2 routes through cblas is now (a) accelerated and (b) byte-identical to NumPy when the package is referenced. Complex64 (#569) stays out of scope — NumSharp has no complex64 dtype. WHY complex needs the BLAS path matmul.c.src's #USEBLAS = 1,1,0,0,1,1,... routes FLOAT/DOUBLE/CFLOAT/CDOUBLE through cblas. complex float accumulation is NOT associative, so — unlike integer/bool products (modular addition IS associative, bit-exact by construction) — a portable managed GEMM cannot reproduce NumPy's bits. The old CLAUDE.md claim that lumped complex with integers as "bit-exact by construction" was wrong and is corrected here. What NumPy actually does (ported route-for-route from src/numpy/) Two dot flavours are the whole story: * UNCONJUGATED zdotu_sub (CDOUBLE_dot, arraytypes.c.src) -> np.dot / matmul row.column, np.inner, np.matvec. * CONJUGATING zdotc_sub (CDOUBLE_vdot vdot.c / @name@_dotc matmul.c.src) -> np.vdot, np.vecdot, np.vecmat. mat@mat -> zgemm; a@a.T (shared data pointer) -> zsyrk (NOT zherk, matmul does not conjugate); mat.vec / vec.mat -> zgemv; np.dot scalar-multiply -> zaxpy. Complex vecmat CANNOT use gemv (the vector must be conjugated), so it is @name@_vecmat_via_gemm: a 1xMxN zgemm with CblasConjTrans on the vector. ABI details (differ from the real s/d routines) cblas z{gemm,gemv,syrk,axpy} take alpha/beta BY POINTER ({1,0}/{0,0}), not by value; the dots use the _sub out-pointer form (result via a trailing double*), never the by-value complex-struct return. System.Numerics.Complex is two interleaved doubles == BLAS complex*16, so a Complex* reinterprets to double* with no copy and a by-value Complex local hands its address over as alpha/beta. Implementation * OpenBlasNative.cs: ILP64+LP64 function pointers for zgemm/zgemv/zsyrk/zdotu_sub/zdotc_sub/zaxpy; BindComplexBlas binds them all-or-nothing behind IsComplexBlasLoaded (a bare real-only CBLAS declines complex cleanly rather than dereferencing a null pointer); Unload nulls them; typed Z* wrappers marshalling the BLAS int width per IsIlp64. * OpenBlasEngine.cs: ComplexBlas : IBlasType<Complex> (Gemm/Gemv/Syrk/Axpy/Dot via the z-wrappers, Dot mirroring CDOUBLE_dot's double-accumulated chunked zdotu). Added Dotc to IBlasType<T> — the conjugating dot behind vdot/vecdot/vecmat; for real dtypes conjugation is a no-op and NumPy uses the same @name@_dot, so SingleBlas/DoubleBlas delegate Dotc to Dot, and ComplexBlas.Dotc drives zdotc. IsSupported widened to Complex, gated on the complex products actually being loaded. ComplexBlas's Level-3 members (trmm/trsm/symm/syr2k) throw — no consumer, deliberately unbound. * OpenBlasEngine.Entry.cs: complex arms in TryDot/TryMatmul2D/TryMatmulBatched. * OpenBlasEngine.Products.cs (new): the five seams, each reproducing NumPy's exact gufunc route over a leading-axis broadcast driver (TryLeadingBroadcast stretches with stride 0). inner = dot on the swapaxes'd operand; vdot = flatten-both + conjugating dot; vecdot = per-inner dotc; matvec = gemv / per-row dotu; vecmat = complex gemm-ConjTrans or real gemv, else per-col dotc. Operands may arrive mixed-dtype (GufuncGuard.ToLoop only casts under an explicit dtype=), so each computes the common type and casts. * OpenBlasBackend.cs: overrides Try{Inner,Vdot,Vecdot,Matvec,Vecmat}; the float32/float64-only remark corrected to include complex128. The managed engine is untouched: with no backend installed the products still compute via the managed composition (the IBlasBackend invariant — a backend changes WHICH implementation runs, never WHETHER an answer exists). vecdot no longer takes the Multiply+ReduceAdd composition when a backend is present. Gate matmul_parity host-pinned tier 342 -> 589 cases: complex128 added to MATMUL_PARITY_DTYPES (with _mp_values fixed to draw genuine real+imag parts — a real-only .astype would zero the imag and never exercise zgemm/zdotc's cross terms), plus a _mp_prod_case block gating the five products under the backend-enabled tier across deep-K / F / stride2 / batched / broadcast / non-blasable layouts. The complex tier alone is 195 cases; the pre-existing float cases stay byte-for-byte identical. Floor bumped 342 -> 470. products.jsonl already carries the managed value-parity for these ops (small-exact); the backend byte-parity belongs in the host-pinned tier, which is where it lives. Verification: matmul_parity + products fuzz tiers green, full FuzzMatrix 104/104, MatmulParityBackendTests 30/30, NumSharp.Interop.UnitTests 402/402, full solution builds. Empirically confirmed 8/8 complex dot/matmul + 60/60 products (all five ops x f32/f64/c128 x C/F/broadcast/batched/non-blasable) byte-identical to NumPy 2.4.2 against the bundled scipy-openblas (sha256 74a40872..., byte-identical to numpy 2.4.2's own OpenBLAS).
…AS products Adds the LIVE half of the complex128 product parity that 6ee562d landed. That commit widened the NumSharp.Interop.OpenBLAS backend to complex128 dot/matmul + the five product gufuncs and gated it with the offline host-pinned matmul_parity corpus — a committed snapshot. This adds the strongest complementary gate: the interop suite's embedded CPython+numpy computing over NumSharp's ACTUAL zero-copy exported bytes, asserted byte-for-byte. No serialization sits between the two stacks, and parity is against the numpy actually installed, live. ProductsLiveParityTests (22 tests) — all byte-exact vs live NumPy 2.4.2 at threads:1. Coverage is built around the dotu/dotc conjugation split, which is where a complex-specific bug hides: np.dot * matrix.matrix -> zgemm * vector.vector -> zdotu (UNCONJUGATED — the discriminator vs vdot) * matrix.vector -> zgemv * >2-D -> the dotfunc (zdotu) tail, NOT gemm * a @ a.T -> zsyrk shortcut (symmetric A.A^T) np.matmul * 2-D -> zgemm * batched (stacked) -> one zgemm per element, plan hoisted * a @ a.T -> zsyrk shortcut * a @ conj(a).T -> zgemm, NO shortcut (conj is a fresh pointer) * non-blasable strided -> copy path (gh-23588), byte-identical through it * vector@matrix / mat@vec-> zgemv special cases * every layout -> C / F / reversed / transposed-view np.inner -> unconjugated (matrixproduct on the swapaxes'd operand) np.vdot -> zdotc (CONJUGATING), 1-D + flattened 2-D; plus a non-vacuity check that the conjugating vdot DIFFERS from the unconjugated dot on the same non-real operands (else every conj assertion is moot) np.vecdot -> zdotc, batched + leading-axis broadcast np.matvec -> zgemv / per-row zdotu (unconjugated), batched np.vecmat -> complex gemm with CblasConjTrans on the vector (gemv cannot conjugate), batched The five gufunc seams route through the backend for float32/float64 too (6ee562d rewired them; vecdot previously composed Multiply+ReduceAdd), so real seams are gated at both widths — byte-identical because the seam's double-accumulated chunked ?dot IS NumPy's @name@_dot / @name@_dotc for real dtypes. The operand builders give every element a non-zero, ASYMMETRIC imaginary part, so a dropped conjugation changes the answer — a real-only operand would make vdot == dot and the whole gate vacuous (the trap the corpus generator's _mp_values fix also guards). New public API: OpenBlasEngine.ComplexProductsAvailable — the mirror of LapackAvailable for the products (IsLoaded && IsComplexBlasLoaded). It is what the live gate keys off to go Inconclusive on a bare real-only CBLAS, where complex products fall back to the (correct but not byte-identical) managed kernel rather than the shared zgemm/zdotc — asserting bytes there would go red for the wrong reason. Exposing it publicly is necessary (OpenBlasNative.IsComplexBlasLoaded is internal and the interop test project is not a friend assembly) and useful in its own right (a user who binds a non-scipy CBLAS can query complex-product parity the same way they query LAPACK). Why byte-exact holds (InteropTestBase CLAUDE.md section 4): both stacks call the same bundled scipy-openblas build, at threads:1, over matrices small enough to stay single-threaded on both sides, in one process on one CPU (same DYNAMIC_ARCH micro-kernel). Complex float accumulation is not associative, so this shared z-BLAS route is the only one that reproduces NumPy's bits.
…shipped The CLAUDE.md "BLAS/LAPACK API surface" section still described the LAPACK factorisations as an unimplemented NSE-throwing shell and the matrix products as float32/float64-only. Both are stale: complex128 dot/matmul + the five product gufuncs (6ee562d) and all ten LAPACK factorisations (solve/inv/det/ slogdet, cholesky/qr, svd/lstsq, eig/eigh via f5ec627 and siblings) now ship in NumSharp.Interop.OpenBLAS and are byte-identical to NumPy. Surgical edits to the section (the interop scope note at L141, the matmul_parity count at L218 and the Products table row were already refreshed by the products work; this finishes the factorisation/LAPACK side): - Header: 'implemented vs. NSE-throwing shell' -> 'fully implemented via OpenBLAS (NSE only for factorisations with no backend)'. - Thesis sentence: the two-way split is now explicitly about whether the numerics exist WITHOUT a backend (with the OpenBLAS package referenced, both rows compute). - Table column header: 'With no backend installed' -> 'Behaviour (installed backend vs none)', since both cells now describe both cases. - Factorisations row: adds the with-backend behaviour mirroring the Products row — all 10 compute through LAPACK (gesv/getrf/potrf/geev/syevd/heevd/ gesdd/geqrf/orgqr/gelsd) for float32/float64/complex128, float32 upcast to double per _commonType, complex via the z-routines, the 10 Try* overridden in OpenBlasBackend. - Seam note: records that OpenBlasBackend now overrides all 15 Try* (5 products + 10 factorisations) while the interface defaults stay false so a bare/real-only CBLAS backend still compiles. - Validation note: drops the 'does not move when the numerics land' future tense now that they have landed. - LinAlgEngineSeamTests description: reframed from a 'delete a case as each implementation lands' checklist to the permanent no-backend contract (Core alone has no LAPACK, so factorisations still raise NSE there); the numerics are gated by the interop package's live-parity tests. Note: LinAlgEngineSeamTests.cs carries a matching stale comment ('pending numerics — remove a case when its implementation lands') and a keep-vs-delete intent tension; left to the owning OpenBLAS session to resolve.
… route)
np.einsum now COMPUTES. Previously the subscript parser resolved the output
shape and the engine threw NotSupportedException; the contraction kernel was
declared out of scope. This lands the contraction as a composition over the
existing matrix products, so it "works via OpenBLAS like other functions" —
with no new backend seam.
Design — a port of NumPy's OWN optimize= path (numpy/_core/einsumfunc.py:
bmm_einsum + _parse_eq_to_batch_matmul), NOT the C c_einsum iterator kernel:
* Each pairwise contraction is classified (batch / contracted / a-kept /
b-kept / summed / diagonal) from the operands' ACTUAL shapes, so size-1
broadcasting is handled exactly as NumPy's does.
* The contraction itself is ONE np.matmul (2-D gemm, or batched when there
are shared output indices), or a broadcast np.multiply when nothing is
contracted (outer / Hadamard / scalar).
* A single-term einsum prepares each operand first — diagonal-collapse via
np.diagonal, sum-out via a dtype-FORCED np.sum, then transpose.
* N operands fold left-to-right; the final step targets the real output term.
Because the products land on TensorEngine.Matmul, which already dispatches on
Blas, referencing NumSharp.Interop.OpenBLAS makes the float32/float64/complex128
contractions byte-identical to NumPy, and without it they fall to the managed
GEMM — exactly as np.matmul itself behaves. There is deliberately no TryEinsum
seam: einsum reaches a backend the indirect way np.tensordot / multi_dot do.
Parity (probed vs NumPy 2.4.2, ~307 differential cases + in-process
self-consistency):
* Integer and boolean contractions are byte-exact everywhere (modular /
logical reduction is order-independent).
* Every product-shaped case (matmul, batched, matvec, ij,kj->ik, tensor
contractions, inner/outer/kron) reduces to np.matmul / np.multiply
bit-for-bit — verified einsum(...) == the direct product in-process across
f64/c128 — so it inherits their OpenBLAS-gated parity.
* A pure FLOAT summation outside a product (ij->i, diagonal-then-sum, or
across 3+ operands) can differ in the last ULP (order follows np.sum + the
left-to-right fold); those stay allclose.
* einsum PRESERVES the operand dtype like NumPy (einsum('ij->i', int32) is
int32, not np.sum's widened int64); two-operand promotion is result_type;
complex is UNCONJUGATED (unlike vdot); all 15 dtypes contract.
* dtype= forces the accumulation dtype under the casting rule; out= / order=
honoured; optimize= validated but numerics-neutral (np.einsum_path stays
out of scope).
Implementation:
* Backends/TensorEngine.Einsum.cs (new) — the whole composition; base virtual
Einsum parses (EinsumSubscripts.Parse, unchanged validation + verbatim
NumPy error text) then contracts.
* LinearAlgebra/EinsumSubscripts.cs — added BuildTerms(): renders the sbyte
label plan into concrete term strings, ellipsis dims -> reserved U+E000+slot
chars (no 52-letter ceiling; never collide with ASCII labels).
* LinearAlgebra/np.einsum.cs — delegates to the engine (single parse); dropped
the pre-parse and the outputShape seam parameter.
* Backends/TensorEngine.LinearAlgebra.cs — removed the throwing Einsum stub.
Tests:
* LinearAlgebra/EinsumContractionTests.cs (new) — value parity vs NumPy 2.4.2
across dtypes / matmul / transpose / diagonal / trace / reductions / outer /
inner / batched / ellipsis / kron / bilinear / implicit-ordering / dtype
preservation / promotion / bool / complex / out= / dtype= / sublists.
* EinsumSubscriptParityTests — Contracts() now asserts the computed shape
instead of reading the removed NotSupportedException message; the kernel
test became a value assertion.
* LinAlgEngineSeamTests — einsum moved from the pending-numerics region to a
permanent test (it computes without a backend, like the products).
All 77 einsum tests pass (net8.0 + net10.0); full LinearAlgebra suite (302)
green; whole suite green apart from two PRE-EXISTING reds unrelated to einsum
(T1_33 Char dtype-name audit; convolve buffer-release sweep).
…no hardcoded surfaces
The inventory tool's module list was a hardcoded typeof(...) sequence — the exact
design that originally hid the whole np.fft and np.linalg surfaces (reported 0.0%
and 19.4% while both were fully implemented). Discovery is now data the types
declare about themselves.
Core: new [ModuleName] attribute (Assembly/ModuleNameAttribute.cs) marks a public
type as the C# host of a NumPy module surface. Annotated: np ("np"), NDArray
("ndarray"), NumPyRandom ("np.random"), FourierModule ("np.fft"), nested
np.linalg ("np.linalg"). Inherited=false is load-bearing: NDArray<T> derives from
NDArray and must not be swept up as a second ndarray host. Single-object DSL
exports (r_/c_/s_/index_exp/mgrid/ogrid) deliberately take NO attribute — NumPy
exports each as ONE object, so the np property is already the whole coverage row.
Tool (schemaVersion 2): scans GetExportedTypes() for [ModuleName], errors on
duplicates and on an empty scan, derives BindingFlags from the type shape (static
class -> Static, facade -> Instance), and emits a modules{name -> TypeInventory}
map ordered ordinally. Dictionary keys bypass the camelCase policy so dotted
module names survive verbatim.
Generator (1.3.0): consumes modules mechanically — surface = np.-prefix suffix
rule ("np.random" -> "random"), target prefix = the module's own CLR type name
with nested '+' normalized ('NumSharp.np+linalg' -> 'NumSharp.np.linalg'),
SourceLocator patterns built from the simple class name. direct_target /
auto_alternative take the derived prefix map; the per-surface hand tables are
gone. New guard: any compared NumPy surface without an annotated host is a hard
SystemExit naming the surface — deleting an annotation can no longer silently
zero a surface back to all-missing (negative-tested: removing np.linalg from the
inventory fires the guard).
Miss-hunt (four independent techniques, all clean — 5 modules is the complete set):
1. Whole-assembly reflection sweep: every exported type + all public members
dumped; every NumPy-name hit for the five in-scope surfaces lives on an
annotated type (found-ONLY-on-unannotated = 0 for np/ndarray/random/linalg/fft).
2. Full NumPy 2.4.2 submodule cross-check (strings/char/ma/testing/polynomial/
emath/rec/dtypes/exceptions/lib/ctypeslib): no NumSharp module hosts for any of
them; the sole stray hit is numpy.strings.index colliding with the iterator
cursor property 'index' on Broadcast/FlatIterator/NDIterator — name
coincidence, not a module.
3. Nested-type sweep: the only module-shaped nested type under np is linalg;
the rest are result structs and iteration-protocol classes (NumPy CLASS
exports, one row each).
4. Facade-property sweep: the only properties returning function-namespace types
are np.random and np.fft — both annotated. Zero unannotated exported types
declare >=3 numpy-named methods.
Regenerated artifact (--check passes): headline 76.8% -> 79.3% (444/560) — the
delta is the features landed since the last regen (device/to_device, arcsinh/
arccosh/arctanh + asinh/acosh/atanh aliases, bincount, corrcoef, kron), with
np.fft 18/18 and np.linalg 31/31 unchanged at 100%. Targets verified
byte-identical to the hardcoded-prefix era (NumSharp.FourierModule.fft,
NumSharp.np.linalg.solve, NumSharp.NumPyRandom.uniform, NumSharp.NDArray.reshape).
README: documents the discovery contract and the no-attribute rule for
single-object DSL exports.
…s, parse cache + layout parity Closes the /np-function einsum cycle on top of 7d2d7a2 (the contraction): every remaining NumPy 2.4.2 semantic was probed, matched, and gated, the memory-layout variation matrix was differentially verified, and the required benchmarks were run (Release, best-of-7, vs NumPy's DEFAULT optimize=False c_einsum and vs optimize=True). Semantics landed (all probed against 2.4.2): * THE VIEW PATH — one operand, no out=, nothing summed. NumPy answers with a VIEW: einsum('ii->i', a) is WRITEABLE (a[:]-style writes set a's diagonal; np.diagonal's own read-only contract is restored to writeable by einsum), a broadcast operand's view stays read-only, and einsum('ij->ij', a) is a DISTINCT view object, never `a` itself. On this path NumPy ignores order= ENTIRELY (order='C'/'F' still return the non-contiguous diagonal view) and DISCARDS a dtype= request (einsum('ii->i', int32, dtype=int64) is the int32 view) — both reproduced. Implementation trap worth the note: Storage.Alias() inherits read-onlyness from the storage it aliases, so the writeable restore must alias from the OPERAND's storage, not the diagonal view's — aliasing the view's storage silently re-clears the bit. * casting= now gates EVERY cast with NumPy's iterator texts verbatim: operand -> loop dtype ("Iterator operand {i} dtype could not be cast from dtype('...') to dtype('...') according to the rule '...'"; what makes casting='no' reject mixed dtypes) and loop -> out dtype ("Iterator requested dtype could not be cast from ..., the operand {nop} dtype, ..."). A same-rank out= extent mismatch stays NumSharp-worded (NumPy leaks NpyIter's "remapped shapes" text there — same divergence class as the label-extent mismatch, documented). * order= — 'A' AND 'K' both produce F-contiguous results when EVERY input is F-contiguous (probed: all-F matmul/hadamard/chain3 is F even at the default 'K'), C otherwise; 'K' leaves the computed C result untouched. * Grammar cache — EinsumSubscripts.Parse split into a cached immutable grammar (key = subscripts + operand ranks, 4096-entry ConcurrentDictionary — the analog of NumPy's lru_cache(2**12) on its equation parse) and a per-call ValidateExtents that never writes the shared instance; the new EinsumPlan struct carries the per-call SingleOperandView. Thread-hammered: 20K concurrent mixed valid/invalid calls, 0 errors, 0 wrong results. Verification: * 186-case memory-layout differential vs NumPy 2.4.2 — F / transposed / strided / negative-stride / negcol / offset / broadcast recipes applied to either operand x matmul / reduce / diagonal-view / trace / batched (incl. swapped-last-axes and stride-0 batch) / ellipsis / permuted-output / frobenius / outer + empties ((2,0)@(0,3), zero-size reduce, (0,0) diagonal), rank-5 permutations, scalars — 186/186 BIT-exact. * +10 unit tests (87 einsum tests total, net8.0 + net10.0 green): view writeability/write-through/identity-distinctness/broadcast-read-only, view-ignores-order-and-dtype, casting='no' verbatim, out-cast verbatim, all-F order matrix, empties, strided-out write-through, non-contiguous operand pins. * Full suite green apart from the two PRE-EXISTING unrelated reds (T1_33 Char dtype-name audit; convolve buffer-release sweep). Perf (NPY/NS vs NumPy's DEFAULT einsum, Release, best-of-7): * With NumSharp.Interop.OpenBLAS referenced: matmul 1024^2 f64 23x, 256^2 5.6x, 3-op chain 64^2 146x (NumPy's default 3-op einsum is the naive 4-label iterator), batched 128x16^2 2.0x, matvec 512^2 1.9x, hadamard 1M 3-4x, complex128 64^2 2.9x, outer ~1.1x. * Core-only: the product cells fall to the managed GEMM exactly as np.matmul does (1024^2 still 2.9x; matvec 0.21x is the managed product kernels' own floor — np.dot(m,v)/np.matvec measure the same 187us). * Sub-1x cells are inherited or fixed-cost floors, documented: sub-5us view calls ~0.5-0.9x vs c_einsum's single C call (1.9-5.9x in OUR favor vs NumPy's own optimize=True), big ij->i ~0.8x (the shared axis-sum's parity), int32 GEMM ~0.5x (kernel class; NumPy's own np.matmul(i32) is slower than its einsum). einsum-specific overhead ~2us/call, <1% by 64^2. BLAS-bench trap encoded in the scripts: a ProjectReference alone does NOT engage the backend in a file-based app — the interop assembly loads lazily, so its [ModuleInitializer] only runs once something touches it (e.g. reading OpenBlasEngine.Enabled); an untouched reference silently benchmarks the managed kernels.
… structurally hard Four guards + per-member static tracking close the residual ways a scan could miss public API, following the [ModuleName] discovery redesign. Each guard turns a silent-gap failure mode into a loud error at generation time. Tool (schemaVersion 3): - Hierarchy guard: annotated types are reflected DeclaredOnly, so their base must be object/ValueType or itself annotated — growing a facade a base class errors instead of silently hiding every inherited public member. - Facade-shape gate: a property on an annotated host returning a concrete assembly class with >=8 NumPy-style lowercase instance methods (the FourierModule shape np.fft was originally missed by), or a public nested static class with any lowercase static methods (the np.linalg shape), must itself be [ModuleName]-annotated. Keyed on lowercase function names so C# infrastructure (UnmanagedStorage ~50 PascalCase methods, abstract TensorEngine, FlatIterator's lone copy(), the DSL indexer classes) never false-positives — verified clean on the real tree. - Dual-flag scan: members reflected Static|Instance always, per-member static:bool recorded — a public static helper on an instance facade can no longer escape an instance-only scan. Surfaces NDArray's 5 statics (Scalar/FromMultiDimArray/AsString/AsStringArray/FromString) as NumSharp-only catalog rows (126 extensions, headline unchanged 79.3%). - unannotatedSurface index: every OTHER exported type's public member names (286 types) — the data behind the generator's stray-host gate. Extension-method hosts are exported static classes, so they land here by construction. Generator (1.4.0): - Stray-host gate: an in-scope NumPy export left 'missing' whose name exists on an unannotated type fails the run naming the candidate hosts — implemented-but- unscanned can no longer masquerade as a genuine gap. Reviewed name coincidences live in overrides.json "stray_allowlist" (validated against discovered ids like aliases/support are). - Stale-alias warning: an override alias whose export now matches directly is reported for deletion. Both mechanisms fired real findings on first run against the live tree: numpy.random's alias was stale (module row resolves directly to the np.random property — deleted), and numpy.ndim tripped the stray gate — reviewed as a name coincidence (Broadcast/NDIterator/UniqueResult carry an ndim RANK PROPERTY, not the unimplemented top-level np.ndim(a) helper) and recorded as the first stray_allowlist entry. Negative-tested via the importlib harness: stray gate fires naming the fake host, allowlist suppresses it, unknown allowlist ids are rejected, and the unbacked-surface guard still fires under schema 3. Tool-side guards run on every generation and pass the real tree. --check green; headline 79.3% (444/560) unchanged — these gates change failure modes, not counts. README documents the four-guard stack and the stray_allowlist contract.
…locked GER + blocked LU via SimdMatMul
Reworks ManagedLu's double hot loops from the generic System.Numerics.Vector<double>
(mul-sub, no FMA, no register-blocking) to hand-written Vector256<double> + FMA
kernels the same shape as SimdMatMul's micro-kernel, and routes large double
factorisations through a BLOCKED LU whose Schur update rides the cache-tiled
SimdMatMul GEMM. Small/complex paths and every value contract are unchanged.
The double SIMD kernels are shared statics (GerDouble / AxpyNegDouble / DivRowDouble)
used by DoubleOps AND the blocked path:
- GerDouble — the O(m³) rank-1 elimination as a REGISTER-BLOCKED GER: four trailing
rows per pass, the pivot-row vector loaded once and reused, four independent
Fma.MultiplyAddNegated chains. seg-parameterised by colEnd so it serves both the
full unblocked update and a blocked panel.
- AxpyNegDouble — the substitution's row updates, 4x-unrolled FMA axpy.
- DivRowDouble — Vector256 divide.
All subtract-accumulate goes through one FmaSub helper (Fma.MultiplyAddNegated /
Math.FusedMultiplyAdd where available, else mul-sub) so the vector body and scalar
tail always agree; the arithmetic matches scipy-openblas' own FMA getrf kernels
(still allclose to NumPy, and Det_OneByOne / the [[1..9]] divergence are unchanged
since 1x1 has no GER and [[1..9]] still lands ~1e-16).
Past BlockedThreshold=256 the double factorisation takes FactorDoubleBlocked —
LAPACK's getrf structure: panel getf2 (partial pivoting over the full column, swaps
confined to the panel), ApplyPanelPivots (laswp to the rest), TrsmLowerUnitDouble,
then A22 -= A21·U12 via SimdMatMul.MatMulDouble into a temp + a fused subtract. The
dispatch is gated `typeof(T)==double && Vector256.IsHardwareAccelerated`, a JIT-time
constant per instantiation (elided entirely from the Complex kernel). Same partial-
pivoting sequence as unblocked, so identical `info`; values differ within tolerance
(the GEMM accumulates blocked). Validated against NumPy 2.4.2 at n=300 (slogdet/solve/
inv match to ~13 digits) and by reconstruction A@inv≈I (max 4e-15), A@x≈b (max 5e-13).
Measured perf reality (NPY/NS, Release, threads=1), reported honestly:
- Small win big and are UNCHANGED (n=4 det 3.3x/inv 4.3x/solve 4.7x, n=16 ~2.5x):
they are dispatch-overhead bound, so SIMD on the tiny inner loops is a wash.
- n=64 ~parity (det 1.11x). n=256 ~0.4-0.6x. n=512/1024 (blocked) ~0.33-0.55x.
- The register-FMA alone moved large only ~5% (the unblocked GER is cache-BANDWIDTH
bound, not compute); the blocked path is neutral at 256 (fits L3), ~1.05x at 512,
~1.3x at 1024 vs unblocked — but stays below NumPy because SimdMatMul's managed
dgemm is slower than the OpenBLAS dgemm NumPy's getrf calls. That is the same
managed-vs-native GEMM ceiling as the products; no managed kernel closes it.
So the threshold is set where blocking helps (256² unblocked 681us beats blocked
703us — keep <=256 unblocked), and the change delivers the house SIMD style + the
SimdMatMul specialized-path reuse without regressing any size.
Tests: ManagedLuTests gains Blocked_LargeMatrix_FactorsCorrectly (n=300 > threshold,
reconstruction + slogdet). Full NumSharp.Tests suite green (14174 pass). CLAUDE.md +
the ManagedLu remarks updated to describe the Vector256/FMA/blocked architecture and
the honest perf ceiling.
…, NumPy 2.4.2 parity
Adds the ufunc buffer-size getter/setter pair (numpy._core._ufunc_config), byte-exact
with NumPy 2.4.2.
- np.getbufsize() -> long : current default NDIter/ufunc buffer size (8192 = NPY_BUFSIZE).
- np.setbufsize(size) -> long : validates, sets, returns the previous size.
Validation order + messages ported verbatim from umath/extobj.c (raised as ValueError):
size < 0 -> "buffer size must be non-negative"
size > 10000000 -> "Buffer size, {n}, is too big"
size < 5 -> "Buffer size, {n}, is too small"
size % 16 != 0 -> "Buffer size, {n}, is not a multiple of 16"
Valid values are therefore the multiples of 16 in [16, 10000000]. NumPy's float/bool
TypeError cases are handled for free by C#'s type system (a non-integer cannot reach a
`long` parameter).
Storage is a [ThreadStatic] field on NDIterBufferManager (CurrentBufferSize, 0-sentinel
-> 8192), mirroring NumPy 2.x's context-local extobj: a setbufsize on one thread never
leaks into another and a fresh thread sees the 8192 default. Wired functionally at the
single canonical NDIter default-resolution point (NDIter.cs), so buffered iteration honours
the setting; an unset thread resolves to 8192, byte-identical to before. Buffering is a
chunking knob only — every computed result is bit-for-bit unchanged (verified: a buffered
cast is identical at bufsize 16 vs 8192).
Scope: only the getbufsize/setbufsize pair. The rest of the _ufunc_config family
(seterr/geterr/seterrcall/geterrcall/errstate) governs floating-point error MODES, which
NumSharp does not model (it raises no FP RuntimeWarning/FloatingPointError), so they would
be misleading no-op stubs; bufsize is the only member with a genuine functional home.
Classified in OracleSurfaceCoverageTests.SiblingOwned (config, no deterministic corpus
bytes to bit-compare, same as get/set_printoptions), gated by
test/NumSharp.Tests/APIs/np.bufsize.Test.cs (23 tests, all from NumPy 2.4.2 output:
round-trip return values, verbatim validation, value-invariance, thread-locality, and the
functional NDIter wiring).
Operators created transient NDArrays that were left to the finalizer instead of being returned promptly to the buffer pool. Each operator was probed with the SizeBucketedBufferPool take/return balance (the ScopeAudit methodology): - operator ! (NDArray.NOT.cs) was the ONLY bucketed-buffer strand: it builds an untyped bool `result`, then returns result.MakeGeneric<bool>() — a typed alias that takes its own ARC ref on the shared block and orphans `result` (one bucketed buffer escaped per `!arr` call, measured). Now [NDScoped]: the weaver disposes `result` at exit while yielding the alias (escaped 1 -> 0). Same MakeGeneric strand + fix the typed NDArray<T> &/|/^ operators already carry. - The 30 `object` overloads (a+obj, a&obj, a==obj, a<<obj, ... across Primitive/AND/OR/XOR/Shift/Equals/NotEquals/Lower/Greater) mint an np.asanyarray(x) leftover for a scalar/array-like operand (and the comparison overloads also strand a Scalar/empty MakeGeneric wrapper on their null/empty branches). All now [NDScoped]. A plain `using var t = np.asanyarray(rhs)` would be a BUG here — asanyarray returns the SAME array when rhs is already an NDArray, so `using` would dispose the caller's input (rule R2) — which is why [NDScoped] (tracks only freshly-constructed temps, never an input passthrough) is the correct instrument. Silences all 28 NDW012 analyzer warnings in the operator files (215 -> 185 total NDW012 sites; 0 remaining in operators). - The core NDArray x NDArray operators (+ - * / %, & | ^ << >>, == != < > <= >=, ~, unary -/+) are DELIBERATELY left unscoped: probed escaped=0 (the engine returns the result directly and disposes its own scalar-cast temp; the comparison engine returns a bool-typed result that AsGeneric passes through). The disposal guideline keeps [NDScoped] off this hot binary path. `a &= b` (both NDArray) has no operator-frame leftover — the old `a` is the caller's (it may be aliased elsewhere) and is uncatchable by the operator, per the two-audience reclamation contract (DISPOSAL-GUIDELINES section 10). Verification: 31 operators confirmed woven (NDScope local present, 0 attr/local mismatches); NDScopeWeaveTests + 162 operator behavior tests green (values unchanged — the weave is byte-neutral); new regression pin UndisposedIntermediateTests.OperatorNot_ReclaimsUntypedMakeGenericWrapper. The scope-audit Corpus_AllOps failure (inv/det/slogdet/solve on linalg_parity) is PRE-EXISTING on journey3 (identical with these 10 source files stashed) — the managed LU family, not operators.
…-set + boundary values Completeness follow-up to the np.getbufsize/np.setbufsize implementation, after a 28-case differential sweep confirmed byte-identical parity with NumPy 2.4.2 across the whole validation surface (negatives, too-small, non-multiple-of-16, every valid multiple of 16 in [16, 10000000], too-big, and 10**18). Adds the semantics the first suite did not pin: - SetBufsize_Chained_ReturnsEachPreviousValue: setbufsize returns the value in effect BEFORE the call (16->32 returns 16, not always the 8192 default). - FailedSet_ReturnsNothing_AndKeepsPreviousOnNextSet: a rejected set never becomes the "previous" value reported by the next successful set. - Two more valid DataRows (48, 9_999_984) proving an arbitrary multiple of 16 round-trips through getbufsize verbatim, with no rounding, including near the top boundary. Test-only; no library change. Suite now 28 cases, all from NumPy 2.4.2 output.
…d (NDW013) Two build-time diagnostics for [NDScoped] memory reclamation, delivered by the NumSharp.Weaver.Analyzer project and wired into NumSharp.Core's own build. This commit also introduces the analyzer project to source control (it carries the pre-existing [NDScoped]-target gate NDScopedTargetAnalyzer + KnownTypes/TypeHelpers); the NEW work here is the leak analyzer and the weaver-missing guard. NDW012 (Roslyn analyzer, Warning) - NDArrayLeakAnalyzer flags an NDArray a method CREATES but never returns, out/ref-assigns, stores, disposes, or yields to an NDScope (a transient left to the finalizer instead of the pool). An IOperation flow analysis classifies every owned-NDArray value as Reclaimed (using / .Dispose() / scope.Returns / NDScope.Attach), Escapes (return / out / ref / store / handed to a non-NumSharp API / yield / await), or Leaks (dropped result, dead local, or fed as an argument into an np.* op that returns an NDArray - those never dispose their inputs). Recognises every carrier: NDArray, NDArray[], ValueTuple/Tuple of NDArrays, and INDArrayCarrier result structs, incl. `var (q, r) = ...` deconstruction (a declaration reference must not count as a use, or the unused side is masked). Methods that are [NDScoped]/[NDScopedAsync] or open an NDScope by hand are exempt. A receiver of a call/property/indexer FOLLOWS the result upward rather than terminating as a leak, so NumSharp's pervasive fluent reinterpret/view chains (x.MakeGeneric<T>(), x.reshape(...)) are not flooded with false positives. NDW013 (MSBuild warning) - src/NumSharp.Core/build/NumSharp.targets warns when a project USES [NDScoped]/[NDScopedAsync] but the NumSharp.Weaver package is not installed (or -p:SkipNDScopeWeave=true): the attributes are then inert and the temporaries leak. It ships in NumSharp itself (not the weaver package) precisely so it can fire when the weaver is ABSENT - the attributes live in NumSharp, so the guard is present exactly when they can be used. The weaver's targets set $(NumSharpWeaverActive)=true so it stays silent when weaving actually runs. Core wiring: NumSharp.Core.csproj references the analyzer with OutputItemType="Analyzer" (gated by -p:EnableNDArrayLeakAnalyzer, default on) so `dotnet build` on NumSharp captures leaks in np.* code; src/NumSharp.Core/.editorconfig silences the redundant [NDScoped]-target gate on Core (already covered by the self-weave), leaving only the NDW012 leak warning; build/NumSharp.targets is packed into the NumSharp nupkg so downstream consumers get NDW013. Known limitation (documented): the analysis is per-method, so a private helper whose temps are actually reclaimed by a caller's ambient [NDScoped] scope is still flagged in isolation - scope the boundary, turn NDW012 down in .editorconfig, or disable it on Core with -p:EnableNDArrayLeakAnalyzer=false.
…ixtures + CI Asserts the NumSharp analyzers (NDW012 leak + the NDW002/003/005/006/009/010/011 [NDScoped]-target gate) fire on exactly the intended lines, and gates NDW013 with a real build. NumSharp.Analyzer.Fixtures (inner csproj) - files crafted to trigger each diagnostic, every expected line tagged `// [NDWxxx]`. LeakScenarios.cs: 6 NDW012 leaks + 11 clean egress cases that must NOT warn (returned / using / Dispose / hand-scoped / Attach / out / field / foreign sink / alias / view chain / [NDScoped]). CarrierScenarios.cs: the carrier variations - dropped tuple, deconstruction with an unused side, dropped NDArray[], dropped INDArrayCarrier. GateScenarios.cs: the gate codes. It applies the leak analyzer (with a local .editorconfig silencing the gate ERRORS) so `dotnet build` on it genuinely emits its own NDW012 warnings while staying green. NumSharp.Tests.Analyzer (MSTest v3, net8.0 + net10.0) - runs both analyzers IN-PROCESS over the fixture files (Roslyn CSharpCompilation from framework refs + NumSharp.dll -> WithAnalyzers().GetAnalyzerDiagnosticsAsync()) and asserts diagnostics match the tags EXACTLY (a missing warning AND an unexpected one both fail; multiset compare). Includes harness self-tests (a real leak warns, clean code does not, broken source is reported, [NDScoped] is exempt), non-vacuity floors, per-gate-code presence checks, and a build-based NDW013 test (categorised AnalyzerBuild; spawns `dotnet build` on a generated project that imports Core's build/NumSharp.targets, asserting the warning fires without the weaver and is silent when NumSharpWeaverActive=true). Writing the suite caught and fixed a real analyzer bug: `var (q, r) = expr; return q;` was not flagging the unused r, because a deconstruction target's local references (DeclarationExpression -> Tuple -> LocalReference) were counted as USES; fixed in the prior commit's IsDeclarationReference guard. 23 tests green on net8.0 and net10.0. CI: build-and-release.yml gains Build/Test steps for the analyzer project on both TFMs (AnalyzerBuild excluded - the `dotnet build`-spawn test is cross-platform-fragile and also covered by tools/verify_weaver_package.sh).
A leak is a performance nudge - the finalizer still reclaims the buffer - so it must never break a build. The NDArrayLeakAnalyzer descriptor is already DiagnosticSeverity.Warning; this makes the guarantee robust and regression-proof: - NumSharp.Core.csproj adds NDW012 to $(WarningsNotAsErrors) (when the leak analyzer is on), so a detected leak stays a WARNING even if Core's build ever enables TreatWarningsAsErrors. Core emits hundreds of NDW012 (mostly ambient-scope-covered helpers) and none may fail the build. - AnalyzerContractTests pins the contract: NDW012 DefaultSeverity == Warning and IsEnabledByDefault, a real leak surfaces as a Warning with no Error, and the target-gate diagnostics (NDW002/003/005/006/009/010/011) stay Errors by design - so the leak/gate severity split can never silently regress NDW012 into an error.
test/NumSharp.Tests.Analyzer/COVERAGE_PLAN.md - a prioritized plan to broaden the analyzer gate. Catalogs coverage gaps across control-flow/dataflow edge cases (reassignment orphans, ternary/switch temps, loop temps, explicit discard, using-statement/try-finally disposal), escape/consume nuances (ref/out/in/params/collection/lambda-capture/yield/await/boxed-observe), owning-expression detection (chained operators, property/indexer views, generic MakeGeneric, compound-assign-on-input), scope-exemption nuances (property getters, using-statement scopes, local functions, lambdas, [NDScopedAsync]), the NDW002-011 gate NEGATIVES (supported carriers that must stay clean - absent today), type-system robustness, and the NDW013 MSBuild matrix. Also documents the testing TECHNIQUES (marker fixtures + exact-match, inline-source, metamorphic warn<->clean pairs, weaver-parity cross-check of Classify, severity/config pins, property-based fuzz, adversarial known-limitation pins for view-vs-fresh + ambient-scope), a probe-then- fix-or-pin workflow, a prioritized P0/P1/P2 backlog with a tracking table, and the design guardrails not to regress (NDW012 stays a warning; receiver-follows-result; gate mirrors the weaver's Classify; the analysis is per-method by design).
…] tutorial A user-facing, read-top-to-bottom file that runs ONE small computation five ways, so you watch identical code go from leaking to leak-free: 1) the leak — temporaries wait for the finalizer 2) prevented by hand — using/Dispose on every temporary 3) NDScope — the same body + two lines; reclaims ALL temporaries 4) [NDScoped] — 100% original body; the build injects the scope 5) [NDScopedAsync] — the same, for async / Task-returning methods The shared workload (RowNorms) exercises operators (- *), np.* calls (mean/sum/sqrt/maximum), broadcasting (matrix - row-vector, and array - scalar), and two named temporaries in a chained expression (`centered`, `squared`) plus the hidden np.* intermediates. Every section observes reclamation through the public NDArray.IsDisposed and prints the (identical) result, so the memory behaviour is visible and the math is provably unchanged across all five. Section 1 additionally uses a WeakReference to show that, unscoped, only the GC finalizer reclaims — and only late. Wired into the driver as `dotnet run -- examples`; the README and --explain point new readers here first. The two attributed methods are genuinely woven (weave coverage 22/22), so the [NDScoped]/[NDScopedAsync] sections show real reclamation.
…bmat transients Detection pass combining three techniques: the NDW012 static flow-analyzer (broad, over-reports Core helpers), the runtime scope-audit take/return balance (ground truth, but only for corpus-covered ops), and targeted pool-counter probing (for surfaces the corpus never reaches). Reconciling them separated real leaks from analyzer false-positives, and fixed the real ones: - np.linalg inv / det / slogdet / solve — the LU family was the scope-audit Corpus_AllOps gate's ONLY failure (8 families / 18 cases on linalg_parity, pre-existing on journey3): each does `ToCommon(a, common)` — a dtype-cast temp (a.typecode == common ? a : Cast(...)) — that was dropped to the finalizer. [NDScoped] reclaims it while a passthrough operand (already the common type) is never tracked; slogdet's (sign, logabsdet) tuple rides the weaver's Returns<T1,T2> egress. Their siblings pinv/tensorinv/tensorsolve were already scoped. The runtime gate Corpus_AllOps now passes (was red). - np.delete — leaks the `np.ravel` work-COPY on the axis=None path over a NON-contiguous input (ravel of a transposed array copies; measured one bucketed buffer escaped, scoping off). The corpus does not exercise that layout. Scoped the 6 public overloads; WithSourceOrder's asfortranarray and the chunk views are covered too, and the bool-path's hand-disposed keepArr stays idempotent. - np.bmat — every overload builds intermediate concatenations (and, for bmat(ndarray), an obj.copy()) then wraps the final result in an asmatrix VIEW, orphaning the intermediates once the view owns the buffer (measured one bucketed buffer escaped). Scoped all 5 overloads; ARC keeps the yielded view's buffer alive. - np.power / np.floor_divide / np.left_shift / np.right_shift (object overloads) — the function analog of the a**obj / a//obj / a<<obj operators: they mint an np.asanyarray(x2) temp (StackedMemoryPool scalar / managed wrapper, not a bucketed buffer) that a plain `using` could not safely dispose (asanyarray returns the input when x2 is already an NDArray). Scoped for consistency with the operator sweep and to silence their NDW012 warnings. Confirmed analyzer FALSE-POSITIVES (escaped=0 at runtime, consistent with the green corpus): linalg.norm, insert, isin, all, any — the NDW012 "ambient-scope over-reports Core helpers" class; left unchanged. A residual np.delete NDW012 at the concatenate return is the same over-report (the value IS returned). Gates: scope-audit Corpus_AllOps now green; new pins Delete_NonContiguousAxisNull_ ReclaimsRavelWorkCopy and Bmat_ReclaimsIntermediateConcatenations (both uncovered by the corpus); NDScopeWeaveTests + 266 behavior tests (linalg/delete/bmat/power/ floor_divide/shift) green — the weave is byte-neutral. Probe note: np.array(int[3]) is itself a bucketed take, so an operand minted inside a measured region and left undisposed reads as a phantom +1 escape — operands must be hoisted and disposed outside the region (bit the first delete probe; the real delete leak is on a different, non-contiguous path).
…ply to covered helpers
NDW012 (the NDArray leak analyzer) is a per-method OperationBlock dataflow pass with no
call-graph, so it over-reports a helper whose NDArray transients are actually reclaimed by a
[NDScoped]/[NDScopedAsync] (or hand-scoped) CALLER's ambient scope — the documented "scope the
boundary, helpers ride the ambient scope" pattern the analysis cannot observe on its own. This
adds an explicit, author-asserted exemption for exactly that case, and applies it to the first
three fully-enclosed helper clusters.
NDScopedHelperAttribute (src/NumSharp.Core/Backends/NDScopedHelperAttribute.cs)
- Public marker on Method|Property. RUNTIME-INERT and NOT woven: the weaver collects targets by
the exact type names NumSharp.NDScoped{,Async}Attribute, so it never touches this one, and no
NDScope is opened here. Its sole effect is to tell the leak analyzer the method always runs
under a caller's ambient scope, so that scope reclaims its transients.
- Distinct from marking the helper [NDScoped]: that also silences NDW012 but WEAVES a per-call
nested scope (Open + Returns). Nested scopes compose correctly but are not free; [NDScopedHelper]
is the zero-runtime-cost choice when the caller's scope already does the reclamation.
- The assertion is the author's responsibility: if the method is ever invoked without an ambient
scope (an un-scoped public entry, a deferred lambda/Task, another thread), its transients fall
back to the finalizer — a real leak the analyzer will no longer report.
Analyzer wiring (tools/NumSharp.Weaver.Analyzer/)
- KnownTypes.HelperAttr resolves NumSharp.NDScopedHelperAttribute.
- HasScopedAttribute -> IsScopeExemptByAttribute, now exempting sync | async | helper. The gate
analyzer (NDW002-011) is untouched: [NDScopedHelper] is not a weave target and draws no target
diagnostics.
Collision fix (the load-bearing detail)
- 'NDScopedHelperAttribute' CONTAINS the substring 'NDScoped' — the exact ASCII token both the
NDW013 "you used [NDScoped] but the weaver is absent" guard (src/NumSharp.Core/build/NumSharp.targets)
and the weaver's usage pre-scan keyed off via .Contains('NDScoped'). Left as-is, a consumer using
only [NDScopedHelper] — which needs NO weaver and is inert — would FALSE-fire NDW013.
- Both scans now match the two WEAVABLE names precisely: .Contains('NDScopedAttribute') OR
.Contains('NDScopedAsyncAttribute') (neither is a substring of 'NDScopedHelperAttribute'; the
async name needs its own check because it does not contain the sync name). Real [NDScoped]/
[NDScopedAsync] detection is preserved; helper-only consumers are excluded. (This commit carries
only the Core-side NumSharp.targets fix; the identical weaver pre-scan tweak in
tools/NumSharp.Weaver/build/NumSharp.Weaver.targets is a harmless perf optimization and is held
back because that file carries entangled uncommitted work.)
Applied to already-covered helpers (runtime-inert; verified by rebuild recount 149 -> 119 NDW012)
- np.linalg.norm: VectorNorm / MatrixNorm / FrobeniusOverBoth / SingularValueNorm — all dispatched
only from the [NDScoped] norm@45 (vector_norm(int) and matrix_norm delegate into scoped norm).
- np.isin: TryTableMembership / SortedSearchMembership / MembershipUInt64Signed — reached only via
ComputeMembership under the [NDScoped] isin.
- DefaultEngine boolean-mask: BroadcastMaskAcrossBlock — both callers are [NDScoped] kernels.
Tests (34 green: 32 in-process + 2 AnalyzerBuild)
- HarnessSelfTests.Harness_ExemptsNDScopedHelperMethod: a metamorphic pair — the SAME dead-local
leak, and the only difference is the attribute — proves [NDScopedHelper] flips warn -> clean.
- LeakScenarios.AmbientHelper: an untagged [NDScopedHelper] fixture method, gated by the exact-match
harness (a leak there would fail).
- Ndw013BuildTests.Ndw013_DoesNotFire_ForHelperAttributeOnly: a real dotnet build of a
[NDScopedHelper]-only consumer asserts NDW013 stays silent — pinning the collision fix.
…plicit [NDScopedAsync] support
Renames the analyzer-only ambient-coverage hint attribute to a clearer name and makes its support
for async scope boundaries explicit and tested. Behaviour is unchanged: the same 119 NDW012 sites,
all analyzer tests green (35).
Rename
- NDScopedHelperAttribute -> NDScopedCoveredAttribute ("covered by an ambient scope" reads more
truthfully than "helper"). File git-moved; class, KnownTypes.CoveredAttr, the analyzer exemption,
the NDW013 guard comment, and every [NDScopedCovered] annotation (norm / isin / boolean-mask)
updated. The precise gate scans are unaffected: 'NDScopedCoveredAttribute' contains neither
'NDScopedAttribute' nor 'NDScopedAsyncAttribute' as a substring, so the collision fix still
excludes it (a covered-only consumer draws no NDW013).
[NDScopedAsync] boundary support (the "supported by NDScopedAsync" requirement)
- The exemption was already uniform over sync|async|covered, so a [NDScopedCovered] helper is
honoured whether its covering boundary is a synchronous [NDScoped] or an async [NDScopedAsync]
method. This is now spelled out: an [NDScopedAsync] method holds ONE invocation scope across its
awaits (re-installed on the resuming thread at every MoveNext), so a helper it calls synchronously
within a segment allocates under that scope and is reclaimed identically to the sync case. The
attribute doc records the two async caveats that follow from the state-machine seam: the scope is
suspended across an await (a covered helper that is itself async only rides the caller's scope up
to its first await), and an awaited callee keeps the caller's tracked temps alive until completion
(intended, not a leak).
- New test HarnessSelfTests.Covered_IsHonored_UnderNDScopedAsyncBoundary: a metamorphic pair with a
[NDScopedCovered] helper called from an [NDScopedAsync] async Task<NDArray> boundary — the control
(no attribute) leaks the helper's dead local; with [NDScopedCovered] the pair is clean.
Tests: 35 green (33 in-process + 2 AnalyzerBuild). Ndw013_DoesNotFire_ForCoveredAttributeOnly and
Harness_ExemptsNDScopedCoveredMethod renamed accordingly. (The identical rename of the weaver
pre-scan comment in tools/NumSharp.Weaver/build/NumSharp.Weaver.targets is held back with that file's
entangled uncommitted work.)
…he chokepoint gate (pre-existing red since 48b00e0) EveryRawNativeAllocation_IsAKnownChokepoint has been RED on journey3 since the managed LU fallback landed: 48b00e0 (2026-08-28) introduced two NativeMemory.Alloc sites in Backends/Default/LinearAlgebra/ManagedLu.cs — the per-factorisation scratch (the working copy of the operand matrix and the int pivot vector, both freed in finally) — without adding the file to this gate's file->site-count allowlist. The gate itself (d150678, 2026-08-26) is an ancestor of that commit, so every FuzzMatrix run since has failed with 'NEW raw-allocation file: ManagedLu.cs (2 sites)'. Pin the file at exactly 2 sites, classified as the same audit-debt class as NDIter's iterator scratch: an internal alloc+free pair invisible to the runtime pool sweep, to be routed through SizeBucketedBufferPool/StackedMemoryPool later (the OpenBLAS backend grew a thread-local retention pool for exactly this scratch pattern in e89d6d8 — the managed LU may warrant the same treatment; until then the exact-count pin still catches any THIRD raw allocation appearing in the file). Gate: FuzzMatrix 98/98 green after this pin (was 97/98).
…d pad/insert/atleast_3d/all/any entries Part of the NDW012 (B) sweep: make every flagged un-scoped np.* entry an NDScope boundary so its whole synchronous call tree's temporaries are reclaimed eagerly instead of falling to the finalizer, then mark the now-covered private helpers [NDScopedCovered] (analyzer-only, runtime-inert) so the per-method analyzer stops over-reporting them. 33 of the 119 pre-sweep NDW012 sites clear here. np.pad: [NDScoped] on PadImpl — the single private dispatcher every value-mode overload funnels through (only the int-pad_width overload was scoped; the int[], int[,], tuple and dict overloads all leaked _SetReflectBoth's '2 * edge' odd-mode temps and BuildLinearRamp's reshaped-coefficient view). [NDScopedCovered] on _SetReflectBoth and BuildLinearRamp. PadCallableImpl is deliberately NOT scoped: its tree disposes everything it creates, and an ambient scope there would reach into the user's PadFunc callback and dispose arrays the callback creates for itself. np.insert: [NDScoped] on the five array-values overloads (long/Slice/int[]/long[]/ NDArray obj) — np.delete's exact pattern: PrepareAxisContext mints a ravel work array and WithSourceOrder mints an asfortranarray copy that supersedes the C-order intermediate; none were reclaimed. The scalar-value overloads forward into these and already dispose their own coercion. np.atleastd: [NDScoped] on the four atleast_3d overloads — the 1-D arm's inner expand_dims alias was dropped on every call. ndim>=3 passthroughs return the untracked input, so Returns is a no-op there (R2-safe by construction); the params overloads yield every element through the NDArray[] egress. np.delete: [NDScopedCovered] on DeleteChunkConcat (only reached from DeleteIndexArray under the six already-[NDScoped] delete overloads). np.all/np.any: [NDScoped] on all eight axis/out/where overloads. Real leaks fixed: ApplyWhereForAll's '!maskBool' negation temp and both helpers' ToBool conversions, plus the reduced temp dropped on every out= call after WriteToOut copies into the caller's array. The int/int[] axis overloads' engine trees (chained single-axis intermediates, the fused axis-run reshape) ride the same scopes; their MarkReductionScalar-as-statement flags were pure FPs silenced as a side effect. [NDScopedCovered] on ApplyWhereForAll/ApplyWhereForAny. Default.LogicalReduction: [NDScopedCovered] on ReduceContiguousAxisRun — the multi-axis engine All/Any forms are reached only from the now-scoped np.all/np.any int[]-axis overloads (verified: the only callers are np.all.cs:74 / np.any.cs:74), and its fused reshape view is reclaimed by those scopes. Gates: NDW012 119 -> 1 across the full sweep; ScopeAudit Corpus_AllOps green at zero undisposed intermediates; FuzzMatrix 98/98; NumSharp.Tests 14k green.
… — scope take/extract/trace/mgrid/Contains, eager-release reshape's copy wrapper np.take: [NDScoped] on both overloads. The array overload leaked its 0-d reshape alias and — on every out= call — the natural-result temp dropped after copyto into the caller's array; the scalar overload leaked its NDArray.Scalar(index) wrapper, which is built in ITS frame before the array overload's scope opens and so needs its own boundary. np.extract: [NDScoped] — the ravel view/copy handed to take was never disposed (and the flatnonzero indices leaked on the throw path). np.trace: [NDScoped] — the diagonal view was never disposed (only its conditional contig copy was), and the pre-out result leaked on the out= path. np.diag / np.fill_diagonal / np.tril_indices: [NDScopedCovered] on DiagonalEmbed, WriteDiagonalBlock and TriangleIndices — each is reached only from already- [NDScoped] entries (diag/diagflat, fill_diagonal, the four tril/triu_indices(_from) entries); the flagged temps are the AOT-fallback alias views and the shared iota column ladder, all reclaimed by the entry scopes. np.mgrid: [NDScoped] on MGridClass.Build — each non-trivial axis rescale dropped its Materialize'd line, the astype supersede, the reshape and broadcast_to views, and the grid[k] layer view. NDArray.ReShape (ReshapeCore): the copy path built the internal order-copy, wrapped a view over it, and dropped the copy WRAPPER — an abandoned counted ref that kept the buffer from freeing until the block finalizer. Now the wrapper is disposed right after the view is built (the returned view holds its own counted ref, so the buffer stays alive — the documented alias-safe release). A precise 2-line fix rather than [NDScoped] because reshape's O(1) contig/nocopy view fast paths are among the hottest calls in the library and must not pay a scope per call. BEHAVIOR PIN UPDATED: ArcLifecycleTests.ReshapeNonContiguous_AllocatesNewOwningBuffer pinned the OLD orphan lifecycle (refcount drains only via finalizer, buffer never freed by the last Dispose). The returned view is now the sole counted owner, so the LAST deterministic Dispose frees eagerly — the test now asserts IsReleased==true with no GC/finalizer drain, mirroring ReshapeContiguous_SharesRefCount's shape. NDArray.Container (Contains): [NDScoped] — the exact structural case the scope solves: np.asanyarray(value) either passes an NDArray-typed value through (constructed before the scope opens -> untracked -> untouched) or mints a fresh conversion (tracked -> reclaimed). The old code could not use-bind it for exactly that reason and leaked the conversion. UnmanagedStorage.Setters: the two validate-only np.broadcast_to calls on the empty-assignment paths dropped their stride-0 view wrappers — now .Dispose()'d inline (the view aliases the caller's value; releasing its counted ref is safe). The storage-level GetData sub-views are UnmanagedStorage (uncounted aliases, no Dispose) and were correctly never flagged. Selection: [NDScopedCovered] on TryBuildMultiAdvancedGrid, TrySetSliceWithSingleAdvanced and PrepareIndex — all reached only from the already-[NDScoped] FetchIndices(object[]) / SetIndices(object[], values) dispatches (verified caller-by-caller), so their mask nonzero components, MakeGeneric aliases and index coercions are reclaimed by those scopes. TryFetchSliceWithSingleAdvanced gets the same marker with an explicit note that it is currently UNREFERENCED (superseded by TryBuildMultiAdvancedGrid; only a doc-cref remains) — the claim is vacuously true and the note keeps a future revival honest. DELIBERATELY LEFT FLAGGED (the sweep's single residual NDW012): Selection/NDArray.Indexing.cs:46 — the NDArray<int>[] indexer getter. Its frame constructs NO NDArray (the LINQ Select((NDArray)a).ToArray() repackages existing references; the covariant-array shortcut is unsafe because the callee may write non-NDArray<int> elements), and everything real happens inside the [NDScoped] FetchIndices it calls. A scope here would tax a hot indexer to silence a pure analyzer FP; a Covered marker would be a false claim on a top-level entry. Gates: ScopeAudit Corpus_AllOps zero-leak green; FuzzMatrix 98/98 (index oracle included); NumSharp.Tests 14k green incl. the updated ArcLifecycle pin.
…ype)/shuffle/permutation/uniform(arrays)/HasZero/Normalize
np.modf: [NDScoped] on the Type-dtype overload — its twin (NPTypeCode) was already
scoped; both route PreserveFContig, whose F-preservation branch supersedes the
C-order (frac, whole) pair with copy('F') and dropped the originals. The tuple
egress yields component-wise.
np.nancumsum: [NDScopedCovered] on _replace_nan_for_scan — its only callers are the
already-[NDScoped] nancumsum/nancumprod, whose scopes reclaim the isnan mask, the
where composition's temps and the astype-superseded fill scalar.
NDArray.Normalize (obsolete): [NDScoped] — a void boundary that leaked min/max AND
one scalar-arithmetic temp pair per element of the 2-D loop; all reclaimed at exit
now (the writes land through this[row,col] into the caller's buffer).
Generator.shuffle: [NDScoped] — the N-D path dropped both the np.array(idx)
Fisher-Yates index array and the reordered take gather after CopyInPlace copied it
back into x. Void boundary; x itself is an untracked input. The RNG draw sequence
is untouched (scoping changes no random_interval calls), so streams stay
byte-identical to NumPy.
Generator.permutation(NDArray, axis): [NDScoped] — dropped the same index-array
temp; the gathered result (or the shuffled copy on the 1-D path) is yielded.
np.random.uniform(NDArray low, NDArray high, Type): [NDScoped] — leaked the rand
draw, its astype, the (high - low) difference and the pre-cast ret on the
dtype-cast return.
np.average's HasZero: [NDScoped] directly on the private bool helper — it has no
NDArray egress at all, so the scope self-contains the zero scalar, its astype and
the == comparison mask. Chosen over scoping the big average entries (not flagged;
they dispose their own temps) and over a Covered claim (average's entries are not
scoped, so coverage would be false).
Gates: ScopeAudit Corpus_AllOps zero-leak green; FuzzMatrix 98/98 (random_parity
tiers included); NumSharp.Tests 14k green.
…tion entries, product gufunc seams and polyfit/roots The linalg module's factorisation entries all shared one leak shape: the ToCommon dtype-normalisation cast (a fresh array whenever the operand is not already at the computed common dtype) was handed straight to the engine and dropped. [NDScoped] on: svd, svdvals (also drops the null U/Vh slots' tuple), eig, eigvals (plus the pre-collapse complex w/v that CollapseEig's real-collapse and astype supersede), eigh, eigvalsh, qr, cholesky, and lstsq (which additionally leaked the column-promoted 1-D b and, on the nrhs==0 pad path, the engine x/resids superseded by the trimmed zeros). Tuple returns yield component-wise through the weaver's typed ValueTuple egresses (arity 2 for qr/eig/eigh, arity 4 for lstsq). matrix_rank/cond/multi_dot/norm and EinsumContract were already scoped. [NDScopedCovered] on the now-covered helpers, each verified caller-by-caller: CondNanToInf (only under [NDScoped] cond), AbsSquared (only from the norm helpers, themselves covered under the scoped norm entries), ThreeInBestOrder and Chain (only under [NDScoped] multi_dot — every recursive partial product is scope-reclaimed), StackCross (only under [NDScoped] np.cross), and einsum's PairwiseContract / PureMultiplication (only under [NDScoped] EinsumContract's left-to-right fold, which reclaims each superseded left/right/ab contraction stage). linalg.cross (the Array-API form): [NDScoped] — one attribute clears 8 flagged sites: the two moveaxis views, the six dropped component products inside the subtract compositions, the expand_dims wrappers and the pre-moveaxis stack. TensorEngine Vdot/Vecdot/Matvec/Vecmat: [NDScoped] on the engine virtuals themselves rather than a Covered claim — they are PUBLIC virtuals callable directly (not only via the np.* wrappers), and the temps (ravel/reshape/conjugate operands, the pre-reduction product, the pre-DropAxis matmul result) are minted in these bodies. A backend-accelerated result created inside the scope is yielded identically. ManagedLu Det/Slogdet/Inv/Solve: [NDScoped] — the Single branches leaked their double-bridge astype temps (up-cast operand(s) and the pre-narrow result) on every float32 call; internal entries, but reached through public engine virtuals, so a self-contained boundary is the honest fix and an O(n^3) factorisation dwarfs the scope cost. np.polyfit: [NDScoped] — the float-coercion '+0.0' casts, vander, the weight column, the scale/unscale chain and the covariance composition all leaked; PolyfitResult already implements INDArrayCarrier.YieldTo, so the carrier's outputs are yielded and everything else is reclaimed. np.roots: [NDScoped] — the ravel/nonzero/strip/astype chain and the companion matrix + eigvals intermediates leaked. np.polyint(double k): [NDScoped] — the 0-d NDArray.Scalar(k) wrapper is built in this frame, before the (already scoped) main overload's scope opens. Gates: ScopeAudit Corpus_AllOps zero-leak green; FuzzMatrix 98/98 (products / poly / einsum tiers included; linalg_parity is host-pinned and green on this host); NumSharp.Tests 14k green.
…hot product/reduction/nonzero dispatchers, scope Evaluate The remaining engine-level NDW012 sites sit on dispatchers where a per-call NDScope would tax exactly the cells the benchmark suite watches (sub-5us tiny matmul/dot, small-N reductions), so these take PRECISE disposal of the concrete leaked temp instead of a boundary scope — per NDScope's own granularity guidance and the comparison-small-N alloc-floor finding (finalizer-bound buffer lifetime is what tanks small-N cells). Default.Dot (1-D x 2-D branch): the row-vector reshape alias of the left operand and the pre-squeeze matmul result wrapper were dropped; both are released after the squeezed view is built (the view holds its own counted ref). Default.Dot.Fused (DotInner1D): the mixed-dtype and default-fallback paths dropped the promoted left*right product after ReduceAdd consumed it — disposed once the scalar reduction is in hand. Default.MatMul (batched loop): every batch iteration minted three GetData sub-view wrappers (lhs/rhs/ret 2-D slices) and dropped them — a 2000-product stack stranded 6000 wrappers on the finalizer. Now each iteration disposes its views (a method scope would instead hold 3 x batch temps alive simultaneously — the pool-bucket overflow the granularity note warns about). MultiplyMatrix's @out form returns ret2d itself; both locals are disposed, the second call being an idempotent no-op (pinned Interlocked guard in NDArray.Dispose). Default.Reduction.CumAdd/CumMul: the bool->int64 conversion feeding the recursive scan was dropped — disposed after the scan returns. Default.Reduction.ArgMax/ArgMin: the size-1-axis fast path dropped the zeros base behind its squeeze view — released after the view is built. Default.NonZero/FlatNonZero/Argwhere: the empty-result base behind the column views, and the 0-d paths' atleast_1d promotions plus Argwhere's nonzero columns, were dropped — all disposed (each column view holds its own counted ref on the shared multi-index buffer). DefaultEngine.Evaluate(expr, out): [NDScoped] — a per-expression boundary (the fused-pass cost dwarfs it); any leaf wrappers BindArrays mints are reclaimed and the fused result (or the untracked caller @out) is yielded. Gates: ScopeAudit Corpus_AllOps zero-leak green (the batched-matmul and nonzero-family cells replay through these exact paths); FuzzMatrix 98/98; NumSharp.Tests 14k green.
Member
Author
…NumSharp.Build Renames the four projects of the [NDScoped] build-time subsystem and every reference to them, so the packaged tool ships under a name that describes what it does to a consumer's build rather than the internal "weaver" mechanism: NumSharp.Weaver -> NumSharp.Build (tool + NuGet package id) NumSharp.Weaver.Analyzer -> NumSharp.Build.Analyzer (Roslyn analyzer, embedded in the package) NumSharp.Tests.Analyzer -> NumSharp.Tests.Build.Analyzer NumSharp.Analyzer.Fixtures -> NumSharp.Tests.Build.Analyzer.Fixtures What moved / changed: - Project folders, csproj files, assembly names, root namespaces, and the C# namespace/using of every moved source file. - The packaged targets file build/NumSharp.Weaver.targets -> build/NumSharp.Build.targets. NuGet auto-imports build/<PackageId>.targets, so the filename MUST track the PackageId or the weave would silently stop running for package consumers. - The MSBuild vocabulary tied to the old package name: the $(NumSharpWeaver*) properties/items/targets (Project, ToolDll, AnalyzerDll, Active, DotnetHost, Assembly, _NumSharpWeaverPackTool, _NumSharpWeaverAnalyzer) -> $(NumSharpBuild*), renamed together across the Core self-weave, the packaged targets, the NDW013 guard, the source-mode example and the Ndw013BuildTests assertion. - NumSharp.Core: the self-weave bootstrap (<NumSharpBuildProject>) and the NDW012 leak-analyzer ProjectReference now point at tools/NumSharp.Build[.Analyzer]; the NDW013 "weaver missing" message names the NumSharp.Build package. - examples/NDScoping: source-mode ProjectReferences, the $(NumSharpBuildToolDll) tool path, and the <Import> of the renamed targets file. - CI (.github/workflows/build-and-release.yml): build/pack/test paths, the 'dotnet add package' line and the release badge; verify_strong_name.cs asserts the new tool-dll and targets names. - Gate scripts (verify_weaver_package.sh, stress_weaver.sh) and the docs (DISPOSAL-GUIDELINES.md, ARCHITECTURE.md, docs/.../ndscoped.md, the NDScoping example READMEs/DESIGN). Deliberately kept: the NDScope/weave FEATURE and mechanism names — the ScopeWeaver classes, the NDScopeWeave MSBuild target, the NumSharpDisableWeaverMissingWarning opt-out, the verify_weaver_package.sh gate and the word "weaver" in prose. The tool is still a weaver; only the names tied to the old package IDENTITY were changed. Fixes found while finishing the partial rename: - The self-weave bootstrap path pointed at ..\..\src\NumSharp.Build (wrong segment); corrected to ..\..\tools\NumSharp.Build. - Removed a stale fixtures glob in the test csproj that still pointed at the deleted NumSharp.Analyzer.Fixtures folder. Verified: NumSharp.Core self-weaves through the renamed tool (woven 265), the source-mode NDScoping example weaves and re-signs (woven 21), and the analyzer test project builds. Note: the moved weaver/analyzer/test files carry the in-progress [NDScopedExit] / async-split subsystem work they were mid-development on; that content moved with them unchanged. Benchmark reports, docs-site data and test/inventory regeneration were left out of this commit.
journey3 accumulated four intermediate benchmark-history snapshots on top of master's three June snapshots. Remove this branch's four additions and publish only the latest run, so the branch contributes a single snapshot instead of a series. Removed (added earlier in journey3): - benchmark/history/2026-08-22_32598ddf - benchmark/history/2026-08-22_aaa41ef2 - benchmark/history/2026-08-24_020e6543 - benchmark/history/2026-08-24_938d0449 Added: - benchmark/history/2026-08-28_6837c918 (latest run; its reports match the working-tree benchmark reports) Retarget benchmark/history/latest -> 2026-08-28_6837c918. Kept master's inherited snapshots (2026-06-05, 2026-06-23, 2026-06-29) and left all non-history working-tree changes untouched. The intermediate uncommitted snapshots 2026-08-26 / 2026-08-27 were local scratch and are discarded, not committed.
…t64 — fixes float32 upcast + complex128 correctness
np.isclose/np.allclose cast BOTH operands to float64 unconditionally
(Default.IsClose.cs). Two consequences, both wrong:
1. PERF (float32/float16): NumPy's isclose forces only the reference operand
inexact via result_type(b, 1.0) and evaluates the whole formula in
result_type(x, y), so (f32,f32) computes in float32 — NumSharp did 2x the
memory work by widening to float64. Measured backwards: NumSharp isclose f32
at 10M was SLOWER (247ms) than its own f64 (200ms); NumPy is the right way
round (f32 98ms < f64 185ms).
2. CORRECTNESS (complex128): astype(complex -> Double) drops the imaginary
part, so isclose compared real parts only — isclose([1+0j],[1+100j]) returned
True where NumPy returns False. This was papered over by a [known bug]
excusal in MisalignedRegistry for "F-contiguous/complex strided pairing
divergence".
Fix: cast each operand through InexactPromote(tc) = {Half,Single,Double,
Complex -> self; else -> Double} (== NumPy's result_type(dtype, 1.0)) and let
the elementwise operators do the NEP50 promotion. This reproduces NumPy's exact
computation dtype result_type(x, result_type(y, 1.0)) for every pair:
(f32,f32)->float32, (f16,f16)->float16, (complex,complex)->complex128,
bool/int/char/decimal->float64 (byte-identical to the previous Double cast, so
those dtypes cannot regress). Weak-scalar rtol/atol already adopt the operand
dtype, so the tolerance stays at the reference dtype exactly as NumPy computes it.
Verification:
- Bit-exact vs NumPy 2.4.2 across 14 dtype pairs (all same-dtype + mixed +
int/bool/complex) x equal_nan x boundary/NaN/inf injection: 0 mismatches.
- The complex128 [known bug] excusal is now stale: FuzzMatrix Logic + Specials
pass with it DISABLED, so it is removed (the whole complex128 tier —
contig/F/strided/broadcast/negstride — is bit-exact).
- No degradation: float64/int paths are the same Double cast; same-session A/B
shows f64 unchanged (~107ms at 10M). 467 Logic + leak-guard tests green.
Perf (NPY/NS, best-of-min, threads=1):
- isclose/allclose float32: 100K 0.18x -> 3.3x, 10M 0.35x -> 1.10x
- isclose/allclose float64: unchanged (100K 3.3x, 10M ~0.9x memory-bound)
10M stays ~1.1x rather than >=1.5x because isclose is a ~15-allocated-pass
composition (NumPy's is too) and is memory-bound; a fused single-pass isclose
kernel is the path to 1.5x there and is left as a follow-up.
…h build-time date priority
The three live docs dashboards (coverage-support, tests-oracle, benchmarks) fetch
same-origin JSON that DocFX bakes from generated files committed on the code branch.
This relocates that data to a dedicated orphan branch, master-code-data, so large,
frequently-regenerated blobs live apart from code history — while keeping master's
committed copies as a backwards-compatible fallback.
Model (generalizes the existing benchmark/history <date>_<sha> + `latest` symlink):
<data_type>/<date>_<commithash>/<files> + latest -> newest (git symlink, mode 120000)
Four data types: benchmark, tests-oracle, inventory (NumPy API coverage), benchmark-coverage.
New tooling (tools/dashboard_data/, stdlib-only):
- common.py — per-type file/overlay map, stamp formatting, `latest` symlink repoint,
resolve_latest (follow `latest`, else max-stamp fallback), git commit-date reads.
- publish.py — append a <date>_<sha> snapshot for a type, repoint `latest`, optional commit.
- resolve.py — at docs-build time, per dataset pick the newer of master vs branch by git
COMMIT DATE and overlay the winner onto the paths DocFX already reads
(no docfx.json change). master stays the fallback, so the site builds from
master alone; any newer publish to either branch wins.
CI wiring:
- docs.yml: contents:write; on master pushes publish inventory + tests-oracle +
benchmark-coverage to master-code-data; build-and-deploy fetches the branch and runs
resolve.py before docfx build (fetch-depth:0 for master-side commit dates; +Setup Python).
- benchmark.yml: after run_benchmark.py, publish the fresh benchmark snapshot to the branch.
The UI JS and its relative data/ fetch are unchanged — selection happens entirely at build time.
… benchmark skill The benchmark skill's run-and-report reference describes how generated data reaches the docs dashboards; that pipeline gained the orphan master-code-data branch + a build-time date-priority resolver. Add a "Dashboard data delivery" section (the two sources, the tools/dashboard_data publish/resolve tooling, the four data types, the CI wiring, and that docfx.json is unchanged), cross-link it from the history-snapshots section, and add a pointer in SKILL.md. Keeps the skill accurate now that benchmark data can be served from the branch instead of only the committed docs copy.
…26-08-29 run The Function Explorer + snapshot pills + effective geomean auto-update from benchmark-report.json, but the story-card / metric-card figures are hand-maintained. Sync them to the 2026-08-29_9b200075 snapshot: - Cast subsystem metric + story: 1,202 -> 1,187 wins (of 1,568 comparable). - NDIter story: 1.45x -> 1.46x geomean; 106/59 -> 108/57 win/loss over 165 cells. - Layout story: by-layout geomean span 0.37-3.81x -> 0.54-9.24x. - Fusion story: best fixed 4.78x -> 4.12x; broadcast fusion 3.27x -> 4.10x. API-coverage card (98.7%, 455/461) left as-is — the perf run does not change it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

NumSharp 0.70.0
It took a while but we are at 85% NumPy API coverage.
This is a huge milestone for the .NET ecosystem as NumSharp grows to matureness.
This branch also delivers integration with NumPy's OpenBLAS backend and full integration with Python giving a new angle of use cases for NumSharp to a point NumSharp is an unmanaged memory and math interop with the python ecosystem. I believe in integration rather than competition thus the large scale of support from PyTorch to Pillow.
OpenBLAS is a rapidly developed ecosystem NumSharp will eventually replace with a simpler version but that requires porting of 100k-300k lines of code to achieve complete mathematical parity. OpenBLAS roughly powers 30% of NumPy's.
📦 New NuGet Packages
Two optional companion packages ship for the first time, co-versioned with NumSharp 0.70.0 (both depend on NumSharp.Core).
All nugets are now published as signed nuget packages.
dot,matmul,inner,vdot,vecdot,matvec,vecmat,tensordot,multi_dot,matrix_power.solve,inv,det,slogdet,tensorsolve,tensorinv.cholesky,qr,svd,svdvals.eig,eigvals,eigh,eigvalsh.lstsq,pinv,matrix_rank,cond,norm.correlate,convolve.arr.ToNumpy()/arr.ToPython()out;pyObj.AsNDArray()/pyObj.FromArrayLike()in.RegisterCodec()once, then pythonnet's ownobj.ToPython()/pyObj.As<NDArray>()round-trip transparently.Auto(view when possible, else copy),View(share or decline),Copy(always independent).FromArrayLikeimports any PEP 3118 exporter, strided/offset/reversed included; read-only stays non-writeable:📊 Dashboards & Docs
Three living dashboards ship on the documentation site, each generated from the same CI artifacts the release gates run on.
✨ New APIs & Modules
np.random.default_rng- the full modern PCG64Generator, byte-identical streams to NumPy 2.4.2 -e868d8ae(+754b7476,febfbbdd,f491c499).default_rng- entry point (seed /SeedSequence/BitGenerator/PCG64overloads).random,integers,standard_normal,normal,exponential,uniform,standard_gamma,gamma,choice,shuffle,permutation,permuted- the Generator draw surface.random_integers,bytes- the legacy RandomState helpers.np.fft.*- the whole 18-function Fourier module, a pure-managed pocketfft port, bit-exact incl. float32/float16 values -3b9d5cfb,a525e355,4cb91898.fft,ifft,fft2,ifft2,fftn,ifftn- complex forward/inverse (1-D/2-D/N-D).rfft,irfft,rfft2,irfft2,rfftn,irfftn- real-input transforms.hfft,ihfft- Hermitian-symmetric transforms.fftfreq,rfftfreq,fftshift,ifftshift- sample-frequency & shift helpers.np.einsum- Einstein summation, now computing and planning -7d2d7a2f(+d78e07db,b61b0998),bb63ba48.einsum- contracts via the matrix products (rides OpenBLAS when the package is referenced).einsum_path- greedy/optimal contraction planner, byte-exact info string.np.r_/np.c_/np.ix_/np.s_/np.index_exp- the grid & slice-expression DSL, 131/131 bit-exact vs NumPy 2.4.2 -00dfe402(+3c63734d,7eea4f7f,c4e27523).np.ogrid/np.mgrid/np.meshgrid- open-mesh / dense-mesh / coordinate-matrix grid constructors, differential bit-exact vs NumPy 2.4.2 -19feaed2,7f558d05,4e8c3925.NDIterRefengine (37 cases probed side-by-side, all identical) -8bd882b3,7112cbe4.np.nditer,np.ndindex,np.ndenumerate- the boxed iterators, full flag/error parity.np.nested_iters,ndarray.flatiter- nested-loop iterators + a write-through flat iterator.b3505398(+8cad3025).partition,argpartition- kth-element partial sort (value + index).lexsort- indirect stable multi-key sort;sort_complex- real-then-imag complex sort.nanargmax,nanargmin- NaN-aware argmax/argmin.np.take_along_axis- the per-slice gather (theargsort/argmaxinverse), NumPy 2.4.2 parity, 24,000+ fuzz cases bit-exact, ≥1.5× faster on every measured variation -f351600a(+88550d13,7091a3c9).np.select- pick each element from the first choice whose condition is true, NumPy 2.4.2 parity (fused single-pass kernel on the contiguous path) -fc10404d(+42d96a14).np.isin+intersect1d/union1d/setxor1d/setdiff1d- element-wise membership + sorted set algebra, NumPy 2.4.2 parity (1.9-13.5× faster) -bfe952d5(+27632ed5).unique_values,unique_counts,unique_inverse,unique_all, 102/102 bit-exact vs NumPy 2.4.2 across 13 dtypes -bec1c497.np.diagfamily + triangular ops - 13 functions, 165/165 side-by-side parity with NumPy 2.4.2 -27b9b012(+a7782984).diag,diagflat,fill_diagonal- diagonal build & in-place fill.tri,tril,triu- triangular masks & extraction.diag_indices,diag_indices_from,tril_indices,tril_indices_from,triu_indices,triu_indices_from,mask_indices- index generators.np.*products; byte-parity via the OpenBLAS backend when referenced -53d7764f(+81509766,297f883f,74aa5d5a).inner,vdot,vecdot,matvec,vecmat,tensordot,multi_dot,matrix_power.956f3392(+2628c921,d6a50593).poly,roots,polyfit,polyval- construction / fitting / evaluation.polyadd,polysub,polymul,polydiv,polyder,polyint- arithmetic & calculus.poly1d- the polynomial object;vander- Vandermonde matrix.savetxt→loadtxtround-trips -a1920a4a,17a1ff8a(+80a0ed50,d39ff824).np.savetxt,np.loadtxt,np.fromstring.9fa48041(+615f1ee5).arcsinh,arccosh,arctanh- primary ufuncs;asinh,acosh,atanh- Array-API aliases.deviceconformance (CPU shim) -ebba2cbf.ndarray.device,ndarray.to_device, anddevice=onarray/zeros/ones/empty/arange/ ….np.kron,np.cross- Kronecker & cross products -7bcad845,73019dce.np.cov,np.corrcoef- covariance & Pearson correlation -92dc537b,aaf731b2.np.choose- index-into-choices gather -aaa41ef2.np.nancumsum,np.nancumprod- NaN-aware cumulative scans -0370c0aa.np.digitize,np.bincount- bin-index + integer histogram, bit-exact vs NumPy 2.4.2 -f2cefba2,12f484c3.np.correlate- sliding cross-correlation (managed SIMD; OpenBLAS byte-parity below) -12f484c3.np.bmat- block-matrix assembly -6ba24752(+d5621d57).np.real,np.imag,np.angle,np.conjugate/np.conj- complex component / phase accessors (post-FFT spectrum extractors) -8b0ac701,d0081b6d.np.iterable- NumPy's pure iterability predicate -ce560796(+8cf54d35).np.isfortran- F-contiguity predicate (a.flags.fnc) -30453696.np.linalgfactorisation surface and complex128dot/matmulare listed under New NuGet Packages above (they compute via the OpenBLAS backend) -dc448acc,f5ec6276,d09e4376,6ee562da.🧩 ndarray surface
ndarraymember parity with NumPy 2.4.2 -data- the memoryview buffer object (np.MemoryView); accepted zero-copy byarray/asarray/frombuffer/ … -25ae7053(+4072577d,bc544403).byteswap- width-dispatched endian byte-swap -67994cbc.getfield,setfield- byte-field views -4b07b71d.real,imag,conj,conjugate- complex accessors -7765ce50.itemsize,nbytes,fill,flags- metadata members -792a9f14(+aee7cbab,27a19ae4);setflags- write/align control -275f089c.all,any,clip,take,repeat,squeeze,trace, …) -06869352.⚡ Performance
Ratios are NumPy ÷ NumSharp - higher is better (
x2= twice NumPy's speed);xLOW->xHIGHspans the worst→best measured cell across sizes and dtypes.1088 bytes -> 192 bytes- NDArray allocation size has been reduced by utilizing StructLayout.Explicit.x0.98->x74-np.uniquefamily routed through the radix sort core -5df10897(+35d12699).x1.6->x5.2-percentile/median/quantilepivot-stack block-partition quickselect -8a1376ff.x0.4->x2.75-np.argpartitionon the same block/pivot-stack path -75a1d873.x1.35->x11-np.isinhash-set membership replaces sort+searchsorted -bd96d541.x15.6- blocked GEBP double GEMM for transposed-Bdot(2.9→43 GFLOP/s) -97e9e82a(+7d680eb1).x40->x249- typednp.nditer<T>/nditer_chunks<T>, allocation-free iteration (chunks +Vector<T>hits 249×) -d58f3728.x1.0->x4.5-take/put/placeelement-copy specialization + gather prefetch (takewent from x0.68 losing to winning everywhere) -88550d13.x1.04->x11.7- float32exp/log/sin/cos/tanh+rad2deg/deg2radreimplemented as bit-exact NumPy kernel ports (tanhalso replaces the float64 loop) -ecdb4581,6bab5754,f5f21ff3.🎯 Parity & Fixes
ndarray.flags/setflags- full NumPy 2.4.2 parity across the whole layout/producer space, hardened by a 1104-case differential oracle (owndata/writeable/contiguity, squeeze-as-view, split-child contiguity, read-only reduction scalars) -275f089c,53b5d82e,ca1b0fac.searchsorted- complex lexicographic order +result_typekey promotion (no more silent key down-cast) + NaN-as-largest total order -93abe13d,cc676ea8,f2cefba2.np.take/np.putindex validation matches NumPy - a negative index undermode='raise'normalizes once (np.take(a, [-1])addresses the last element instead of throwing), and a non-castable float/complex index raises the verbatimTypeErrorinstead of silently truncating -fc10404d,88550d13.np.correlate/np.convolve- OpenBLAS byte-parity via the new sliding-dot seam -d0be3132.broadcast_tois read-only,broadcast_arraysis writeable, and writing a non-writeable view now raises NumPy's verbatim message instead of silently corrupting the shared source -1eadb83b,6fb518c0,1cc67d47(+baf41c89).size×itemsizeoverflow,reshape(-1, …), andexpand_dimsaxis now raise NumPy's verbatim texts instead of silent wrong-size allocations or raw .NET exceptions -c2552d6a.ones(3000)@ones(3000)=2048 saturation), stacked/fancy indexing into zero-sized arrays, and the 0-d boolean setter -03d0f0c8,7636100a,f6e258c0.SetIndicesNDNonLinear), bit-exact vs NumPy 2.4.2 across all 15 dtypes -ff68bf14.astype(copy: false)never mutates the caller's array on a dtype conversion, matching NumPy -e5274cdc.ndarray.view(dtype)of a different-itemsize dtype now follows NumPy 2.x's last-axis-contiguous rule, soarr[::2].view(int32)works instead of throwing -970ee7f1.np.matmulgains the full ufunc keyword surface (out=/axes=/axis=/keepdims=/dtype=/casting=/order=), andnp.dot/np.outergainout=-73019dce(+87ff5797).argmax/argminbugs the sort audit exposed - the Decimal and Char flat paths and a NaN-tie ordering - now match NumPy 2.4.2 -8cad3025.np.uniquefull-parameter parity with NumPy 2.4.2 - the axis path's slab equality is corrected so each NaN sub-array is distinct and signed-zero sub-arrays collapse (a real unique-row-count bug for floats/complex),sorted=/equal_nan=are accepted, an out-of-range axis raises the verbatimAxisError, the bare-return overloads (np.unique(ar, axis: 0)) andintersect1d(return_indices:)now port verbatim, andUniqueResultfields are case-identical to NumPy -9f573dd5,262eefd7,0151a832.np.linalgfactorisations without a backend now raise a typedOpenBlasMissingBackendException- derives fromNotSupportedExceptionso existing catches still work, and names theNumSharp.Interop.OpenBLASpackage to install (was a bareNotSupportedException) -d1347c36.b2a8374b:np.ascontiguousarray/np.asfortranarray- a 0-D input returns a length-1 view (shares storage), matching NumPy's ndim≥1 contract.np.eye/np.ones-Charfills numeric one U+0001, not the character'1'.np.full_like- preserves the source array's dtype;fill_value's CLR type no longer selects the result dtype.np.linspace- floors inexact values before an integer-dtype cast and pins the endpoint tostopexactly.np.einsum- a scalar (ndim==0) contraction keeps its()shape instead of promoting to(1,).np.angle(deg: true)- a 0-DHalf/Singleresult keeps its float tier instead of promoting toDouble.var/std/cumsum/cumprod/all/any) andndarray.fillnow run at unlimited ndim like the rest of NumSharp -8f34e8ff,7fa96750.🧰 Testing & Tooling
out=/where=(3,727 cases over out × mask layouts), result-kinds + verbatim-error + iterator-trace, IEEE special-values (nan/±inf/±0/subnormal), and a truthful-vs-precise precision channel -bc91dd25,6cd1de9b,0882edbb,359e9d3c,76f0c918.np.randomsampler byte-parity divergences (f/pareto/standard_cauchy/binomial/negative_binomial/multinomial/multivariate_normal/gamma(shape<1)), now pinned as known[OpenBugs]issues (not yet fixed) -31a178f2.cf559a1a,03415ec9,5ff54a72.💥 Breaking Changes
np.random.bytes/Generator.bytesnow returnNDArray<byte>instead ofbyte[], so draws >2 GiB succeed (NumPynpy_intpparity) -44d2e7d9.ndarray.stridesnow reports bytes per axis (was elements), matching NumPy'sPyArray_STRIDES-6ef30215.np.unique(ar)now returns aUniqueResultstruct instead of a bareNDArray, sonp.unique(ar)[k]selects the k-th output (use.values[k]for the k-th value); it converts implicitly toNDArray/NDArray[]so most call-sites are unchanged -17f571ef.np.mgrid/np.meshgriddrop their legacy non-NumPy signatures:mgrid[...]is now an indexer (was a 2-arg method) andmeshgridis variadic returningMeshgridResult(was a fixed 2-tuple +Kwargs) -7f558d05,4e8c3925.PublicKeyTokenchanges fromnulltocc7b13ffcd2ddd51(published NumSharp had shipped unsigned since 2019); every consumer (TensorFlow.NET, Pandas.NET, Gym.NET) must recompile -478d550d.NUMSHARP_<AREA>_<SETTING>scheme with no back-compat aliases -NUMSHARP_GUARD_PAGES(shipped in 0.60.0) becomesNUMSHARP_DEBUG_GUARD_PAGES, and the OpenBLAS/pythonnet knobs take_LIBRARY/_SEARCH_PATH/_USE_BUNDLED/_PYPI_FEED_URL/_REQUIRE_ENGINEnames -079d1859.NDArray.Normalize()(a non-NumPy extension) is marked[Obsolete]in favour ofnp.clip()-b701843e.