Skip to content

[Release 0.70.0] ~85% NumPy 2.x API coverage: 100+ new np.* APIs, new nuget packages (OpenBLAS & pythonnet), 3 living dashboards - #628

Draft
Nucs wants to merge 418 commits into
masterfrom
journey3
Draft

[Release 0.70.0] ~85% NumPy 2.x API coverage: 100+ new np.* APIs, new nuget packages (OpenBLAS & pythonnet), 3 living dashboards#628
Nucs wants to merge 418 commits into
masterfrom
journey3

Conversation

@Nucs

@Nucs Nucs commented Aug 23, 2026

Copy link
Copy Markdown
Member

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.

  • NumSharp.Interop.OpenBLAS - new TensorEngine.Blas BLAS+LAPACK backend (NumPy's own dependency): powered by OpenBLAS, byte-identical to NumPy 2.4.2; Core stays 100% managed without it but lacks support for most of the functions.
    • Delivery - bundles the exact binaries NumPy 2.4.2 pinned dependency version (the scipy-openblas64 / scipy-openblas32 PyPI packages), per-RID for 8 platforms; enable/disable at runtime. Supports PyPI version pin and build-time download with auto-install at runtime.
    • Products - dot, matmul, inner, vdot, vecdot, matvec, vecmat, tensordot, multi_dot, matrix_power.
    • Linear systems & inverses - solve, inv, det, slogdet, tensorsolve, tensorinv.
    • Decompositions - cholesky, qr, svd, svdvals.
    • Eigenproblems - eig, eigvals, eigh, eigvalsh.
    • Least-squares & SVD-derived - lstsq, pinv, matrix_rank, cond, norm.
    • Sliding dot - correlate, convolve.
  • NumSharp.Interop.pythonnet - zero-copy NumSharp ↔ Python via Python.NET; any numpy / any Python, no Numpy.NET dependency.
    • Explicit - arr.ToNumpy() / arr.ToPython() out; pyObj.AsNDArray() / pyObj.FromArrayLike() in.
    • Implicit - RegisterCodec() once, then pythonnet's own obj.ToPython() / pyObj.As<NDArray>() round-trip transparently.
    • Mode Copy vs view - Auto (view when possible, else copy), View (share or decline), Copy (always independent).
    • Buffer protocol - FromArrayLike imports any PEP 3118 exporter, strided/offset/reversed included; read-only stays non-writeable:
      • numpy arrays (numpy);
      • memoryview, bytes, bytearray, array.array, ctypes arrays (Python stdlib);
      • PIL images (Pillow);
      • tensors (PyTorch);
      • plus anything exposing array_interface (e.g. pandas) - and plain list/tuple/nested sequences via numpy's asarray.
    • Lifetime & GIL - GC-safe leases, optional GIL control, live export/import counters for leak checks.
    • Dependency - pythonnet 3.0.5+ (Python 3.7-3.13, and future 3.x).

📊 Dashboards & Docs

Three living dashboards ship on the documentation site, each generated from the same CI artifacts the release gates run on.

  • Supported Features Dashboard - NumPy 2.x API coverage & support: every public NumPy API in scope, its NumSharp equivalent, known limitations and C# overloads, and the coverage-score math. Headline ~85% (478/560), with np.random / np.fft / np.linalg at 100%.
    • Surface scoreboard (top-level · ndarray · random · linalg · fft), a deterministic capability map, and a searchable API explorer.
  • Benchmark Dashboard - the NumSharp-vs-NumPy performance lab: 18 op suites × all dtypes × three cache tiers (1K/100K/10M), plus six scans (iterator, layout, operand, cast, fusion, native OpenBLAS/LAPACK); 456/456 benchmarkable APIs have evidence.
    • NumPy÷NumSharp heatmaps with drill-down, published as release-tracked history snapshots (not scratch output).
  • Tests & Oracle Dashboard - the correctness/verification lab: reflected MSTest inventory (net8.0 + net10.0), the committed NumPy 2.4.2 differential-fuzz corpus (116K+ cases, bit-exact, no Python in CI), independent Decimal evidence, format/index oracles, known-bug gates, and live interop suites.

✨ New APIs & Modules

  • np.random.default_rng - the full modern PCG64 Generator, byte-identical streams to NumPy 2.4.2 - e868d8ae (+ 754b7476, febfbbdd, f491c499).
    • default_rng - entry point (seed / SeedSequence / BitGenerator / PCG64 overloads).
    • 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.
  • Iteration objects - NumPy 2.4.2 parity over the NDIterRef engine (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.
  • The issue Sorting & searching: implement partition/argpartition, lexsort, nanargmax/nanargmin, sort_complex #623 sorting/searching six - NumPy 2.4.2 parity, ~2,170 fuzz cases bit-exact - 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 (the argsort/argmax inverse), 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).
  • Array-API unique family - unique_values, unique_counts, unique_inverse, unique_all, 102/102 bit-exact vs NumPy 2.4.2 across 13 dtypes - bec1c497.
  • The np.diag family + 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.
  • Linear-algebra product family - new managed np.* products; byte-parity via the OpenBLAS backend when referenced - 53d7764f (+ 81509766, 297f883f, 74aa5d5a).
    • inner, vdot, vecdot, matvec, vecmat, tensordot, multi_dot, matrix_power.
  • Polynomial family - NumPy 2.4.2 parity, pure functions bit-exact - 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.
  • Text I/O - byte-exact with NumPy, savetxtloadtxt round-trips - a1920a4a, 17a1ff8a (+ 80a0ed50, d39ff824).
    • np.savetxt, np.loadtxt, np.fromstring.
  • Inverse-hyperbolic trig - byte-exact vs NumPy 2.4.2 - 9fa48041 (+ 615f1ee5).
    • arcsinh, arccosh, arctanh - primary ufuncs; asinh, acosh, atanh - Array-API aliases.
  • Array-API device conformance (CPU shim) - ebba2cbf.
    • ndarray.device, ndarray.to_device, and device= on array / zeros / ones / empty / arange / ….
  • Additional array, linalg & stats functions -
    • 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.
  • The np.linalg factorisation surface and complex128 dot/matmul are listed under New NuGet Packages above (they compute via the OpenBLAS backend) - dc448acc, f5ec6276, d09e4376, 6ee562da.

🧩 ndarray surface

  • ndarray member parity with NumPy 2.4.2 -
    • data - the memoryview buffer object (np.MemoryView); accepted zero-copy by array / 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.
    • +14 instance methods (all, any, clip, take, repeat, squeeze, trace, …) - 06869352.

⚡ Performance

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

  • 1088 bytes -> 192 bytes - NDArray allocation size has been reduced by utilizing StructLayout.Explicit.
  • x0.98->x74 - np.unique family routed through the radix sort core - 5df10897 (+ 35d12699).
  • x1.6->x5.2 - percentile / median / quantile pivot-stack block-partition quickselect - 8a1376ff.
  • x0.4->x2.75 - np.argpartition on the same block/pivot-stack path - 75a1d873.
  • x1.35->x11 - np.isin hash-set membership replaces sort+searchsorted - bd96d541.
  • x15.6 - blocked GEBP double GEMM for transposed-B dot (2.9→43 GFLOP/s) - 97e9e82a (+ 7d680eb1).
  • x40->x249 - typed np.nditer<T> / nditer_chunks<T>, allocation-free iteration (chunks + Vector<T> hits 249×) - d58f3728.
  • x1.0->x4.5 - take / put / place element-copy specialization + gather prefetch (take went from x0.68 losing to winning everywhere) - 88550d13.
  • x1.04->x11.7 - float32 exp / log / sin / cos / tanh + rad2deg / deg2rad reimplemented as bit-exact NumPy kernel ports (tanh also 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_type key promotion (no more silent key down-cast) + NaN-as-largest total order - 93abe13d, cc676ea8, f2cefba2.
  • np.take / np.put index validation matches NumPy - a negative index under mode='raise' normalizes once (np.take(a, [-1]) addresses the last element instead of throwing), and a non-castable float/complex index raises the verbatim TypeError instead of silently truncating - fc10404d, 88550d13.
  • np.correlate / np.convolve - OpenBLAS byte-parity via the new sliding-dot seam - d0be3132.
  • Broadcast write semantics - broadcast_to is read-only, broadcast_arrays is writeable, and writing a non-writeable view now raises NumPy's verbatim message instead of silently corrupting the shared source - 1eadb83b, 6fb518c0, 1cc67d47 (+ baf41c89).
  • Allocation & reshape guards - size×itemsize overflow, reshape(-1, …), and expand_dims axis now raise NumPy's verbatim texts instead of silent wrong-size allocations or raw .NET exceptions - c2552d6a.
  • Empty / zero-sized array and float16-matmul edge cases now match NumPy - float16 products accumulate in float32 (no more ones(3000)@ones(3000)=2048 saturation), stacked/fancy indexing into zero-sized arrays, and the 0-d boolean setter - 03d0f0c8, 7636100a, f6e258c0.
  • Fancy-set into a non-contiguous destination no longer silently corrupts the view (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, so arr[::2].view(int32) works instead of throwing - 970ee7f1.
  • np.matmul gains the full ufunc keyword surface (out=/axes=/axis=/keepdims=/dtype=/casting=/order=), and np.dot/np.outer gain out= - 73019dce (+ 87ff5797).
  • Three engine argmax / argmin bugs the sort audit exposed - the Decimal and Char flat paths and a NaN-tie ordering - now match NumPy 2.4.2 - 8cad3025.
  • np.unique full-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 verbatim AxisError, the bare-return overloads (np.unique(ar, axis: 0)) and intersect1d(return_indices:) now port verbatim, and UniqueResult fields are case-identical to NumPy - 9f573dd5, 262eefd7, 0151a832.
  • np.linalg factorisations without a backend now raise a typed OpenBlasMissingBackendException - derives from NotSupportedException so existing catches still work, and names the NumSharp.Interop.OpenBLAS package to install (was a bare NotSupportedException) - d1347c36.
  • Six creation/math/linalg/stats functions brought to NumPy 2.4.2 parity - 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 - Char fills 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 to stop exactly.
    • np.einsum - a scalar (ndim==0) contraction keeps its () shape instead of promoting to (1,).
    • np.angle(deg: true) - a 0-D Half / Single result keeps its float tier instead of promoting to Double.
  • Removed the last 64-dimension caps - axis reductions (var/std/cumsum/cumprod/all/any) and ndarray.fill now run at unlimited ndim like the rest of NumSharp - 8f34e8ff, 7fa96750.

🧰 Testing & Tooling

  • The NumPy differential-fuzz oracle gained new tiers - FFT transforms, ufunc 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.random byte-parity + CBLAS product-value + axis-precision oracle tiers - the seeded-stream gate surfaced 8 np.random sampler 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.
  • First host-pinned differential-fuzz coverage for the OpenBLAS-backed LAPACK factorisations (eigen/SVD/QR/Cholesky + the LU family, 366 cases byte-exact) plus the polynomial / einsum / cross / cov families - cf559a1a, 03415ec9, 5ff54a72.

💥 Breaking Changes

  • np.random.bytes / Generator.bytes now return NDArray<byte> instead of byte[], so draws >2 GiB succeed (NumPy npy_intp parity) - 44d2e7d9.
  • ndarray.strides now reports bytes per axis (was elements), matching NumPy's PyArray_STRIDES - 6ef30215.
  • np.unique(ar) now returns a UniqueResult struct instead of a bare NDArray, so np.unique(ar)[k] selects the k-th output (use .values[k] for the k-th value); it converts implicitly to NDArray / NDArray[] so most call-sites are unchanged - 17f571ef.
  • np.mgrid / np.meshgrid drop their legacy non-NumPy signatures: mgrid[...] is now an indexer (was a 2-arg method) and meshgrid is variadic returning MeshgridResult (was a fixed 2-tuple + Kwargs) - 7f558d05, 4e8c3925.
  • Every NumSharp assembly is now strong-named - PublicKeyToken changes from null to cc7b13ffcd2ddd51 (published NumSharp had shipped unsigned since 2019); every consumer (TensorFlow.NET, Pandas.NET, Gym.NET) must recompile - 478d550d.
  • Environment variables were hard-renamed to a consistent NUMSHARP_<AREA>_<SETTING> scheme with no back-compat aliases - NUMSHARP_GUARD_PAGES (shipped in 0.60.0) becomes NUMSHARP_DEBUG_GUARD_PAGES, and the OpenBLAS/pythonnet knobs take _LIBRARY / _SEARCH_PATH / _USE_BUNDLED / _PYPI_FEED_URL / _REQUIRE_ENGINE names - 079d1859.
  • NDArray.Normalize() (a non-NumPy extension) is marked [Obsolete] in favour of np.clip() - b701843e.

Nucs added 30 commits August 15, 2026 11:00
…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.
Nucs added 23 commits August 28, 2026 17:09
…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.
@Nucs

Nucs commented Aug 28, 2026

Copy link
Copy Markdown
Member Author
image

Oh boy

Nucs added 6 commits August 28, 2026 21:56
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant