Skip to content

[0.81.0] NumPy 2.5.4, NumPy's own signatures, scalars and exception classes, 17 new np.* functions, TorchSharp interop, NumPy-parity fixes - #635

Merged
Nucs merged 243 commits into
masterfrom
release-0.81.0
Nov 13, 2026
Merged

Nucs merged 243 commits into
masterfrom
release-0.81.0

Conversation

@Nucs

@Nucs Nucs commented Nov 13, 2026 •

Copy link
Copy Markdown
Member

This release moves NumSharp to NumPy 2.5.4, gives its functions NumPy's own signatures and brings NumPy's scalars, every Python and NumPy exception class, and 17 new np.* functions.
An NDArray now shares its memory with TorchSharp tensors, in both directions.

🧭 Overview

  • NumPy 2.x API coverage around 96.2%, everything except datetime, complex64 and other exotic dtypes is fully supported, with byte-to-byte equality and compatibility to NumPy.
    • NumPy's scalars - a 0-d result is NumPy's read-only scalar, an element read a[i, j] returns one, and NumPy's 36 scalar classes are callable (np.float64(1.5), np.int8("12")).
  • NumPy 2.5.4 is the reference (0.80.0: NumPy 2.4.2) - every oracle test case is NumPy 2.5.4's answer, with NumPy 2.5's behavior across linear algebra, random, numpy.ma, np.fft, text, sorting, reductions and dtypes, its keyword-only descending= sort, and NumPy 2.5.4's own OpenBLAS in NumSharp.Interop.OpenBLAS.
  • Fresh results laid out as NumPy's - ufunc results take NumPy's trivial-loop C / F or NpyIter K layout down to the strides of length-1 axes, keepdims reductions and np.median lay out as NumPy's, and every zero-size array has NumPy's all-zero strides.
  • NumSharp on phones - proven end to end on two Android phones with ONNX Runtime's CPU, XNNPACK and NNAPI providers: an XNNPACK session's outputs now import without a copy, and float-to-int casts on Mono run 4.5-5.7x faster with NumPy's bytes.
  • New documentation - the TorchSharp, Exceptions and NumPy Scalars pages, and a diagram of NumSharp's 18 released integrations opening the README and the Interoperability page.
  • 36 NumPy-parity, memory-safety and tooling fixes, 2 measured speedups and 20 breaking changes, across the release's 243 commits.
  • 1 new NuGet package - NumSharp.Interop.TorchSharp.
  • 1,122,248 NumPy 2.5.4 oracle test cases (0.80.0: 1,119,305 NumPy 2.4.2 cases) and 21,115 test methods (0.80.0: 20,506).

📦 New NuGet Packages

  • NumSharp.Interop.TorchSharp - zero-copy interop between NDArray and TorchSharp's tensors in both directions, with Auto / View / Copy modes.

Detailed Breakdown

Show All

📦 New NuGet Packages

One optional companion package ships for the first time, co-versioned with NumSharp (it depends on NumSharp). It converts arrays between NumSharp and TorchSharp, with no engine backend and no native asset of its own (it calls the libtorch that TorchSharp loads), so Core stays 100% managed without it.

  • NumSharp.Interop.TorchSharp - zero-copy interop between NDArray and TorchSharp's torch.Tensor (the .NET bindings for libtorch, the engine under PyTorch), in both directions and without Python.
    • Modes - every verb takes Auto (a view when the dtype and the layout allow it, else a copy; the default), View (shares memory or throws, naming what prevents the view) or Copy (always independent).
    • Export - AsTensor hands the array's buffer to libtorch: every non-negative-stride layout (contiguous, sliced, transposed, stepped) crosses as the same strided tensor, a broadcast of writeable memory as torch's own expand, and libtorch releases the buffer when the last tensor over it is freed; ToTensor copies.
    • Import - AsNDArray views a CPU tensor's storage through its own detach(), so the view survives set_ and the tensor's Dispose; ToNDArray copies; a tensor that requires grad, and an expanded one, import read-only (t.detach().AsNDArray() is the writeable route).
    • Dtypes - the ten dtypes both sides share cross as themselves; uint16 / uint32 / uint64 / char / decimal export by NumPy's NEP 50 promotion (int32 / int64 / float64 / int32 / float64), and an explicit dtype converts under NumPy's casting rule (same_kind by default); complex64 and bfloat16 tensors import widened exactly under Auto / Copy; a reversed view, a read-only array, a sparse, conjugated or non-CPU tensor copies under Auto; text arrays and quantized or meta tensors are refused in every mode.
    • Checks - MayShareMemory tells whether a result shares the source's memory, ToScalarType / ToDType map the dtypes, and live export / import counters help leak checks.
    • Proven - a state matrix holds 45 array states x the ten shared dtypes x the three modes to NumPy 2.5.4's flags and torch.from_numpy's layout, and 24 tensor states to Tensor.numpy(), with the deliberate differences asserted (read-only, misaligned and off-16-byte complex128 arrays copy where from_numpy shares).
    • Depends - TorchSharp 0.100.0 or later, the managed package only, with no upper bound (tested on 0.100.0 and 0.107.0); you reference the libtorch flavour you run (TorchSharp-cpu or a CUDA package), and on macOS install Homebrew's OpenMP first (brew install libomp), which libtorch links by Homebrew's path.

📊 Dashboards & Docs

  • Supported Features Dashboard - headline 96.2% (538 / 559 in NumPy 2.5.4's catalog), 368 with NumPy's exact signature and 539 oracle-verified; its coverage generator 1.18.0 counts the whole NumPy API at 70.3% (2,747 of 3,908; 0.80.0's generator read 41.6% of 4,844), because it credits what NumSharp provides and stops counting what NumPy itself refuses.
    • The ndarray operators and numpy.char / chararray are credited from the oracle cases that run them, NumPy's scalar types against NDArray (which now models them), and StringDType's members against DType.
    • Calls NumPy refuses (np.sin.reduce, del a[i]) and the members a class only inherits (memmap, recarray, MaskedRecords, record) show as "Not counted" instead of gaps.
  • Tests & Oracle Dashboard - 1,122,248 committed NumPy 2.5.4 oracle test cases over 1,455 operations (0.80.0: 1,119,305 NumPy 2.4.2 cases over 1,448), and 21,115 test declarations (0.80.0: 20,506).
  • Benchmark Dashboard - benchmark evidence for 441 of 520 benchmarkable APIs (0.80.0: 452 of 518).
    • np.acos, np.asin, np.atan, np.atan2, np.pow, np.bitwise_invert, np.bitwise_left_shift, np.bitwise_right_shift, np.cumulative_sum and np.cumulative_prod, which 0.80.0 credited through the functions they alias, are functions of their own now with no benchmark of their own, and np.fromiter, np.fromfunction and np.ndim have none yet.
  • Documentation site - new pages and sections, whose code samples run as tests:
    • TorchSharp - the new bridge, with what shares and what copies for 45 array states and 24 tensor states beside NumPy's and PyTorch's own answers.
    • Exceptions - every Python and NumPy class NumSharp raises, in one hierarchy with its .NET base, Python's attributes and the catch footguns (OverflowError is not OverflowException).
    • NumPy Scalars - the class calls, read-only scalars, and a scalar against a 0-d array.
    • The README and the Interoperability page open with a diagram of NumSharp's 18 released integrations.

✨ New APIs & Modules

  • NumPy's scalars - NumSharp keeps no scalar classes of its own: NumPy's scalar is a read-only 0-d NDArray, made at NumPy's own C-API points (PyArray_Return, PyArray_Scalar).
    • np.float64(1.5), np.int8("12"), np.complex128(1, 2), np.str_(5), np.bytes_(5) - the class calls of NumPy's 36 scalar class names, ported from NumPy's *_arrtype_new (C# 14; NDScalarClassCalls.float64(x) on C# 12): 3,042 of 3,210 NumPy calls answered identically, every other one understood.
    • A 0-d result is the read-only scalar NumPy returns - every ufunc and operator over 0-d operands, np.clip, np.round, np.dot / np.vdot / np.inner, np.sinc, np.nan_to_num, np.interp and the reductions; an out= array stays the array it was.
    • A full integer index (a[1, 2], a[0] of a 1-D array, z[()], m[0, 1]) reads NumPy's scalar, a read-only copy of the element; every key of a scalar indexes a copy of its value (s[...] is a writeable 0-d array, s[0] NumPy's "invalid index to scalar variable.").
  • Python's and NumPy's exceptions - every exception class of CPython 3.12 and NumPy 2.5.4 (47 new), each under a .NET twin and then Python's hierarchy, with Python's constructors, attributes and str() texts, raised where NumPy and CPython raise them.
    • ArithmeticError, OverflowError, ZeroDivisionError, FloatingPointError, MemoryError, ArrayMemoryError, EOFError, RecursionError, ReferenceError, NotImplementedError - the arithmetic, memory and runtime families, under ArithmeticException, OutOfMemoryException and EndOfStreamException where .NET has a twin.
    • OSError with errno / strerror / filename / winerror and its 15 subclasses (FileNotFoundError, PermissionError, IsADirectoryError, ...), chosen by errno through OSError.Create as Python's class call chooses them.
    • UFuncTypeError, UFuncNoLoopError, UFuncBinaryResolutionError, UFuncInputCastingError, UFuncOutputCastingError - NumPy's ufunc refusals, with ufunc, dtypes, casting, from_ and to.
    • SyntaxError / IndentationError / TabError (the location), NameError / UnboundLocalError / AttributeError (name, obj), UnicodeEncodeError / UnicodeDecodeError / UnicodeTranslateError (encoding, object, start, end, reason), StopIteration (value), SystemExit (code), KeyboardInterrupt, GeneratorExit, ImportError / ModuleNotFoundError, LapackError, ConverterError - Python's attributes.
    • BaseExceptionGroup / ExceptionGroup - Python's groups, with subgroup, split and derive.
    • Every class takes .NET's (message, innerException), Python's raise X(msg) from e, and a bare X() reads Python's str().
  • New functions and modules (17 new np.* functions; the ten most used on GitHub: np.round, np.shape, np.ndim, np.fromiter, np.astype, np.row_stack (kept, although NumPy 2.5 removed it), np.pow, np.fromfunction, np.atan and np.atan2) -
    • np.round - NumPy 2's name for rounding (np.round_ is gone).
    • np.acos, np.asin, np.atan, np.atan2, np.pow, np.bitwise_invert, np.bitwise_left_shift, np.bitwise_right_shift - NumPy 2.0's Array-API names.
    • np.cumulative_sum, np.cumulative_prod - NumPy 2.1's scans, with include_initial.
    • np.ndim, np.shape, np.astype, np.fromiter, np.fromfunction, np.row_stack and the constant np.little_endian.
    • np.lib.stride_tricks.sliding_window_view and np.lib.stride_tricks.as_strided (NumPy's byte strides, kept inside the array's buffer).
    • np.lib.array_utils (byte_bounds, normalize_axis_index, normalize_axis_tuple) and np.lib.format (read_magic, read_array_header_1_0 / 2_0, write_array_header_1_0 / 2_0, read_array, write_array, header_data_from_array_1_0, dtype_to_descr, descr_to_dtype, drop_metadata).
    • np.ma.MaskedArray(...), new np.NDIndex(2, 3), new np.NDIterator(...), new np.Broadcast(...) and new NDCharArray(...) - NumPy's class calls, with NumPy's defaults.
  • descending= - NumPy 2.5's keyword-only descending sort on np.sort, np.argsort, ndarray.sort and ndarray.argsort: NaN still sorts last, ties land where NumPy's default introsort or its stable timsort / radix sort puts them, and a StringDType array, a masked array or a kind= beside it is refused as NumPy refuses it.
  • NumPy's one signature - one overload in NumPy's parameter order, with NumPy's names, defaults and Nones, over thirteen validated rounds (each pick compiled before and after at C# 12, with NumPy 2.4.2's answers as its tests): 1,047 NumPy names now take NumPy's exact signature (0.80.0: 719).
    • NDAxis - NumPy's axis argument (None, an int or a tuple) as one type, so np.sum(a, axis: (0, 2)) is one reduction over the set with NumPy's bits; taken by sum, prod, mean, var, std, max / amax, min / amin, the seven nan reductions, all, any, count_nonzero, ptp, median / percentile / quantile and their nan forms, average, flip, squeeze, expand_dims, moveaxis, roll, trim_zeros, size, gradient, fft.fftshift, linalg.norm / vector_norm and apply_over_axes, with the int and int[] forms kept beside it.
    • None wherever NumPy takes it - axis: null (it was ambiguous for nine functions), size: null for every np.random sampler, order, density, and the shape of the *_like family.
    • Creation - np.linspace and np.geomspace take array_like ends (float32 ends compute in float32, axis=, and retstep= for linspace), np.arange(start, stop, step, dtype, device) takes any array_like, np.zeros / ones / empty / full take order and device, np.reshape NumPy's copy modes, np.asanyarray copy, and Shape is a C# collection (np.zeros([2, 3]), foreach (long d in a.Shape)).
    • Keywords - np.stack (out, dtype, casting), np.hstack / np.vstack (dtype, casting), np.nansum / np.nanprod (initial, where), np.nanmean / np.nanvar / np.nanstd (where, mean, correction), np.ldexp (dtype), np.frexp / np.modf (out=(o1, o2)), np.tensordot (axes=(a, b)), np.require (any iterable of requirements), and np.interp's fill values converted as NumPy converts them.
    • Result structs - np.intersect1d(return_indices), np.average(returned), np.indices(sparse) and np.linspace(retstep) return one value that converts to the array and deconstructs into NumPy's tuple.
    • numpy.ma - the 48 masked ufuncs take out / where / dtype, the reductions tuple axes and NumPy's out=, ma.sort / ma.argsort take kind / order / stable, ma.median out / overwrite_input, ma.array / masked_array NumPy's argument order, and MaskedArray gains base, device, to_device, getfield, setfield and item(*args).
    • finfo - nmant, iexp, nexp, machep and negep; np.iinfo / np.finfo read dtype strings as NumPy does.
  • The bit generators' State snapshots (MT19937, PCG64, PCG64DXSM, Philox, SFC64) and poly1d are unsealed: a subclass engine carries its own state, and a poly1d subclass's operations return a plain poly1d, as NumPy's do.

🧩 ndarray surface

  • astype(dtype, order, casting, copy), reshape(shape, order, copy) (and loose integers), transpose(*axes), dot(b, out) and the reductions' tuple axes - NumPy's method signatures on NDArray, NDArray<T>, NDMatrix, NDCharArray and NDMaskedArray.

⚡ 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.

  • x4.0->x6.7 - np.argpartition / np.partition with one kth near either end of a long contiguous line (a top-k) find it in one read: a 51,200-wide float32 row's top 5 and 1M elements (the argpartition was x0.44).
  • x1.6->x2.0 - the ONNX Runtime and ML.NET bridges' Postprocess.TopK selects with np.argpartition (and np.argmax for k = 1) instead of sorting the whole row, measured against NumPy's argpartition idiom over a 51,200-wide row (it was x0.12).

🎯 Parity & Fixes

  • NumPy 2.5's behavior, as NumPy 2.5.4 answers -
    • Linear algebra - np.linalg.eig / eigvals always return complex, svd / lstsq refuse inf or NaN with LinAlgError before LAPACK runs, cond reads a failed SVD as NaN and p='nuc' sums the singular values, svd(hermitian=True) signs a zero eigenvalue +1, and tensorsolve returns a view.
    • Random - Generator's binomial, negative_binomial and multinomial draw through NumPy 2.5's corrected BTPE (RandomState keeps the legacy stream), SeedSequence refuses nested sequences and spawn a negative count, np.random.seed over a swapped bit generator drops the cached Gaussian, and choice / multivariate_normal return views.
    • numpy.ma - a stored fill_value no longer follows a dtype change (converted with its errors caught, else dropped), mask_or passes shrink on, and the shared-mask resize text is NumPy 2.5's.
    • np.fft - ifft2, irfft2 and hfft write the caller's out= (it was checked, then ignored), and the forward transforms' invalid-norm text is NumPy 2.5's.
    • Text - StringDType compares every byte (trailing NULs are characters), a loop takes the effective NA descriptor, products over text resolve a common descriptor, bytes cast to StringDType are checked as UTF-8, and multiply / replace / zfill / slicing follow NumPy 2.5's count and overflow rules.
    • Sorting and searching - np.searchsorted runs NumPy 2.5's batched bisection and checks a sorter's middle entry even with no keys, and np.unique and the set routines keep each NaN's own bits.
    • Reductions - np.cumsum / np.cumprod refuse an out= whose dimensions differ from the input's, and the quantile family answers several reduced axes beside a kept axis of length 0 (an empty result, not a reshape error).
    • Dtypes - np.finfo(null) raises TypeError and a non-float dtype names NumPy 2.5's text, np.isdtype accepts a StringDType descriptor, a StringDType sub-array ('2T') is NumPy's TypeError, the 'a' alias is gone, and np.sctypeDict / np.typecodes raise KeyError for a missing key.
    • Core - np.where makes a Python-literal operand in the common dtype (an out-of-range literal is OverflowError; it wrapped), take / compress check out= in NumPy 2.5's direction, float16 nextafter of equal zeros returns y, and ndarray.resize's refusal is NumPy 2.5's wording.
  • np.partition / np.argpartition with several kth, and the quantile family, run NumPy's introselect_ statement for statement: equal values land where NumPy puts them (the bytes, the sign of a ±0 tie in np.percentile / np.quantile, argpartition's tied indices, which NaN a NaN slice answers), with a single kth and the speed kept; a zero np.median is +0.0 and a complex median follows np.mean's statements (np.median([1+infj]) is nan+infj).
  • float32 / float64 np.prod multiplies in NumPy's sequential multiply.reduce order on every route: NumPy's last bits, and an overflow before a zero is NaN (it was 0).
  • A fresh ufunc result takes NumPy's trivial-loop C / F allocation or NpyIter's K order, down to the strides of length-1 axes; a keepdims reduction keeps its axis at NumPy's stride, np.median is laid out as NumPy's mean of the partitioned copy (keepdims a stride-0 view), and every zero-size allocation has all-zero strides.
  • np.ldexp of a contiguous view at an offset (an np.split child) reads the view (it scaled the owner's first elements), and np.positive of a read-only array is writeable.
  • np.unwrap(p, 4) reads 4 as discont, NumPy's second parameter (it was the period-4 unwrap, a silent wrong answer).
  • np.empty_like(a, null, 'F') is NumPy's F-ordered array of a's shape (the order letter bound the Shape parameter and returned a (70,) array).
  • np.zeros(t.Shape), np.ones, np.empty and np.full of a transposed view's shape are C-ordered, as NumPy's np.zeros(t.shape) is (they were F).
  • np.arange with C# float bounds is float64, as NumPy's Python floats give (it computed in float32), and a C# ulong bound is a Python int (np.arange(18446744073709551610UL, 18446744073709551615UL, 2UL) was an empty float32 array).
  • np.linspace keeps integer ends past 2^24 exact (np.linspace(-20000001, 20000001, 3); they were rounded to float32).
  • np.asanyarray(300, np.int8) raises NumPy's OverflowError (it wrapped to 44), and np.asanyarray(5, np.str_) is <U1.
  • np.finfo(float32 / float64).minexp is NumPy's -126 / -1022 (it was C's -125 / -1021), and float32's smallest_normal / tiny are exactly 2^-126.
  • item() of a non-size-1 array and item(i, j) with the wrong number of indices raise NumPy's ValueError and texts, and np.repeat's negative-count text ends with NumPy's period.
  • np.array(["SFFF", "FHFH", "FFFH", "HFFG"], "c") splits each text into its characters, NumPy's character dtype (Gymnasium's maps), and keeps the 'c' type.
  • np.subtract of two bool arrays with dtype=float64 or int8 computes as NumPy's (only the bool loop refuses).
  • np.all / np.any of a numpy.matrix along an axis return NumPy's matrix (they returned plain arrays).
  • np.unravel_index / np.ravel_multi_index take shapes past int32 (a (2**40, 3) shape unravels).
  • numpy.ma -
    • np.ma.clip with a masked bound masks those elements (the bound's mask was lost).
    • np.ma.make_mask(m, False) / np.ma.mask_or(m1, m2, False) read False as NumPy's copy (it was shrink).
    • np.ma.choose reads the choices filled with their own fill values (it read the raw data), and MaskedArray.choose is ndarray.choose (it ran the module form).
    • An F-ordered astype / *_like result gets an F-ordered mask (it was C).
  • Smaller alignments -
    • np.isscalar(np.array("x")) is False, as for any array np.array builds.
    • np.concatenate reports a later 0-d array as NumPy's rank mismatch.
    • np.fft.fftshift / ifftshift of a 0-d array raise NumPy's error (they returned a copy).
  • NumSharp.Interop.OpenBLAS - bundles OpenBLAS 0.3.34.106.0, the build NumPy 2.5.4 ships (byte-identical to its library), so the products and LAPACK factorizations give NumPy 2.5.4's bits; the build-time version override now finds glibc Linux wheels by platform family, so it can move between manylinux2014 and manylinux_2_27 / 2_28 releases (it found no wheel).
  • NumSharp.Build - deconstructing a result struct (np.meshgrid, np.unique, np.gradient, the histograms, ...) after a deconstruction that held matrices returned the previous statement's arrays; the weaver no longer touches a Deconstruct.
  • NumSharp.Interop.OnnxRuntime - the outputs of an XNNPACK session (ONNX Runtime's Android / iOS CPU accelerator) import without a copy (they were refused as device memory), proven end to end for the CPU, XNNPACK and NNAPI providers on two Android phones, and the synchronous Run's own overhead drops 30% per step of a 27-input decoder.
  • Float to int8 / uint8 / int16 / uint16 / int32 casts on hosts without the x86 vector cast (Mono's .NET for Android, iOS and WebAssembly) no longer call Math.Truncate per element, which Mono compiles to a call: 4.5-5.7x faster on an arm64 Android phone, with NumPy's bytes for all 2^32 float32 inputs.

🧰 Testing & Tooling

  • NumPy 2.5.4 is the oracle's NumPy, written once in numpy-version.txt (the generators, the coverage generator, the workflows, the interop environments and the C# corpus gates read it), and every corpus and fixture is regenerated under it: the cases NumPy 2.5 fixed return (StringDType byteswap / place, raw text cell copies in choose / put / putmask, invalid UTF-8, tofile of non-ASCII text), and the sort tier gains descending=True.
  • A second NumPy parity QA journey - about 372,000 probe comparisons over 160 log rows (random fuzzers, an ambiguity fuzzer compiling NumPy's call spellings at C# 12 and 14, NaN-identity, aliasing and layout sweeps): 62 findings, 58 of them new, each an [OpenBugs] reproduction with a journal entry; 9 are fixed in this release.
  • Every answer of the 0.80.0 issue triage (34 issues closed, 4 progress comments) is a test: 35 Issue<n> classes assert each answer's claims against NumPy 2.4.2, and each gap an answer admits is an [OpenBugs] pin.
  • The np.* signature gap map is validated against its ground truth (the C# compiler for missing keywords, NumPy's runtime for refusals, tuples and defaults; 19 findings refuted), and docs/plans/API_PARITY_DIFF.md lists each change as a patch: the list the thirteen rounds were picked from.
  • Oracle tiers - cast_boundary (float32 / float64 -> int8 ... int32 at every integer edge, NaN payloads and infinities, zero excuses; all 2^32 float32 inputs proven once against NumPy), the layout-parity ufunc family (263 cases), np.linspace over array ends, and the surface tier's cumulative_sum / cumulative_prod, fromiter, fromfunction and sliding_window_view.
  • Releases pause before nuget.org until the owner approves the deployment: the publish job runs in the GitHub environment nuget, and the nuget.org Trusted Publishing policy accepts only that environment.

💥 Breaking Changes

  • NumPy's parameter names (calls that name the old ones must be renamed): np.transpose(a, axes) (was premute), np.array_equal(a1, a2), np.iscomplex / isreal / iscomplexobj / isrealobj(x), np.isscalar(element), np.array_repr(arr), np.cumsum / np.cumprod(a, axis, dtype, out) (were arr / typeCode), reshape(shape) (was newShape), np.unravel_index(indices, shape), np.ravel_multi_index(multi_index, dims, mode), np.insert(arr, obj, values), np.tensordot(a, b, axes: (a, b)) (were two lists), np.ma.default_fill_value(obj), np.ma.size(obj) and MaskedArray.clip(min, max).
  • Removed names: np.round_ (use np.round), np.average_returned / np.ma.average_returned (returned: true), np.indices_sparse (sparse: true), the generic np.zeros<T> / ones<T> / empty<T> / full<T> / argsort<T> / finfo<T> / iinfo<T> / can_cast<TFrom, TTo> (pass the dtype), NDArray.Normalize() and AxisOutOfRangeException.
  • Calls that no longer compile: np.zeros(shape, 'F') (name order: 'F'; NumPy reads that letter as a dtype), np.tile(a, 2, 2) (np.tile(a, (2, 2))), np.random.random_sample(2, 3) / rand(Shape) (random_sample((2, 3)) / rand(2, 3)), astype(t, false) (astype(t, copy: false)), np.frexp(x, m, e) (out: (m, e)), np.ma.array(data, mask) (name mask:), np.ma.reshape(a, 2, 3) (a tuple), np.fft.fftfreq with a float n, dtype.newbyteorder('>') (">"), an object-typed value to np.insert / np.append, and a method group of a unary ufunc (Select(np.exp) needs a lambda).
  • Positional arguments that now land on NumPy's parameter: the fourth argument of np.average / np.ma.average is returned (it was keepdims, so name keepdims:; a silent change), the second argument of np.unwrap is discont (it was the period; silent too), and a keepdims passed after the axis of np.all / np.any / np.argmax / np.argmin lands in out and raises NumPy's TypeError.
  • Refusals that follow NumPy: np.can_cast refuses a Python number with NumPy's NEP 50 TypeError (pass an array or a dtype), np.isdtype / np.issubdtype / np.iinfo / np.finfo refuse arrays (pass a.dtype), np.require raises KeyError for an unknown requirement (it was ValueError), and np.ma.amax / np.ma.amin take no fill_value.
  • NpzFile has NumPy's names: files, f, zip, fid, allow_pickle, max_header_size, keys(), values(), items(), get() and close() (Files, F, Zip, AllowPickle, MaxHeaderSize and Close() are gone).
  • Engine and subclass hooks: the TensorEngine reductions (ReduceAdd ... NanStd) and CountNonZero take NDAxis?, NDArray._collapse(result, NDAxis?) replaces the int? hook, TensorEngine.Geomspace / Interp take arrays, and UnravelIndex / RavelMultiIndex take long[] dims.
  • NDArray.Scalar(...) returns NumPy's read-only scalar (a write raises ValueError); build a mutable 0-d buffer with np.array(x) or np.zeros(Shape.Scalar, dtype).
  • A 0-d result of a ufunc, an operator, np.clip, np.round, np.dot and the other return points is a read-only scalar, and np.isscalar(np.array(5)) is False.
  • An element read (a[i, j], a[i] of a 1-D array) is a read-only copy, not a writeable 0-d view: write through the setter (a[i, j] = v); a read costs ~0.55 us (the view 0.36 us), so a hot loop keeps GetValue<T> / GetDouble.
  • NumPy's errors are Python's classes: catch (OverflowException) / (DivideByZeroException) / (FileNotFoundException) / (KeyNotFoundException) no longer see them (catch OverflowError, ZeroDivisionError, FileNotFoundError, KeyError, or the .NET bases ArithmeticException / IOException), and an exact-type check for TypeError no longer matches a ufunc refusal (a UFuncTypeError).
  • A bare Python-named exception's Message is Python's str() (""; it was the class name or .NET's default text); numpy.ma's tofile / compress_rows / compress_cols / compress_rowcols / mask_rowcols, notmasked_contiguous over more than 2-D and a seedless SeedSequence raise NumPy's NotImplementedError, a RuntimeError (they raised NotImplementedException / NotSupportedException); and item() / MaskedArray.resize raise ValueError (they raised IncorrectSizeException / NotSupportedException).
  • .strides of fresh results follow NumPy's: length-1 axes of trivial-loop results and of keepdims reductions, every zero-size allocation (np.zeros((2, 0)).strides is (0, 0)), np.median(..., keepdims: true) (a stride-0 view, OWNDATA false), and np.zeros / ones / empty / full, which lay out from the dimensions alone (an F Shape gives C; pass order: 'F').
  • float32 / float64 np.prod multiplies sequentially, as NumPy does: the last bits change, an overflow before a zero is NaN, and a flat product runs at NumPy's speed (the SIMD lanes ran it 6-16x faster).
  • np.partition / np.argpartition with several kth arrange equal values as NumPy's introselect does, so the bytes around tied values change.
  • np.arange with C# float bounds returns float64 (np.arange(0f, 1f, 0.1f) was float32).
  • np.append of a C# number appends NumPy's array of the Python number: np.append(int32_array, 5) is int64 and np.append(float32_array, 1.5f) float64 (they kept the array's dtype).
  • NumPy 2.5's answers: np.linalg.eig / eigvals return complex even when every eigenvalue is real (take .real), a seeded Generator's binomial, negative_binomial and multinomial draw NumPy 2.5's corrected stream (np.random.* and RandomState keep the legacy one), and np.unique / the set routines keep a NaN's own bits (they returned the canonical NaN).
  • NumPy 2.5's refusals: np.cross / np.linalg.cross of 2-element vectors (give them a zero third component), np.cumsum / np.cumprod into an out= whose dimensions differ from the input's, the 'a' dtype alias (np.dtype("a5"); write "S5"), and np.finfo(null) (a null gave float64's limits).
  • NumPy 2.5's text rules: a StringDType text's trailing NULs are characters (its length, the comparisons and strip see them), byteswap of a StringDType array raises NumPy's TypeError (it copied), a view or getfield through a separate StringDType() descriptor raises, and bytes that are not UTF-8 no longer cast to StringDType.

Nucs and others added 30 commits November 5, 2026 19:30
…ry_trivial_single_output_loop's C / F allocation, NpyIter's K order otherwise (P19, parity QA-36, with QA-09 / QA-35) - and the functions built on it follow: the scans (bool too), np.where, np.array, np.round, np.positive, keepdims reductions, zero-size allocations and the memoryview's strides; 17 oracle gaps (and VF08's median half) return to the gate

The owner's P19 decision of 2026-11-05 ("Port KEEPORDER now", with the comment "With parity to Numpy"): an
elementwise result of a permuted, F or broadcast operand was C-ordered (only a strictly F operand set kept F), so
`np.ravel(np.negative(p), order='K')` read other values than NumPy's and a reduction over a computed operand summed in
another order (F79's 1-3 ulp Softmax / LogSoftmax drift through the interop bridges). Closes parity QA-36, and QA-09 and
QA-35 with it.

NumPy's rule, ported once (numpy/_core/src/umath/ufunc_object.c, nditer_constr.c):
- try_trivial_single_output_loop: taken with no where= mask, one output, and no input of two or more dimensions that
  needs a cast or is unaligned (check_for_trivial_loop). 0-d inputs are skipped; all other inputs need the same shape;
  a 1-D input fixes no order; every N-D input must be C- or F-contiguous with the SAME (C, F) flag pair as the first.
  The result is F when that pair is F-only, else C.
- Otherwise NpyIter's K order (Shape.NpyIterOutputShape, npyiter_fill_axisdata + npyiter_find_best_axis_ordering +
  npyiter_new_temp_array): an axis abstains for broadcast extent 1, a missing axis, operand extent 1 or stride 0; the
  insertion sort by |stride| lets C order win a conflict; an allocated output never flips a negative stride.
- The two allocators differ only in the strides of extent-1 axes (np.negative of an F (2, 1, 3) int32 array: (4, 8, 8)
  through the trivial loop; np.sqrt of it, a cast input: (8, 48, 16) through NpyIter).
- A fresh zero-size allocation has ALL-ZERO strides (PyArray_NewFromDescr_int: "Make sure all the strides are 0").

Core:
- Shape.UfuncOutputShape (View/Shape.StridePerm.cs) is that rule; DefaultEngine.UfuncResultLayout.cs decides it for an
  engine call (TryUnaryNonCLayout / TryBinaryNonCLayout / TryNonCResultLayout; the C fast path costs a flag test) and
  computes a non-C layout one of two ways: the LINEAR walk (every operand stored exactly as the layout: the whole-array
  kernels run over flat memory into the layout's result) or the PERMUTED walk (OperandInLayoutOrder views in the
  layout's dense axis order, the C routes, NDArray.AdoptLayout relabels the result). Two exact C answers skip the vote
  (VotesCOrder: every operand's strides already fall along its axes, as a strided or reversed C view's do;
  DecidesCOrder: one full-rank C operand, since C wins every conflict), and the trivial loop's C / F result reuses an
  input shape that already is that plain layout (PlainInputOrAdopt, as the C bypass's CanonicalResultShape does; the two
  share Shape.HasPlainStrides now).
- Engines: the binary (ExecuteBinaryOpResolved -> ExecuteBinaryOpInLayout / ExecuteBinaryOpCOrder), unary (with
  UnaryLoopCastsInput: predicates and logical_not read any dtype, signbit has float loops only, bitwise_count no bool
  loop, abs(complex) is D->d), comparison (the comparison dtype; int64 / uint64 is an exact pair, no cast), the
  float-tier binaries (logaddexp family), arctan2, the shifts, ldexp and power's stride-zero fast path. The old
  partial F handling is gone: ShouldProduceFContigOutput x2, AreAllOperandsStrictFContig, HasNonScalarBroadcastOperand,
  the reversed-axis walk and the "looser F" copies; the C routes (trivial bypass, NDIter, direct) now only ever
  produce C.
- Two-output ufuncs (np.divmod, np.modf, np.frexp) never take the trivial loop: NpyIter's layout, the mask voting for
  modf / frexp (TwoOutputsInNumPyLayout).
- np.where votes K over (condition, x, y) (WhereSelect behind a layout decision) and reads every operand from its
  view's offset (FirstElement): VA03 / RL02, silent wrong values from an np.split child, are fixed.
- MaterializeContiguous copies a C-contiguous view at a non-zero offset: np.ldexp read such a mantissa or exponent from
  its owner's start (RL03 / VA05).
- np.array(nd) is nd.copy('K'): a writeable K-order copy (QA-35; a read-only source gave a read-only copy, a broadcast
  copy was C).
- np.positive is a ufunc result, not a storage clone: the ufunc layout, always writeable (VF06's last case and VF14);
  np.clip's no-bound route is the same result. np.clip's fresh result takes the trivial loop too (it only voted
  through NpyIter: an F (2, 1, 3) operand gave (8, 48, 16) where NumPy has (8, 16, 16)).
- np.sinc and np.i0 (one fused np.evaluate pass each, where NumPy runs ufunc calls / piecewise over zeros_like) keep the
  ufunc layout of x: the pass runs over x viewed in that layout's order and the C result is relabelled (a permuted x
  was C).
- np.round / np.around: decimals != 0 copies with order 'A' (PyArray_Round's C or ISFORTRAN), a complex input runs the
  rint ufunc into a K-order result (MX04). np.nan_to_num copies in K order. np.diff allocates the subtract's layout.
  np.tril / np.triu decline their C blit where NumPy's np.where would give another layout.
- The scans: np.cumsum / np.cumprod (and the nan forms) allocate in the accumulate's order over the ORIGINAL operand
  (AccumulateResultLayout); the length-1-axis shortcut casts into that layout (QA-09: np.cumprod kept int32); a bool
  operand's int64 copy is allocated in that layout too (BoolOperandAsAccumulateInt64 - a K-order astype sorted a
  broadcast axis innermost and the scan came out F, the matrix tier's matrix.cumprod/bcast/bool cases); the nan forms
  replace their NaNs as NumPy's _replace_nan does (a K-order copy, then np.copyto(where=isnan); NanReplace.ReplaceDenseFloat).
- Keepdims: ExpandReducedAxis gives the kept axis NumPy's stride (the running stride at its sorted place) for sum /
  mean / var / std / the nan family / min / max, and now np.any / np.all and every empty-input exit (np.sum / np.max /
  np.mean / np.prod / np.count_nonzero: an empty result has all-zero strides, a reduced axis of length 0 the running
  stride); RelabelKeptAxes does it for np.any / np.all over a tuple axis. np.median's mean is laid out over the K-order
  partition copy, and quantile / median keepdims is NumPy's newaxis view (stride 0, OWNDATA false).
- Shape.ForFreshAllocation zeroes every stride of a zero-size allocation.
- np.memoryview.strides recomputes the strides of a contiguous array from its shape, as NumPy's buffer export does
  (_buffer_info_new): np.empty((0, 3)).data.strides is (24, 8) and np.zeros((2, 0, 3)).data.strides (0, 24, 8) while
  the arrays' own strides are all zero. It matched only by accident while empty arrays had C strides; the surface
  tier's ndarray.data/<dtype>:empty:strides cases caught it.

Tests:
- The layout-parity corpus gains a `ufunc` family (test/oracle/gen_layout_parity_oracle.py; 263 NumPy-generated cases
  over 32 ufuncs x 8 sources, 6 more over an int32 F (2, 1, 3) source, np.around of a broadcast): 568 cases, every
  family green.
- Backends/UfuncResultLayoutTests.cs (11 tests, NumPy 2.4.2 literals): the QA-36 reproduction, C winning conflicts,
  extent-1 strides of the two allocators, flipped operands, two-output ufuncs, empty results, keepdims (sum, median, the
  logical reductions, tuple axes, empty inputs), np.positive, the bool scans of a broadcast, np.clip / np.sinc / np.i0.
- np.memoryview.Test.cs (the recomputed strides), np.array.Tests.cs (QA-35), NDArray.cumprod.Test.cs (QA-09),
  Shape.ForFreshAllocation.Tests.cs (the zero-size rule; its pin moved out of OpenBugs.EmptyStrides.cs, whose slice
  half stays open).
- Pins untagged as regression tests: OpenBugs.Matrix.cs MX02 / MX04 / MX05; OpenBugs.Relayout.cs RL02 x2 / RL03 (no
  relayout pin is open); OpenBugs.ViewFlags.cs VA03 x2 / VA05 / VF14. The QA-09 / QA-35 / QA-36 pins left
  OpenBugs.ParityQa.cs (20 remain).

Oracle (NumPy 2.4.2's answers already in the corpus; nothing regenerated):
- The KnownGaps replays named the carved cases that now match NumPy: matrix MX02 (36), MX04 (5), MX05 (2) and MX09 (4,
  found matching too); relayout RL02 (17) and RL03 (3); view_flags VF06 (22), VF07 (7), VF09 (13), VF10 (3), VF12 (2),
  VF14 (4), VF32 (1), VF34 (3), VF38 (6), VA03 (1) and VF08's np.median half (14); array_args VA03 (3), VA05 (1).
- Their generator rules are dropped (VF08 narrowed to np.nanmedian, whose numpy.ma chain and MaskedArray.filled view
  stay open), and their 147 cases moved back into matrix.jsonl / relayout.jsonl / view_flags.jsonl / array_args.jsonl
  at the positions the generator gave them (the ids' trailing counters), each line the gap file's line with its
  trailing "gap" key removed. Checked by applying the edited _gap_of rules to every committed case: the gate / gap
  split matches exactly in every tier (view_flags 310 carved, shape_args 24, array_args 16, matrix 64, relayout 0).
- The ledger (test/NumSharp.Tests.Oracle/Fuzz/README.md) records the redraw.

Verification:
- Unit suite, CI filter (TestCategory!=OpenBugs&TestCategory!=HighMemory), Debug: net10.0 22,629 passed and net8.0
  22,628 passed; the 3 failures on both (SumProd_NaNPayloads_AreNumPys, Trapezoid_NaNSpacing_IsTheProductsFirstOperand,
  ShortRows_SeveralNaNs_KeepTheRunningSumsNaN) fail identically on the base commit 1901e0c. A wall-clock test
  (T2_NonZero_Is_Much_Slower_Than_NumPy) missed its limit once under the parallel suite on a loaded machine and passes
  alone.
- Oracle FuzzMatrix: Debug net10.0 and net8.0 223 passed, 0 failed, 15 skipped (the backend-pinned tiers, and the two
  lifetime tests, which need a Release build); Release net10.0 225 passed, 0 failed, 13 skipped (Lifetime and
  LifetimeScaled_EveryOpKey pass). Mid-port the gate caught two regressions this commit fixes: the bool scans of a
  broadcast (Matrix x3) and the memoryview strides of an empty array (Surface).
- Guard pages (NUMSHARP_DEBUG_GUARD_PAGES=1, Release net10.0): the unit suite runs to the end without a crash in this
  commit's code - 22,623 passed; the failures are the 4 pool-counting tests the mode breaks by design and
  Comparisons_CastOut_KeepsThePerRunRoute (it reads the process-wide NDStringComparison.MaskedBlendRuns counter without
  [DoNotParallelize]; passes alone). Two tests crashed the host only inside the full parallel run and were excluded to
  finish it: StringSortTests.TailPort_EqualsTheRecordsPort (TextSort.CompareTail) and
  NpUniqueArgsortRouteTest.LongIndexedFallback_GivesTheManagedOutputs (EmitOutputsLong) - code this commit does not
  touch; both pass alone under guard pages on this tree, the first also on the base. The oracle under guard pages: 218
  passed; the failures are the 5 pool-counting gates and Lifetime / LifetimeScaled_EveryOpKey, whose control is a
  deliberate use-after-free that faults once freed buffers are unmapped.
- Probes against NumPy 2.4.2 (byte strides and bits printed by twin scripts): 790 + 172 + 316 layout rows earlier in
  the port, then 25 (np.max / np.min of p * 1 over a permuted input - the reductions rule's open item, now matching to
  the zero signs; np.positive; the bool scans; memoryview strides) and 64 (keepdims of any / all / count_nonzero / sum
  / prod / argmax / ptp over F and permuted inputs, tuple axes, empty inputs): every row identical; then 36 (clip,
  maximum, fmax, copysign, isnan, signbit, sinc, isclose, angle, square, rint, power, float_power, heaviside, fmod,
  the logical / bitwise ufuncs over an F (2, 1, 3) and a permuted operand) and 48 (sinc, clip, positive, unwrap,
  polynomial.polyval / chebyshev.chebval, the legacy np.polyval, the *_like family, np.copy, astype, i0,
  real_if_close, nan_to_num over F (2, 1, 3), F and permuted operands): identical after the clip / sinc / i0 fixes
  (before them, clip of the F (2, 1, 3) operand and sinc / i0 of the permuted one differed).
- Release, two pinned P-cores, best of 7 rounds after two warm-ups, base 1901e0c and this tree alternated (us per
  call, float64, an n x n x 4 operand): permuted operands are 2-4x faster, because they now compute over contiguous
  views and relabel where the base walked them strided into a C result - add 1.48 -> 0.78 (1K), 184 -> 124 (256K),
  14,361 -> 3,541 (4M); negative 1.16 -> 0.59, 215 -> 126, 14,714 -> 3,890; p * 2.0 2.49 -> 1.77, 233 -> 134,
  14,587 -> 3,783. C operands are unchanged (add 0.59 / 0.59, 129.6 / 128.7). Two small-call costs remain for F
  operands at 1K: add 0.57 -> 0.68 (the layout decision and a Shape[] per call), negative 0.51 -> 0.53; at 256K and
  4M they are within noise. A permuted operand beside a C one (p < c) costs what it did (2.36 / 2.28).

Docs: the area rules (kernels, ufuncs, comparison, reductions, memory-views, creation-manipulation, matrix, oracle,
tests, float-math, fft-windows), the oracle ledger, docs/bugs/parity-qa-2026-11-02.md (QA-09 / QA-35 / QA-36 fixed, the P19 decision), and the
website's compliance and under-the-hood pages (fresh results follow NumPy's layout; K is not "C or F").

Still open (unchanged by this commit): np.nanmedian along an axis of an F or permuted input (VF08 / VF26: numpy.ma's
median chain and its _data view); a slice's strides on an extent-0 axis (OpenBugs.EmptyStrides.cs); np.reshape of an
empty array (NumPy's _array_fill_strides skips zero extents); np.evaluate keeps numexpr's own K heuristic for a user's
fused program (np.sinc and np.i0 relabel theirs); np.asarray(a.data) returns a.data.obj, so for an empty array it reports the array's zero strides where NumPy
rebuilds from the memoryview's.
…ery release pauses before nuget.org until Nucs approves the deployment ("Review deployments" on the run), only v* tags may deploy, admins cannot bypass the review, and the nuget.org Trusted Publishing policy is tied to the environment, so no other job can obtain a publish key

WHY
The owner, 2026-11-08, after OptunaSharp got the same gate (SciSharp/OptunaSharp b45615df): "I need NumSharp to also be
driven by this kind of approval design starting next release." 0.80.0 published straight from the tag run; from the
next release on, a human approves the one irreversible step.

THE GATE (set up outside the repo, read back through the API and the nuget.org page)
- GitHub: environment `nuget` on SciSharp/NumSharp (PUT /repos/SciSharp/NumSharp/environments/nuget), identical to
  OptunaSharp's: required reviewer Nucs (user id 649919) and no one else, prevent_self_review false (the owner approves
  the owner's own tag), can_admins_bypass false (the other admins and maintainers cannot skip it), deployment policy =
  custom with one tag rule `v*` (POST .../deployment-branch-policies {name: "v*", type: "tag"}), wait timer 0.
- nuget.org: the NumSharp Trusted Publishing policy (owner elibelash; package owner SciSharp, SciSharp/NumSharp,
  build-and-release.yml, "Push new packages and package versions", `*NumSharp*`) gains environment `nuget`, so the token
  exchange succeeds only for a job running in that environment: a workflow edited on any branch, or this job without
  the environment, gets HTTP 401.

WHAT (this commit)
- .github/workflows/build-and-release.yml, publish-nuget: `environment: nuget`, with a comment saying what holds the job,
  who approves, which refs may deploy, why the policy names the environment, that only this job waits (tests,
  build-nuget and create-release run unattended) and how a pending or failed approval behaves (30-day expiry; a re-run
  asks again and reuses build-nuget's artifact). The Trusted Publishing comment names the environment claim and
  `environment nuget` instead of "no environment", and the NuGet/login comment names NUGET_USER's value (elibelash, the
  policy OWNER) and the 401 text a wrong name produces (0.80.0's first attempt, fixed by setting the secret and
  re-running the failed job).
- .claude/rules/ci-release.md: the release flow step by step (annotated v* tag on master -> unattended tests,
  validate-release, build-nuget, create-release -> the pause at publish-nuget -> Review deployments -> nuget -> Approve),
  the environment's rules, and the Trusted Publishing paragraph with the environment, the policy owner and the 401 fix.

BRANCH
`strings` was fast-forwarded to master d523eb4 (the 0.80.0 merge; strings f10aae1 was its ancestor with nothing master
lacks, and the strings worktree was clean), so the release-only commits (CI host pins, the LT31 anchor,
RequireStringFits, the [SubclassAware] frames, the version, the notes, the Collections change, the analyzer tracking)
are on strings again and this edit of the workflow cannot conflict with them. The change reaches master with the next
release's merge; until then master's workflow has no environment, and with the policy now requiring one, a release cut
from master as it is would fail at NuGet/login without pushing anything (fail-closed).

VERIFIED: the workflow parses (PyYAML: publish-nuget environment = nuget, needs and permissions unchanged); the
environment reads back can_admins_bypass false, custom_branch_policies true, required_reviewers [User Nucs] with
prevent_self_review false, and one deployment rule `tag v*` - the same as OptunaSharp's.
…) re-ported onto the 0.80.0 release - NumPy's try_trivial_single_output_loop allocation beside NpyIter's K order, keepdims axes at NumPy's sorted stride, np.median's mean layout and newaxis keepdims, all-zero strides for zero-size allocations, np.positive's writeable identity loop and np.ldexp's offset views; the carves RL03, VA05, VF14 and VF08's np.median half return to the gate

WHY A RE-PORT AND NOT A TEXTUAL MERGE
qa/ufunc-keeporder (ecd7023, one commit on 1901e0c, 2026-11-05) and parity batch 10 (157293e, shipped in 0.80.0,
d523eb4) were written in parallel from the same base for the same owner decision (P19 / parity QA-36, "Port KEEPORDER
now", "With parity to Numpy"), and both rewrote the same routes - the binary, comparison, unary, shift, float-tier,
ldexp, clip and positive engines, the scans, np.where, np.round, np.array - on two different backbones:
  - master (batch 10): DefaultEngine.KOrder.cs - TryKOrderPlan (Shape.NpyIterOutputShape, linear above 64 dimensions),
    IntoKOrder views, NDArray.AdoptLayout relabels, KOrderBinary / KOrderBinaryPair / KOrderUnaryPair wrappers, NpyIter's
    K order ONLY;
  - the branch: DefaultEngine.UfuncResultLayout.cs + Shape.UfuncOutputShape - the trivial loop's C / F allocation OR
    NpyIter's K order, decided once per call.
A textual merge produced 40 conflicted hunks and, where it applied cleanly, DOUBLE routing (both layout steps active on
the binary and comparison routes). Master's backbone is the shipped, measured one (batch 10's A/B: permuted / F /
flipped operands as fast or faster, the scan row route), so it stays, and only what the branch has that master lacks is
ported onto it, with the branch's own tests and layout-parity corpus as the spec. The merge that records the branch as
an ancestor follows this commit (strategy "ours": its tree is this commit's).

WHAT THE BRANCH HAD THAT MASTER LACKED (ported here)
1. NumPy's trivial loop (ufunc_object.c try_trivial_single_output_loop + check_for_trivial_loop). A call with no where=
   mask and one output, whose N-D inputs are all aligned, read in their own dtype and C- or F-contiguous with the same
   (C, F) flag pair, allocates PLAIN C or F (F when the pair is F-only; 0-d and 1-D inputs never fix the order); every
   other call is NpyIter's K order. The two differ only in the strides of extent-1 axes, which master always gave
   NpyIter's: np.negative of an F (2, 1, 3) int32 array is (4, 8, 8) through the trivial loop, np.sqrt of it (a cast
   input) (8, 48, 16) through NpyIter (probed 2.4.2, matched).
   - DefaultEngine.KOrder.cs: TrivialLoopLaysOut(a, b, aCasts, bCasts, out fortran) and its one-input form (a cast or
     unaligned N-D input refuses it, UfuncLoopOperand.IsAligned; then Shape.TrivialLoopIsFortran); TryUfuncLayoutPlan
     joins it with TryKOrderPlan (the trivial C needs no plan, the trivial F is plain F over its dense axis order);
     KOrderBinary takes the callers' cast flags. IsAllStrictF is gone (the trivial-loop test replaces it).
   - Shape.StridePerm.cs: UfuncOutputShape(dims, inputs, trivialLoopEligible), TrivialLoopIsFortran, PlainInputOrAdopt,
     HasPlainStrides, SameDimensions.
   - Callers say which inputs the loop casts: BinaryOp (lhsType / rhsType != resultType), CompareOp
     (ComparisonTrivialLoopLaysOut over ComparisonKernelKey.GetComparisonType; an int64 / uint64 pair is an exact loop,
     NumPy's qQ / Qq), UnaryOp (UnaryLoopCastsInput: T->T loops cast a different input; the float predicates,
     logical_not, abs of complex128 and bitwise_count keep a loop per input dtype; signbit casts every non-float input),
     ATan2 (the arctan2 loop), LogAddExp (the tier loop), Ldexp (x1 not the float loop, x2 not int32 / int64), Shift.
   - np.positive (Default.Positive.cs IdentityLoopResult): the plain and dtype-only identity loops allocate the ufunc
     layout and copy into it (UnmanagedStorage.Copyto, unsafe casting) instead of cloning the input, so the result of a
     read-only or same-shape broadcast operand is writeable (array-object gap VF14), and a permuted / flipped input keeps
     NumPy's layout.
   - np.clip (Default.ClipNDArray{,.Iter}.cs ClipResultLayout): Shape.UfuncOutputShape with the trivial-loop operands;
     the no-bound route is positive's IdentityLoopResult.
   - np.power's fast path (Default.Power.StrideZero.cs PowerPlainResultShape): the trivial loop's C / F, else the K plan.
2. keepdims reductions (Default.Reduction.Add.cs ExpandReducedAxis / RelabelKeptAxes): the reduced axis returns at its
   sorted place with the running stride (the keepdims form of Shape.ReductionOutputShape, npyiter_new_temp_array), not
   Storage.ExpandDimension's C-style insert: np.sum(F (2, 3, 4), axis=1, keepdims=True) has NumPy's (8, 16, 16) bytes
   (was (8, 64, 16): QA-36's "Not counted" item). Wired into sum / prod / mean / var / std, amax / amin, the half
   min / max route, the keyword max / min, the NaN-aware forms (sequential and engine), any / all / count_nonzero (the
   tuple-axis chain relabelled once, RelabelKeptAxes) and the empty-input keepdims handlers (Mean, Product, NonZero),
   which used to reshape.
3. np.median along one axis (Default.Quantile.cs MedianMeanLayout): NumPy's _median is np.mean of the middle slice of
   partition's order='K' copy, so the result is the mean's allocation in that copy's axis order - an F input's median is
   F, a broadcast input's follows its K copy (np.median(broadcast_to(row, (2, 3, 4)), axis=1) is F-contiguous); and
   _ureduce's keepdims (ApplyKeepdims, every quantile form) is r[(Ellipsis,) + index_r] with np.newaxis: a VIEW, stride 0
   on the new axes, OWNDATA false (np.median(F (2,3,4), axis=1, keepdims=True): (8, 0, 16); axis=None: (0, 0, 0)).
   VF08's np.median half matches; np.nanmedian's numpy.ma chain stays carved.
4. np.tril / np.triu (NDArray.TwoDimBase.cs): a non-contiguous input whose NpyIter layout of (mask, m) is not C takes
   np.where's composition instead of the C row blit (a transposed m keeps the transpose's strides, as NumPy's
   where(tri(...), m, zeros(1)) allocates).
5. Zero-size allocations (Shape.Allocation.cs ZeroStridedEmpty): every fresh zero-size array gets all-zero strides
   whatever layout was asked for (PyArray_NewFromDescr_int: "Make sure all the strides are 0") - np.zeros((2, 0, 3)), an
   empty ufunc result, an empty keepdims reduction. The buffer export recomputes strides for a contiguous array as
   NumPy's _buffer_info_new does (np.memoryview.cs strides), so np.empty((0, 3)).data.strides is still (24, 8). The slice
   half of OpenBugs.EmptyStrides.cs (a view keeps its parent's stride on an extent-0 axis) stays open.
6. The scans' length-1 axis (Default.Reduction.CumAdd.cs LengthOneAxisScan, also CumMul): the input is copied into the
   accumulate layout at the accumulator dtype, not astype's K copy (np.cumprod(asfortranarray(ones((1, 3, 2))), axis=0)
   has NumPy's (48, 8, 24)).
7. np.ldexp's operand materialization (Default.DivMod.cs MaterializeContiguous): a C-contiguous view of the right dtype
   at a non-zero offset (an np.split child) is copied; np.ascontiguousarray passed it through and the kernel read
   .Address without Shape.offset, scaling the owner's first elements (relayout gap RL03, array-object gap VA05). The
   temporary broadcast views are disposed.

WHAT THE BRANCH HAD THAT MASTER ALREADY HAD (not ported, master's code kept)
np.where's offset reads (VA03 / RL02), np.nancumsum / np.nancumprod's _replace_nan statements (RL02), np.array's K copy
(QA-35, VF12), the K order of the binary / unary / comparison / float-tier / two-output ufuncs, the scans, np.diff,
np.round, np.nan_to_num (VF06, VF07, VF09, VF10, VF32, VF34, VF38, MX02, MX04, MX05), np.fft's layout (VF36), np.nanmean
keepdims (VF37), QA-09's dtype, and np.sinc / np.i0 of a permuted operand (np.evaluate already allocates NumPy's layout;
the branch's sinc / i0 assertions pass on master's code unchanged).

TESTS (the branch's spec, applied)
- Backends/UfuncResultLayoutTests.cs (new: the trivial loop against NpyIter per route, cast and unaligned inputs, the
  permuted-operand pin), Backends/LayoutParityOracleTests.cs + Backends/corpus/layout_parity_oracle.jsonl (the 263-case
  `ufunc` family, generated by test/oracle/gen_layout_parity_oracle.py), View/Shape.ForFreshAllocation.Tests.cs
  (EmptyAllocation_HasAllZeroStrides; Shape.Adopt -> new Shape(dims, copy: false)), APIs/np.memoryview.Test.cs,
  Creation/np.array.Tests.cs (ArrayOfNDArray_IsAWriteableKOrderCopy), Math/NDArray.cumprod.Test.cs
  (Cumprod_LengthOneAxis_PromotesNarrowIntegers), OpenBugs.EmptyStrides.cs (the allocation half moved out).
- The spec classes went 11 -> 0 failures over the port (140 / 140).
- Pins untagged (now regression tests): OpenBugs.Relayout.cs Relayout_Ldexp_MantissaAtAnOffset_ShouldReadTheView (RL03),
  OpenBugs.ViewFlags.cs ViewFlags_Ldexp_ExponentAtAnOffset_ShouldReadTheView (VA05),
  ViewFlags_CopiesOfAReadOnlyArray_ShouldBeWriteable (VF14).

ORACLE CARVES RETIRED (no regeneration; the cases moved back at the positions their ids' trailing counters give them,
the "gap" key removed, every other line byte-identical; the serializer round-trip was checked on every tier line first)
- gen_relayout_oracle.py: RL03's rule dropped, GAPS = {} (no relayout gap is open); its 3 cases back in relayout.jsonl
  (15,134 -> 15,137), gaps/relayout_gaps.jsonl empty.
- gen_viewflags_oracle.py: VF14's and VA05's rules dropped, VF08's narrowed to np.nanmedian; 16 cases back in
  view_flags.jsonl (7,679 -> 7,695; gaps 225 -> 209) and 1 in array_args.jsonl (355 -> 356; gaps 17 -> 16); 249 carved
  cells under 15 ids remain.
- The KnownGaps replays before the move: Relayout_KnownGaps "0/3 still diverge"; VA05 0/1, VF14 0/2, VF08 14/28 (the 14
  np.median cases match); every other id unchanged.

RESULTS
- Unit suite (Release, CI filter TestCategory!=OpenBugs&TestCategory!=HighMemory): net10.0 23,114 passed, 0 failed,
  280 skipped; net8.0 23,113 passed, 0 failed, 280 skipped.
- Guard pages (NUMSHARP_DEBUG_GUARD_PAGES=1, net10.0, the same filter): no crash; the 4 failures are the pool-counting
  tests tests.md lists as failing in that mode (NoOperationDefersItsBufferRelease,
  EightThreads_MixedOps_ZeroErrors_ZeroStrandSlope, ScopedSurface_ZeroStrandsPerCall, TupleCarrier_Polydiv_ZeroStrands).
- Oracle (net10.0): FuzzMatrix 225 passed, 0 failed, 13 skipped (the host-pinned / backend tiers);
  RelayoutLayoutInvariance 16,805 variants compared, 60 skipped, 0 layout-caused; every *_KnownGaps replay reports each
  remaining carved case still diverging (VF08 14/14, VA02 2/2, VA04 14/14, VS01 24/24, MX01 56/56, ...; no "now match"
  anywhere), and the relayout / relayout_host / out_flags gap files are empty.
- OpenBugs audit (net10.0 Release): 104 tagged tests, 12 pass - the same 12 that pass on master d523eb4 (checked in a
  detached build: 10 stale tags, among them five FancyGet / one FancySet pin, plus the two kept on purpose), so the port
  neither fixes nor breaks another pin.
- A dotnet-run probe against NumPy 2.4.2's answers: np.negative / np.sqrt of an F (2, 1, 3) int32 array (4, 8, 8) /
  (8, 48, 16); np.sum(F (2, 3, 4), axis=1, keepdims=True) (8, 16, 16); np.median keepdims (8, 0, 16), OWNDATA False, and
  (0, 0, 0) for axis=None; np.median of a broadcast (2, 3, 4) along axis 1 F-contiguous; np.zeros((2, 0, 3)) (0, 0, 0);
  np.positive of a read-only array writeable; np.ldexp(ones(4), split child) [0.5, 1, 2, 0.5] - all equal.

DOCS
- .claude/rules/ufuncs.md "The result layout: NumPy's trivial loop, else K order" (the trivial-loop test, the cast
  flags, TryUfuncLayoutPlan, positive / clip / power / scans, zero-size strides, the reductions that follow, the one
  item not ported: np.nanmedian), np.clip's result-layout bullet; reductions.md (keepdims at the sorted stride, the
  logical reductions, the scans' length-1 axis); quantile.md (np.median's layout, _ureduce's newaxis keepdims);
  memory-views.md (zero-size allocations, the buffer export's strides); comparison.md (the comparison dtype's cast
  test); oracle.md (no relayout gap open, the offset-view class fixed but a[:, idx]); tests.md (EmptyStrides' open half,
  OpenBugs.Relayout.cs regression pins only).
- test/NumSharp.Tests.Oracle/Fuzz/README.md: the ledger rows, the tier tables and a "Redrawn 2026-11-08" paragraph in
  both the array-object and the relayout sections.
- docs/bugs/parity-qa-2026-11-02.md: a P19 follow-up row and a "Follow-up fixed 2026-11-08" note under QA-36.
- docs/website-src: compliance.md (fresh results laid out as NumPy's: trivial loop or K order, keepdims, zero-size
  strides) and advanced/under-the-hood.md (a fresh ufunc result keeps the operands' memory order).

BREAKING (NumPy-aligning): .strides of fresh results change where NumPy's differ - extent-1 axes of trivial-loop
results and of keepdims reductions, every zero-size allocation (np.zeros((2, 0)).strides (0, 8) -> (0, 0)), np.median
keepdims (a view with stride-0 axes, OWNDATA false). Flags and values are unchanged except np.positive of a read-only
operand (now writeable) and np.ldexp of an offset view (now NumPy's values).
…gy "ours") - its fix was re-ported onto the 0.80.0 release by the previous commit, because parity batch 10 had rewritten the same routes on another backbone

The branch (ecd7023, "fix(layout)!: every fresh ufunc result takes NumPy 2.4.2's layout - try_trivial_single_output_loop's
C / F allocation, NpyIter's K order otherwise ...") was written on 1901e0c in parallel with parity batch 10 (157293e),
which shipped in 0.80.0 and already gave fresh results NumPy's K order through DefaultEngine.KOrder.cs. Merged
textually, the two produced 40 conflicted hunks and double layout routing on the binary and comparison routes, so the
branch's unique fixes - the trivial loop's plain C / F allocation, keepdims axes at NumPy's sorted stride, np.median's
mean layout and newaxis keepdims, np.tril / np.triu's composition, all-zero strides for zero-size allocations, the
memoryview's recomputed strides, np.positive's writeable identity loop, np.clip / power / the length-1 scans on the same
rule, np.ldexp's offset views - were ported onto master's backbone in the previous commit, with the branch's tests and
layout-parity corpus as the spec (140 / 140) and its oracle carves RL03, VA05, VF14 and VF08's np.median half retired.

This merge keeps the previous commit's tree unchanged and records the branch as an ancestor, so `git branch --merged`
reports it and it can be deleted. What the branch did that master already had (np.where's offset reads, nancumsum's
_replace_nan, np.array's K copy, the K order of every ufunc family, the matrix carves) is master's code.
…491 as the published 0.80.0 package does - #396's own 227x227 image gets a pin (Bug 1c) that the stride analysis wrongly called working, Bug 5 fails inside ToBitmap again (its np.arange operand had become int64), and each pin tied to an issue links it

Why
---
The 2026-11-08 issue triage after the 0.80.0 release re-ran every open SciSharp/NumSharp issue's repro against the
packages published on nuget.org (a console app restoring NumSharp, NumSharp.Bitmap and NumSharp.Interop.OnnxRuntime
0.80.0 from nuget.org only, diffed against NumPy 2.4.2). It closed 34 issues and left progress comments on 4. The four
NumSharp.Bitmap issues all still reproduce on 0.80.0:
  #396 Bitmap.ToNDArray(copy: true), 227x227 24bpp -> IncorrectShapeException "cannot reshape array of size 155268
       into shape (1,227,227,3)"
  #440 np.ones((1, 2, 1, 3)) uint8 .ToBitmap() -> ArgumentOutOfRangeException (Parameter 'block'); when reported it
       returned a wrong pixel
  #475 ToBitmap() of a sliced (1, H, W, 3) view -> IncorrectShapeException "input operand has more dimensions than
       allowed by the axis remapping"
  #491 ToBitmap() of an int32 array -> InvalidCastException "Unable to perform CopyTo when T does not match dtype"
The owner's decision for these four: "Create OpenBugs and skip for now" - pin them, no fix in this change, no GitHub
note. They were already pinned in OpenBugs.Bitmap.cs, but reading the pins against the 0.80.0 behaviour found two that
did not hold the bug they name.

What changed (test/NumSharp.Tests/OpenBugs.Bitmap.cs only)
---------------------------------------------------------
1. Bug1c_ToNDArray_CopyTrue_227pxWide24bpp_Issue396Repro (new): #396's own image. GDI+ pads the 681-byte rows to a
   684-byte stride; 684 / 227 truncates to the right 3 bytes per pixel, but the copy path keeps stride * height =
   155,268 bytes and the reshape to (1, 227, 227, 3) needs 154,587, so it throws. Bugs 1a / 1b alone would let a fix
   that only corrects the bpp guess (stride / width -> the pixel format's bpp) pass while the reporter's image still
   throws: the copy must also drop each row's padding. The pin also reads the last pixel back in GDI+'s BGR order
   (30, 20, 10), so a fix that keeps one padding byte per row cannot pass it by shape alone.
2. The BUG 1 root-cause block said 227px "happens to work" (and, above it, that 227px truncates stride / width to 2).
   Both were wrong; it now states the size mismatch and points at Bug 1c.
3. Bug5_ToBitmap_NonByteDtype_InvalidCastException: its operand was np.arange(0, 36).reshape(1, 3, 4, 3) with the
   comment "// int32", and the precondition nd.dtype.Should().Be(typeof(int)). np.arange returns int64 now (NumPy 2's
   default integer), so the pin failed at that precondition before ToBitmap ran: red for the wrong reason, and it would
   have stayed red after a fix. The operand is .astype(np.int32) now and the pin fails inside ToBitmap with #491's
   InvalidCastException.
4. OpenBugs(IssueUrl = ...) on the eight pins tied to a GitHub issue, the attribute's documented form (68 pins use it):
   #396 Bugs 1a / 1b / 1c, #440 Bugs 2 / 6a / 6b, #475 Bug 4, #491 Bug 5. Bugs 3a / 3b / 7 / 8 have no issue.
5. The class summary matches the code: the bug-category list had Bug 7 / Bug 8 swapped (7 is the 24bpp round trip,
   8 the unsupported channel count), the total said 10 test methods (there were 11; 12 with Bug 1c), and the issue line
   now maps each issue to its pins. A remarks paragraph records the 0.80.0 re-verification and why Bugs 1c and 5
   changed.

Not changed: src/NumSharp.Bitmap (the fixes are deferred), and the four GitHub issues stay open with no comment.

Verification (a separate worktree at ce1824f, journey5's tip, plus this change)
--------------------------------------------------------------------------------
- Release net10.0 and net8.0: dotnet test --filter "ClassName~OpenBugsBitmap" -> Failed 12, Passed 0 on both, and each
  pin fails inside the call it is about:
    1a / 2 shape[3] 4 instead of 3; 1b / 1c IncorrectShapeException from ToNDArray's reshape (size 32 / 155268);
    3a / 3b IndexOutOfRangeException from AsNDArray(flat: true); 4 IncorrectShapeException from ToBitmap's copy of a
    sliced view; 5 InvalidCastException from ToBitmap; 6a / 6b ArgumentOutOfRangeException (Parameter 'block') from
    ToBitmap; 7 the round-tripped pixel (1,0) R = 0x00 instead of 0x28; 8 GDI+'s "Parameter is not valid." without
    the channel count.
- CI's filter (ClassName~OpenBugsBitmap&TestCategory!=OpenBugs&TestCategory!=HighMemory) matches none of the 12, so
  OpenBugs(IssueUrl = ...) keeps the category.

Branch: journey5, which starts at origin/strings ce1824f (v0.80.0 + the 4 post-release commits); this is its first
commit, applied path-limited (this one file) in journey5's worktree.
…t test - 35 Issue<n> classes in NumSharp.Tests/Issues assert each answer's claims against NumPy 2.4.2's output, each "still missing" list is an [OpenBugs] pin, and the ONNX Runtime / pythonnet halves are backed in their own projects (91 new tests, 6 of them pins)

Why
---
On 2026-11-08 the post-0.80.0 triage closed 34 SciSharp/NumSharp issues and left progress comments on 4, every claim
checked by hand against the published 0.80.0 packages and NumPy 2.4.2. The owner: "Every single answer needs to become
a unit test". So each posted answer is now held by tests that name its issue: a regression that would make an answer
untrue fails a test called Issue<n>, and every gap an answer admits flips its pin green when it closes.

Layout
------
- test/NumSharp.Tests/Issues/<n>.cs: one class Issue<n> per answered issue (the folder's existing convention, beside
  443 / 447 / 448), documented with the issue URL and what the answer claimed. Expected values are NumPy 2.4.2's, from a
  scratch generator, spelled as literals, NumPy reprs or float bit patterns.
- test/NumSharp.Tests/Issues/IssueAnswerSupport.cs: the repository-root lookup for the documentation answers
  (Inconclusive outside a checkout), bit-pattern helpers, and the platform assertion several "not planned" answers share
  (net8.0 / net10.0 assembly, kernels are DynamicMethods emitted at run time).
- test/NumSharp.Tests.Interop.OnnxRuntime/ClosedIssue512Tests.cs, and two methods in NumSharp.Tests.Interop's
  ClosedInteropIssuesTests (Issue498_*, Issue349_*): the answers' interop halves, live where they need ONNX Runtime /
  CPython.
- Answers already backed by existing tests get a doc link instead of a duplicate: KarpathyMicroGptTests and
  KarpathyMicroGptLiveTests (#540, every claim of the microGPT answer) and Ndw021BuildTests (#455 / #511's NDW021 claim).
- .claude/rules/tests.md's OpenBugs inventory names the new pins and where they live.

Per answer (C = closed as completed, N = closed as not planned, P = progress comment, issue still open)
--------------------------------------------------------------------------------------------------
C #490 choice(71, 40, replace: false): no duplicates over 200 seeds, seed 42 = NumPy's 40 values (int64, the LP64
       legacy integer), re-seeding reproduces.
C #529 uint8 / uint16 / uint32 / uint64 + 1, (short)1, 1L, * 3 keep the dtype; uint8 + int32 array is int32; an explicit
       0-d int16 is strong (int16); 300 and -1 raise "Python integer ... out of bounds for uint8".
C #341 new NDArray(typeof(string), Shape(3)) is array(['', '', ''], dtype='<U1'); np.array(string[]) and
       np.strings.upper reprs.
C #418 var (xx, yy) = np.meshgrid(...) gives both grids; the reporter's (scales, ratios) = np.meshgrid(...) pattern too.
C #555 NEP 34 ragged ValueError (NumPy's text); no financial functions; isnan / isfinite / isclose / & / | / any(axis)
       / roll over 8 dtypes / the mask setter compute; NDArray.inv / qr / svd / lstsq stubs gone; np.linalg.norm public.
C #575 repeat and nonzero allocate no managed object per element (GC.GetAllocatedBytesForCurrentThread, per-thread so
       parallel-safe); sum(axis=0) of a float32 cube.
C #595 maximum / minimum / fmax / fmin with out= aliasing the second operand, and clip with out= aliasing a bound or the
       array: NumPy's values and out is returned (the old copyto-then-clip route overwrote the aliased input first).
C #615 [DoNotParallelize] 500 shifts of fresh temporary bool[100,000] arrays (the reported call, on the astype + SIMD
       route under BoolSimdScalarShiftMaxElements) while a thread forces collections and poisons pooled buffers of the
       widened size: every result is NumPy's int64 4s. The library-wide claims point at the lifetime oracle gates.
C #592 S / U / T descriptors; np.strings has NumPy 2.4.2's 46 names; [email protected] strips, chararray == strips;
       sort / stable argsort / unique / searchsorted / where / take / nonzero / any / all / concatenate / str_len reprs;
       np.save / np.load of U and StringDType (pickle) and savetxt / loadtxt of U round-trip.
C #548 nditer external_loop over a C (2, 3, 4) array is one 24-element loop; a.T + 1 is F-contiguous (K order);
       buffered + op_dtypes casts to float64; can_cast and the same_kind out= TypeError text; VectorBits is the hardware's.
C #549 dtype instances belong to np.dtypes.Float64DType / StrDType / StringDType; common_dtype follows NumPy's slot
       contract (the larger type number answers, the other returns NotImplemented = null); ensure_canonical(>i4) = <i4;
       castingimpls lists a resolved cast; StringDType na_object NaN + np.isnan, coerce=False refusal text, default
       coercion.
C #550 0-d shape / x[...] view sharing memory / x[np.newaxis] / repr / str / item() / take / flat / ravel; the refusal
       texts of len(), take(x, 1), x[0] and item(1).
C #551 vectorize "(i),(i)->()": (3, 5, 4) x (5, 4) gives NumPy's 15 inner products, the core-dim mismatch text; matmul's
       four flexible-dimension shapes.
C #540 existing KarpathyMicroGpt tests, linked (see Layout).
C #462 frombuffer + reshape + np.average(yuv[":, :, 2"]) = 9.5.
C #512 (ONNX project) the answer's one np.multiply pass into tensor.AsNDArray()["0"] gives the reporter's loop's exact
       float bits on a 5 x 7 image; ToDenseTensor / AsDenseTensor / ToNDArray / AsNDArray round trip.
C #343 [WindowsOnly] np_ has Bitmap.ToNDArray / Image.ToNDArray / BitmapData.AsNDArray / NDArray.ToBitmap, is
       [SupportedOSPlatform("windows")] in a net8.0 / net10.0 assembly; AsNDArray writes through to the locked pixels.
C #384 [WindowsOnly] (1, 4, 6, 4) uint8 -> ToBitmap (32bppArgb) -> PNG -> ToNDArray(copy: true) is exact.
C #406 [WindowsOnly] Image.ToNDArray(copy: true) of a 32bpp image is (1, 2, 3, 4) uint8 in BGRA order.
C #70, #95, #129, #479 the linked documents exist in the repository (site guides, dashboard, CONTRIBUTING.md's "Match
       NumPy Exactly", ARCHITECTURE.md and under-the-hood.md naming NDArray / UnmanagedStorage / Shape / TensorEngine);
       #129 also checks the split in code, #479 that NumSharp is 0.80.0 or later.
C #284 the 15 dtypes compute, complex128 multiplies as complex, a 32-d array reduces, np.sum is the engine's ReduceAdd,
       slices are views and copy() copies, the oracle project exists.
N #349 a Hann-windowed np.fft.rfft spectrogram matches NumPy within 1e-12 (sin / cos carry the platform libm's last bit);
       live: scipy.signal.spectrogram over an exported NDArray matches NumSharp's own within 1e-12 ([PythonEcosystem],
       SkipUnless("scipy") first).
N #372 [DoNotParallelize] choice(5, 3, p=[...]) after seed(0): NumPy's [2, 3, 3] with replacement, [2, 3, 1] without.
N #412, #433 of every NumSharp*.dll deployed with the tests only NumSharp defines NumSharp.NDArray (no CS0433 from
       NumSharp's own packages).
N #424, #430, #511 net8.0 / net10.0 assembly, dynamic code supported, binary kernels are DynamicMethods.
N #455 the same, and no type named LAPACKProviderType remains.
N #498 net8.0 / net10.0 only; live: an exported NDArray and numpy share memory both ways.
  #482 (OptaPlanner) has no test: its answer is a scope statement with nothing in NumSharp to execute.
P #547 NEP 50 (weak literal, overflow, order-independent result_type); the present NEP 56 names, .mT, concat,
       ceil / floor / trunc keeping int dtypes; .T on 3-D reverses axes; cross of 2-vectors is the z-component;
       find_common_type kept on purpose. Pins: the 11 missing NumPy 2.x names; round_ / asscalar still present.
P #560 attributes, to_device, constants, the listed functions, std(correction: 1), fft / linalg samples, int8 /
       complex128. Pins: complex64; the 11 spec names.
P #626 [DoNotParallelize] P1 seed(1337) normal(0, 0.05, 6).astype(float32) bits; P3 float32 exp bits (subnormal,
       overflow, -0.0); P4 the MLP's axis sums bits; P2 with the OpenBLAS backend bound a (3, 300) @ (300, 2) float32
       product gives NumPy's bits (which a sequential sum misses in 4 of 6), Inconclusive without the native library.
P #627 inv / det / solve / slogdet without a backend within 1e-12; cholesky / qr / svd / eig / eigh / lstsq / pinv /
       matrix_rank raise OpenBlasMissingBackendException naming NumSharp.Interop.OpenBLAS (Inconclusive if a backend is
       bound).

Found while turning answers into tests
--------------------------------------
- item(1) of a 0-d array raises the right text as System.IndexOutOfRangeException where NumPy raises IndexError
  (NumSharp's IndexError derives from NumSharpException), so a ported `catch (IndexError)` misses it. The #550 answer
  claimed the texts, which hold; the type is a new [OpenBugs] pin (Issue550.ZeroRankArray_ItemOutOfRange_RaisesIndexError).
- nonzero's N-D route stages its flat indices in one managed long[count] (8 bytes per nonzero); it is one allocation,
  not one per element, so #575's answer holds; pooling it is a separate optimization.
- The seeded np.random answers (#490, #372, #626 P1) failed when run in the parallel phase: the GLOBAL np.random is shared
  with every other drawing test, so those classes carry [DoNotParallelize] (TestParallelization's rule).

Verification (a worktree at journey5 90ae655 + this change, Release)
----------------------------------------------------------------------
- NumSharp.Tests, CI filter (TestCategory!=OpenBugs&TestCategory!=HighMemory): net10.0 23,194 passed / 0 failed /
  281 skipped; net8.0 23,193 / 0 / 281.
- The new Issue classes: 81 pass, P2 is Inconclusive without an OpenBLAS library, and the 6 [OpenBugs] pins fail with
  their diagnostics on net10.0 and net8.0 (missing: acos ... astype; round_ / asscalar present; complex64 not supported;
  genfromtxt, memmap, rec; IndexOutOfRangeException instead of IndexError).
- P2 with NUMSHARP_OPENBLAS_LIBRARY = NumPy 2.4.2's own libscipy_openblas64_ DLL: passes (bit-identical GEMM).
- NumSharp.Tests.Interop.OnnxRuntime ClosedIssue512Tests 2/2; NumSharp.Tests.Interop ClosedInteropIssuesTests 8/8
  (PYTHONNET_PYDLL = CPython 3.12 + NumPy 2.4.2 + SciPy 1.16.3; Issue349 ran, not skipped); NumSharp.Tests.Build.Analyzer
  builds (doc-only change).
…dings QB-01..QB-06 pinned as [OpenBugs] with a journal

A second deep manual QA pass over journey5 (base e25b752) against NumPy 2.4.2, aimed at the code that
landed after the 2026-11-02 journey and at new techniques. Nothing is fixed here: every finding is an
[OpenBugs] reproduction in test/NumSharp.Tests/OpenBugs.ParityQb.cs (IssueUrl "parity-qa-QBNN") with a
journal entry in docs/bugs/parity-qa-2026-11-08.md (symptom, NumPy's answer, root cause, why no gate
caught it, fix size). The owner decides which to fix.

Findings so far:
- QB-01 (S1, ULP) complex np.sign divides by NDComplexMath.Abs (np.absolute's FMA-ratio kernel)
  instead of npy_hypot (the UCRT hypot NumSharp already models since QA-25): 22.8% of values differ.
- QB-02 (S3/S1) np.arange's C# float overloads return float32 computed in float32; NumPy never
  returns float32 from arange without dtype= (np.float32 scalars give float64).
- QB-03 (S4) np.repeat with an array of counts: "repeats may not contain negative values" lacks
  NumPy's final period.
- QB-04 (S4) item() / item<T>() of a non-size-1 array throws IncorrectSizeException, and
  MaskedArray.item() InvalidOperationException, where NumPy raises ValueError.
- QB-05 (S1, ULP; up to 2,048 ULP in a cancelling part) complex log / log2 / log10 / arctan /
  arctanh / expm1 call NDComplexMath's stale private MathLog1p / RealExpm1 instead of QA-22's exact
  UCRT models (NDLog1pExpm1Math); rebuilt from NumSharp's own exact real ufuncs, the complex log
  matches NumPy on 30,000 of 30,000 values.
- QB-06 (S1, up to 5 ULP) complex arcsin / arccos / arcsinh / arccosh run System.Numerics.Complex
  Asin / Acos for every finite input, not npymath's msun ports (MSVC's casin/cacos are blocklisted in
  npy_config.h); a Python port of npy_casinh with the UCRT real models matches NumPy on 30,000 of
  30,000.

Directions covered with no new finding (probe counts in the journal's progress log): the Shape
copy-keyword refactor (static aliasing scan + in-place shape changes), 199 numpy.matrix-operand calls,
12,024 binary-kernel / 18,261 reduction / 12,280 unary-where-clip-astype probes over reversed, stepped,
offset and 2-D-block layouts at lengths around the vector widths, a cold-process 16/32-thread
concurrency stress over 1,157 ops (8 processes, zero mismatches), number-to-text casts and np.strings
round 2, C# overload families, 105 ports of NumPy's test_regression.py, the registry's "[known bug]"
excuses (most no longer reproduce), buffered casts around the 8,192-element buffer, and every batch
re-run under NUMSHARP_DEBUG_GUARD_PAGES=1 (no crash).

All six pins fail on net10.0 Release for the documented reason and sit outside CI's
TestCategory!=OpenBugs filter.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…s are transient (no ARC token, no owner / disposed-value table entries), import views carry their release in the lease instead of a delegate, ORT's fresh shape array is adopted, the feed check is O(n) - the bridge's own cost of a 27-input / 25-output decoder step 92 -> 65 us (-30%) and 47.8 -> 35.8 KB (-25%), tokens unchanged

Found while upgrading a consumer (sappho192/EDMTranslator, a BERT + GPT-2 translator whose greedy search runs
session.Run(Dictionary<string, NDArray>, zeroCopyOutputs: true) once per generated token and feeds the 24 present.*
views back as the next step's past_key_values.*). Measured with a mock of its merged decoder (the same 27 inputs and
25 outputs, names, dtypes and growing cache, but ConstantOfShape / Slice / Concat instead of compute), so a C# loop
around it measures managed overhead: route A = ORT's own Run(RunOptions, names, OrtValue[], outputNames) feeding the
returned present.* OrtValues back (the floor), route B = this tier as the consumer uses it; B - A is the bridge.
A phase probe through the DotNetRunScript friend access split one run (past length 10):

    whole Run(dict, zeroCopyOutputs)    268 us  52.4 KB
    ORT Run + dispose outputs (floor)   170 us   3.2 KB
    27 x PrepareInput + dispose          42 us  17.1 KB   <- 632 B and 1.6 us per input
    ViewOutputs (25 views) + dispose     58 us  31.2 KB   <- 1.25 KB per view
    RequireEveryInput / outputs / lease   3 us   0.3 KB

and dotnet-trace showed where an input's cost went: OrtTensor.Dispose 50% (the token's Unpin -> FinalRelease
takeover: a shadow OrtValue built to release the native value once, 34%), ConditionalWeakTable.AddOrUpdate 9%
(ValueOwners, plus OrtValueBooking.Tokens inside Track), ORT's CreateTensorValueWithData 36%. All of that machinery -
the ARC token in ORT's _memHandle slot, the owner table, the disposed-value table - exists so that a value a CALLER
can reach survives a Dispose on another thread, being held past its handle, or being handed to an import verb. The
handles this tier makes for its own inputs (and RunInto's pre-allocated outputs) are none of that: created inside
one synchronous call, read by ORT during it, disposed in the call's finally on the same thread, never returned.

1. Transient handles. OrtTensor(..., bool transient = false): a transient handle skips OrtValueBooking.Track and the
   ValueOwners registration, and its Dispose skips ValueOwners.Remove / DisposedValues.AddOrUpdate (ORT's own
   Dispose releases the value directly; there is no token to take over). Kept: the counted reference on the buffer,
   Source, the data range, the export counters, the finalizer safety net. The flag flows through AsOrtValueCore,
   TextAsOrtValue, PrepareInput, PrepareTextInput, PrepareOutput and SeparateInputsFromOutputs; only the synchronous
   Run(dict) and RunInto pass true. The asynchronous verbs, whose handles are disposed on a continuation, pass false
   explicitly (SeparateInputsFromOutputs now takes the flag, so RunIntoAsync's overlap copy stays non-transient), and
   every public export verb keeps the default. The collectors (CopyOutputs / ViewOutputs / pass-through outputs) read
   only the handles, never a transient value, so an output that is a fed input is unaffected.
2. ImportLease(OrtValue value, Token token, bool ownsValue, long ownedBytes): the import view's release (dispose an
   owned value, DisposeThenReleaseView with a token, give back a view lease, or only keep a caller-owned value
   reachable) is decided from fields at release time instead of a per-view delegate - and a closure in two of the
   four cases. Same four behaviours, same once-only flag; Disarm also drops the held value. DisposeThenReleaseView is
   internal so the lease reuses it.
3. ShapeFrom(dims, adopt: true) at the two import sites that pass OrtTensorTypeAndShapeInfo.Shape, which ORT fills
   fresh per call: the Shape keeps the array (new Shape(dims, copy: false)) instead of copying it. The other callers
   pass shared arrays (OrtTensor.ShapeNoCopy) and keep copying.
4. RequireEveryInput uses the fed key collection's own Contains (a dictionary's O(1) lookup) instead of copying it into
   a List with a linear Contains: O(inputs x fed) became O(inputs) - 27 x 27 compares per GPT-2 step, 130 x 130 for a
   64-layer decoder. A plain sequence is still copied into a list.

Measured (mock decoder, 30-step sequences x 40 reps, A/B interleaved per process, net8.0, ORT 1.22.1, i9-13900K with
other work running; best per run, three alternating runs per tree plus two earlier ones):

                         B - A best us/step               bridge bytes/step
    journey5 e25b752    92.1  93.9  94.3                 47,814
    this commit          65.4  66.3  (79.8, 76.7, 97.4)   35,820   (route A's own logits view: 6,255 -> 6,149)

On the real model (jesc-ja-en-translator's decoder_model_merged.onnx, 8 intra-op threads, 24 steps x 6 reps) both
routes on both trees produce the same greedy token checksum (171635); the per-step time is ONNX Runtime's (33-50 ms
on the loaded machine) and the difference is below its noise, while the bridge's bytes drop 48,211 -> 35,806 per step.

Tests: test/NumSharp.Tests.Interop.OnnxRuntime green on both legs and both frameworks - ORT 1.16.0 (floor) 362 passed
+ 24 async skipped as designed, ORT 1.31.1 386/386, net8.0 and net10.0 - plus RunTierTransientHandleTests (5),
pinned through OrtValueBooking.TakenOverReleases (one takeover per disposed value that carried a token): a synchronous
Run, a 5-step zero-copy feed-back loop and RunInto move it by 0; RunIntoAsync by 2 (its input and output keep their
tokens); a public AsOrtValue handle by 1 (HasToken true). Negative control: the same class on journey5 fails the three
synchronous tests with exactly the old counts (2 for add_f32's two inputs, 5 for five steps, 2 for RunInto's input and
output) and passes the two "keeps its token" ones. Each test reports Inconclusive on an ORT without the takeover
members rather than pass for the wrong reason.

Docs: the package README, docs/website-src/docs/interop/onnxruntime.md and .claude/rules/interop-packages.md said
every value the bridge touches carries an ARC token; they now name the exception (the synchronous Run / RunInto's own
values, which never reach the caller; an asynchronous run's keep theirs).

Not changed, measured: each output view still costs ~1.15 us in ORT's own GetTensorTypeAndShape + the
GetTensorMutableRawData that re-queries it internally + the memory-info check (a CPU Run output reports the arena
allocator, so it never equals OrtMemoryInfo.DefaultInstance and the name check stays); the second UnmanagedStorage per
view is the base that makes the view non-owning (NumPy's base / OWNDATA invariant).
…reversed or stepped C-ordered view and an F array is F-ordered where NumPy's NpyIter gives C

Second hour of the 2026-11-08 parity QA journey (docs/bugs/parity-qa-2026-11-08.md).

QB-07 (S3 layout, plus another ravel('K') element order and an extra copy):
np.add(m[::-1], np.asfortranarray(n)) is F-ordered (strides (8, 24)) where NumPy 2.4.2 gives C
(32, 8): NpyIter's npyiter_find_best_axis_ordering compares |strides| per operand and C order wins
a conflict. NumSharp's K-order plan (IsKOrderCVoter) agrees with NumPy, but the binary routes
(DefaultEngine.BinaryOp.cs, direct and Tier-3B) and the comparison routes (DefaultEngine.CompareOp.cs)
then run the legacy "looser-F preservation" step, ShouldProduceFContigOutput: some operand strictly F
and none strictly C -> result.copy('F'). A reversed or stepped view is neither, so it never votes.
Every dtype and 17 ufunc families are affected (add ... power, floor_divide, mod, fmod, fmax, gcd,
shifts, bitwise, logical_and, the comparisons, np.isclose); arctan2 / hypot / logaddexp / copysign /
heaviside / nextafter / logical_xor / ldexp / where / clip / evaluate match. Pin
ParityQb_QB07_BinaryOfAReversedViewAndAnFArray_IsCOrdered (fails on net10.0 Release as documented).

QB-04 gains a caveat: the oracle's ErrorTypeMap names item()'s IncorrectSizeException as the house
counterpart of NumPy's ValueError, so the exception-class question is recorded as Open question 1.

Covered with no new finding this hour: the registry's "[known bug]" excuses (all but a documented one
no longer reproduce; listed as stale gate hazards in Observations), buffered casts around the 8,192
buffer, every batch re-run under NUMSHARP_DEBUG_GUARD_PAGES=1, text I/O round 2 (68 probes identical,
error texts included), composite numerics bit for bit (interp, unwrap, gradient, linspace /
logspace / geomspace, windows, rounding, ...), the neighbours of recently landed fixes, legacy
chararray semantics, np.evaluate mixed-width and text trees, np.random boundary parameters. The
directions queue is updated and gains four new directions (25-28).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…sed - Dispose and a full collection leave the file mapped (Windows refuses to delete it), where a C-ordered map is freed by Dispose

Found by KokoroSharp's voice loader on NumSharp 0.80.0 (2026-11-09): a test that loads a Fortran-ordered voice through
NpyFormat.OpenMemmap and then deletes its temp folder failed with "The process cannot access the file ... because it is
being used by another process". Reproduced on journey5 (a98e9ce) by this test (net10.0, Windows): the C-ordered half
passes (the file deletes right after Dispose), the Fortran-ordered half throws IOException even after
GcQuiescence.CollectFull().

Cause as far as read, not fixed (the owner's rule: a bug found on the way gets an [OpenBugs] reproduction and waits):
NpyFormat.OpenMemmap returns a Fortran file as a transposed view of an internal mapped array
(reshape(shape[::-1]).T), and the MemoryMappedFile + view accessor are owned by that internal array, which nothing
disposes; disposing the returned view does not release them, and in the probe neither did a full collection with
finalizers. NumPy's `del a; gc.collect()` closes the map in both orders.

Why the suite never saw it: Mmap_FortranOrder_MatchesNormalLoad (NpyMemmapTests) maps a Fortran file and disposes it,
and MappedFileCleanup.DeleteDirectory, which removes the temp folder, swallows a failed delete after its full
collection - so the leaked map left a folder behind instead of failing a test.

Observable on Windows only (Unix unlinks a mapped file), so the test reports Inconclusive elsewhere.
…ation-strides fix misses, and np.matmul's surviving NaN

QB-08 (S3, strides only): the ufunc result-layout re-port (18314ca) routed allocators through
Shape.ForFreshAllocation so a zero-size allocation gets NumPy's all-zero strides, but np.full_like
((0, 32, 8) for a (2, 0, 4) array), np.unique of an empty array ((8,)) and np.array of a C# array
((24, 8)) still build their shapes without it; np.tile(x, 0) and np.fromstring("", sep=" ") go the
other way ((0,) where NumPy's reshaped view keeps (8,)). 44 of 50 zero-size producers match.

QB-09 (S1, NaN sign / payload): np.signbit(np.matmul([-nan, 1], [1, nan])) is False where NumPy is
True: NumPy hands vector-vector products and 1 x 1 outputs to the dtype's dot function (ddot), which
keeps the first operand's NaN, as NumSharp's 1-D np.dot already does since PA3b; NumSharp's matmul
and 2-D np.dot routes accumulate in the managed kernels' order. With distinct payloads, 1-D @ 1-D,
1-D @ 2-D, (1, n) @ (n, 1), (1, n) @ (n, 2), (2, n) @ (n, 2) and 2-D np.dot differ; NumPy's gemv
shape and einsum match.

Also covered this hour with no new finding (journal progress log): the legacy post-step sites besides
QB-07's three routes, np.emath (inherits QB-05 / QB-06, recorded in their entries), a second blanket
audit over 36 real unary ops x 3 float widths x 40,000 values, the dot family's shapes / dtypes /
layouts, private copies of fixed math helpers (a latent magnitude-ordered complex ArgMax kernel no
public route reaches is recorded as an observation), 339 offset-view probes over ~110 functions
(the class oracle.md calls the worst open one now matches everywhere probed), an [OpenBugs] audit
(the same 12 passers the first journey listed), and an attempt to model F5's complex power (no
UCRT cpow candidate reproduces NumPy, F5 stays documented).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…ns: median family, real correlate / convolve, float32 exp2, spacing, sNaN through hypot / nextafter

A new direction of the 2026-11-08 parity QA journey: which NaN survives (sign, payload, quiet bit)
where NumPy's answer is a NaN, compared raw instead of as a token (the oracle compares NaNs as
tokens outside its nan_order tier, so none of these could fail a gate).

- QB-10 (S1, sign visible): real np.median / percentile / quantile return the FIRST NaN; NumPy's
  _median_nancheck returns data.take(-1), the last element after its partition, so
  np.signbit(np.median([-nan, 1, nan])) is False in NumPy and True here; np.nanmedian([-nan]) is
  NumPy's arr1d[-1] (-nan) where NumSharp returns the canonical +nan. Complex median already follows
  the last-element rule (c39a29b).
- QB-11 (S1, sign visible): real np.correlate / np.convolve in the small-kernel regime that
  float-math.md calls byte-identical keep another NaN: np.correlate([-nan, 1], [1, nan]) is -nan in
  NumPy, +nan here; 8 of 9 / 63 of 65 full-mode outputs differ in payload. PA3b fixed the complex path.
- QB-12 (S1, sign visible): float32 np.exp2 answers every NaN with 0x7fc00000 (the rule of NumPy's
  SIMD exp / log / sin / cos kernels); NumPy has no SIMD exp2 on win-amd64 and the CRT exp2f returns
  its NaN argument (negative and signalling NaNs keep sign and payload, the latter quieted).
- QB-13 (S1, payload): np.spacing's SIMD body adds 1 to a NaN's payload (every full 4 / 8-lane vector)
  while its scalar tail returns a float64 quiet NaN unchanged, so one array disagrees with itself;
  the float32 tail leaves a signalling NaN signalling. NumPy's npy_spacing returns the input quieted.
- QB-14 (S1, quiet bit): a signalling NaN comes out of np.hypot still signalling (NumPy quiets it)
  and out of np.nextafter quiet (NumPy's CRT nextafter returns it as it is).

All five pins fail on net10.0 Release for the documented reason. Covered in the same sweep with no
new finding: 16 binary libm / selection ops, 30 reductions and compositions, argsort / sort / unique
/ searchsorted / lexsort / isin orders among several NaNs, and 54 unary / binary ops over NaN cycles
at lengths crossing every vector width (vector bodies and scalar tails agree with NumPy everywhere
but in spacing). tensordot of two vectors inherits QB-09 (recorded in its entry).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…es and signalling NaNs through quantiles, one-element products and scans

The 2026-11-08/09 NumPy parity QA journey (docs/bugs/parity-qa-2026-11-08.md), hour two. Four new [OpenBugs]
pins in test/NumSharp.Tests/OpenBugs.ParityQb.cs, each verified failing on net10.0 Release for the documented
reason; nothing in Core is changed (the owner decides what to fix).

QB-15 (S1) np.median / np.nanmedian / np.ma.median of ONE element return (x + x) / 2.
  np.median(np.array([1e308])) is inf (NumPy 1e308); np.median(np.array([40000], float16)) is inf (NumPy 40000).
  Every route reaching a one-element line: 0-d input, one-element array, keepdims, out=, overwrite_input, a
  length-1 axis, nanmedian of a slice with one non-NaN value, ma.median of one unmasked value.
  Root cause: DirectILKernelGenerator.WriteCell (Quantile.cs) takes np.median's two-sample mean when
  gamma >= 0.5; for a one-element line the plan's gamma is 1 (NumPy's _get_indexes moves an upper-bound index
  to -1, so gamma = 0 - (-1)) and prev and next are the same element. Values under half the dtype's maximum
  come back right, which is why no gate saw it. percentile / quantile of one element take the lerp and match.

QB-16 (S1) np.median's middle mean is not np.mean's.
  float16: np.median([40000, 40000]) is inf (NumPy 40000: _mean sums float16 in the float32 loop); the flat
  nanmedian of [30000, 40000] is inf (NumPy 35008). WriteCell rounds the pair's sum to float16 first and its
  comment calls that NumPy's rule. complex128: np.median([1+infj]) is 1+infj (NumPy nan+infj: _mean divides by
  n+0j through CDOUBLE_divide's Smith algorithm, whose in1i*rat / in1r*rat terms turn the other part NaN when a
  part is infinite); np.median([inf+1j, inf+0j, -1+0j]) is inf+0j (NumPy inf+nanj); np.median of an overflowing
  pair [1.7e308+0j, 1.7e308+1j] is 1.7e308+0.5j (NumPy inf+nanj): the complex route returns the middle element
  or 0.5*a + 0.5*b. Same root cause as OpenBugsReductionTests.Median_OfNegativeZeros_IsPositiveZero, whose
  remarks say the values differ only in a zero's sign - these are value differences that pin does not exercise.

QB-17 (S1, overflow edge) np.nanmedian / np.ma.median along an axis shorter than 600.
  NumPy's _nanmedian sends such an axis to _nanmedian_small = np.ma.median, whose N-D _median gathers low_high
  (the middle element TWICE for an odd count), sums it with np.ma.sum and halves it, so
  np.nanmedian(np.array([[1e308, 1e308, 1e308]]), axis=1) is [inf]; NumSharp returns [1e308]. Flat nanmedian
  and the >= 600 axis form go through np.median and stay finite in both. A NumPy quirk, but its behaviour; the
  QB-15 fix must keep it (the length-1 axis currently matches only because both double the element).

QB-18 (S1, quiet bit only) signalling NaNs.
  float32 / float16 median / percentile / quantile (every method, lower / nearest included, flat and per axis)
  quiet a slice's sNaN that NumPy returns untouched (_median_nancheck's data.take(-1), _quantile's arr[-1]);
  float64 keeps it. np.prod of one element (flat or a length-1 axis) keeps an sNaN signalling where NumPy's
  1 * x quiets it (the real-dtype face of Prod_OneComplexElement_IsTheIdentityTimesTheElement). cumsum / cumprod
  quiet a leading sNaN NumPy's accumulate copies (the root cause of CumSum_Flat_KeepsFirstNegativeZero / the
  complex CumProd pin).

Coverage logged without new findings: NaN bits through every float / complex cast pair (q15, 138 probes; only
the documented complex-add NaN order), integer extremes of six integer dtypes through 30 binary ops, 15 unary
ops and 22 reductions / casts (d23, 396 probes). Directions 26 / 27 marked done (their log rows were already in);
two new directions queued: overflow edges of composite statistics (30) and NumPy's size-switched routes (31).
…18314ca (the ufunc result-layout re-port) the generator refused to publish on journey5 ("Oracle layout variant 'ufunc' (layout_parity_oracle.jsonl) has no entry in coverage/oracle_map.json's 'layout' section"), and test_oracle_evidence's zero-problems gate failed with it; each ufunc case now joins the function it calls (ufunc:neg -> numpy.negative, ufunc:add0d -> numpy.add, ufunc:mediank1 -> numpy.median, 34 case names)

Found by the third pass of the np.* signature gap map, whose completeness numbers come from the coverage artifact:
`python coverage/generate_coverage.py` on journey5 (882a66a) exited with

  NumPy-oracle evidence join failed:
    - Oracle layout variant 'ufunc' (layout_parity_oracle.jsonl) has no entry in coverage/oracle_map.json's 'layout' section.

18314ca added a family "ufunc" to test/NumSharp.Tests/Backends/corpus/layout_parity_oracle.jsonl (263 cases: a fresh
ufunc result's layout, the UF / UF1 tables of test/oracle/gen_layout_parity_oracle.py) without a map entry. The join is
strict by design (a committed contract that resolves to no catalog row fails the generator), so the docs workflow's
"Generate coverage artifact" step and the coverage test suite (CommittedCorpusTests.
test_every_committed_contract_resolves_with_zero_problems) were red on journey5; master and v0.80.0 do not have the
family.

The family exercises some 30 functions, so one family-wide entry would credit every one of them with all 263 cases.
The variant follows the case name instead, the way the reduce family maps by its leading reduction:

- coverage/oracle_evidence.py `_layout_variant`: a ufunc key maps to "ufunc:" + the case name before the source recipe
  (`add0d_c3` -> `ufunc:add0d`, `around0_bcx` -> `ufunc:around0`, `adds111_f213` -> `ufunc:adds111`).
- coverage/oracle_map.json, layout section: one entry per case name, each the function its generator lambda calls -
  neg negative, sqrt / sqrti sqrt, isnan, add0d / addself / addrow / addc / adds111 add (the operator forms x + 1.5,
  x + x, ...), muli multiply (an int32 array * x), maximum, copysign, gt greater (x > 7.0), eq equal, land logical_and,
  where, divmod, modf, frexp, array, n2n nan_to_num, around0 / around1 around, diff1 diff, cumsum1 cumsum, cumprod0
  cumprod, nancumsum2 nancumsum, sumk1 sum, median1 / mediank1 median, tril, shl left_shift, ldexp, clip. The section's
  note says so. Every entry is used (the join fails on a stale one).
- coverage/test_oracle_evidence.py: the committed-corpus test checks the case-name mapping and that numpy.negative,
  numpy.median and numpy.add carry their ufunc-family layout contracts.

Verification: python -m unittest discover -s coverage -p 'test_*.py' 91 tests OK, node --test coverage/test_dashboard.cjs
26 pass; the generator on the patched tree: 5,223 catalog rows, expanded 2,017 / 4,844 available, headline 533 / 560,
887,513 oracle contracts, headline 537 / 560 oracle-verified.
882a66a) with every finding validated against its ground truth - the compiler for M and the absent names, NumPy's runtime for M / X / Q / L - and the change list docs/plans/API_PARITY_DIFF.md (one patch per API, the absent names, the refuted findings); 1,293 NumPy names, 719 with NumPy's signature, 574 to change; 19 findings refuted and 2 probe artifacts fixed

The owner's request: "Do another pass finding all the differences, validating all finds we had. Then dashlist diff show
me what will need to change to gain parity and completeness".

The pass
--------
dump_modules.cs -> binding_probe.py -> signature_diff.py on journey5 at 882a66a (the commands of API_DIFF_GAP.md
§10). 1,533 C# names, 1,293 with a NumPy counterpart, 7,825 compile probes (491 fail at C# 12, 458 at C# 14). Against the
last committed data (c32a300, 2026-10-20): only the one-overload collapse of sum / prod / mean / var / std (38bcad0)
removed marks (33: np and ndarray O / M / B / L of those five), 78 spellings are new (constructors of NDCharArray,
NDMaskedArray, NDMatrix, np.ndindex, np.nditer that are public now; the second ndarray constructor takes `engine`), and
every other finding is the same. Regenerated: docs/plans/numpy-signature-diff.{md,csv}, numpy-signature-marks.md.

Validation (new tool tools/api-parameter-parity/validate_findings.py)
---------------------------------------------------------------------
signature_diff.py decides most marks from reflection and from NumPy's signature as it READS it (runtime signature,
completed from the .pyi stubs and numpydoc). The tool asks the ground truth of each finding:
- Probe artifacts: a probe failure with a non-binding code. Found 2 (CS0246 'UnmanagedStorage' not found): the probe
  file lacked `using NumSharp.Backends;`, so the R probes of the NDMatrix and NDCharArray constructors (storage
  parameter) read as B marks. Fixed in binding_probe.py (B 42 -> 40; with the fix the NDCharArray constructor shows a
  real Q: `shape` as a sequence).
- M by the compiler: every missing keyword-capable NumPy parameter compiled by name on the name's base call (the R
  probe's), `np.sum(a, initial: default)`. 429 confirmed by CS1739; 10 REFUTED: the call compiles because C# binds a
  member the class inherits from NDArray while the gap map reads an own-members group's own overloads (NDMatrix
  argsort kind / stable, choose / clip / compress / cumprod / cumsum / take / trace out, NDArray<T>.reshape order). The
  call reaches NDArray's member, so the fix is still the class's own member; it is listed as refuted, not as a gap.
- Absent names by the compiler: `nameof(np.X)` / `nameof(NDMaskedArray.X)` of every NumPy name a group lacks: 94 of 96
  confirmed (CS0117 / CS1061 / CS0571); the 2 others are dunders (`__version__`, `__array_namespace_info__`).
- M and X by NumPy: NumPy checks keyword NAMES before values (probed for C functions, ufuncs, dispatchers, methods,
  numpy.ma wrappers and constructors), so each callable is called with None placeholders and one keyword after a bogus
  control keyword proves the keyword check is reached (real calls for frompyfunc, printoptions, the ndarray
  constructor and the numpy.ma stacks). M 436 confirmed, 3 REFUTED: `ndarray.std(correction=)` and
  `ndarray.var(correction=)` (NumPy's stubs promise them, the methods refuse: "_std() got an unexpected keyword
  argument 'correction'"), `ndarray.setfield(val=)` (the docstring says val, the runtime keyword is value). X 141
  confirmed, 1 REFUTED: setfield's `value` (NumSharp is right).
- Q by NumPy: a real call with a tuple. 74 confirmed, 5 REFUTED where the stubs over-promise: np.ma.compress,
  MaskedArray.compress, matrix.argmax, matrix.argmin ("'tuple' object cannot be interpreted as an integer") and
  matrix.ptp ("unsupported axis").
- L by NumPy: the runtime signature's default, else a real call with None (random samplers with domain-valid
  arguments: logseries p = 0.5, zipf a = 2, so no rejection sampler can loop). 116 confirmed, 0 refuted.
O and T are reflection facts (the overload list and the parameter types) and were not re-proved.

The change list (new tool tools/api-parameter-parity/parity_diff.py, output docs/plans/API_PARITY_DIFF.md)
--------------------------------------------------------------------------------------------------------
One ```diff block per C# spelling: `-` what must go (overloads beyond the one NumPy has, parameters NumPy lacks, a
dtype as NPTypeCode / Type or a generic type argument, a type that refuses None / a sequence), `+` what must come
(NumPy's missing parameters with defaults, NumPy's keyword for a renamed one, DType, the types that take None / a
sequence), context lines with NumPy's signature, the overload that stays and the calls the compiler refuses today.
Refuted findings are left out and listed with their evidence. Then the completeness part: the 96 NumPy names the
compared groups lack, the 15 public modules without a facade (140 names) and the coverage artifact's census of every
other missing row (2,823 API rows: scalar-type protocols 832, ufunc object methods 810, masked / record / memmap
array protocols and methods, ...).

Numbers after validation: 1,293 NumPy names with a C# spelling, 719 with NumPy's signature (101 of them miss only
plan-excluded parameters), 574 to change: O 342 spellings (903 overloads to delete), M 216 (out 104, dtype 65, where 55,
order 20, copy 12, ...; 68 renames), X 90, T 24, L 115 (73 of them a Shape size that refuses null), Q 73, B 40.

Docs
----
- docs/plans/API_DIFF_GAP.md: a refresh note (the validated change list supersedes the per-package counts of
  2026-10-17), §10 the two new commands and what they check, §11 the limits the validation found (NumPy's stubs and
  docstrings misstating keywords and tuple axes, own-member groups vs inherited overloads, the probe file's usings).
- .claude/rules/oracle.md: the inventory's marks are raw; the validator and the change list are where they are checked.

Verification: validate_findings.py ran end to end (summary above); parity_diff.py regenerates the committed list
byte for byte from the committed CSV and the validation JSON; documentation check clean on the three tools.
…inst NumPy's overlap rules (an overlapping value, an index that is a view of the destination)

The 2026-11-08/09 NumPy parity QA journey (docs/bugs/parity-qa-2026-11-08.md), hours two to three. Three new
[OpenBugs] pins in test/NumSharp.Tests/OpenBugs.ParityQb.cs, each verified failing on net10.0 Release for the
documented reason; nothing in Core is changed.

QB-19 (S1) a fancy assignment whose VALUE shares memory with the destination reads it while writing.
  a = np.arange(10); a[[1, 2, 3, 4]] = a[:4] -> NumSharp [0, 0, 0, 0, 0, 5, ...], NumPy [0, 0, 1, 2, 3, 5, ...].
  Every dtype; 1-D index arrays of any integer type and a raw long[]; permutations, duplicates, negative indices,
  2-D rows, coordinate pairs. NumPy's array_assign_subscript (mapping.c) copies a value that may share memory with
  the array (solve_may_share_memory(self, tmp_arr, 1) -> PyArray_NewCopy); NumSharp's fancy setter
  (Selection/NDArray.Indexing.Selection.Setter.cs) has no overlap check, and only a reversed / strided value comes
  out right because it is materialized on the way.

QB-20 (S2 / S1) a fancy assignment whose INDEX array is a view of the destination reads the index while writing.
  a = [2, 0, 1, 3]; a[a[:3]] = 7 -> NumSharp IndexError "index 7 is out of bounds for axis 0 with size 4",
  NumPy [7, 7, 7, 3]; a[a] = a over [1, 2, 3, 0] -> NumSharp [1, 1, 3, 3], NumPy [0, 1, 2, 3];
  np.put_along_axis(a, a, a * 10, 0) -> NumSharp [1, 10, 3, 30], NumPy [0, 10, 20, 30]. NumPy copies every index
  array that may share memory with the destination in the same function; NumSharp reads it lazily during the
  stores. np.put and np.take(out=) with aliasing indices already match.

QB-21 (S1) a boolean-mask assignment copies a reversed overlapping value where NumPy reads through it.
  a = np.arange(6); a[a >= 0] = a[::-1] -> NumSharp [5, 4, 3, 2, 1, 0], NumPy [5, 4, 3, 3, 4, 5]: NumPy's HAS_BOOL
  branch (array_assign_boolean_subscript) has no overlap copy and streams the value in C order while storing.
  NumSharp decides copy-or-alias by the value's layout, NumPy by the index kind (fancy: copy; one boolean array:
  read through). A forward view (a[m] = a[:4]) already matches. The owner may prefer documenting it.

Coverage logged without new findings: composite statistics at +-max / subnormal edges, histogram at extreme
ranges and np.around over decimals -30..40 (q22-q24, 369 probes); 28 reduction forms over extreme magnitudes and
np.prod's order (q25-q26, 2,745; prod is the owner's documented lane-order decision); np.pad x 18 modes and the
shape_base family (q27-q28, 944); histogram's bin estimators, isclose / allclose / nan_to_num / real_if_close /
bincount / digitize / ediff1d (q29-q30, 298); Generator streams across sampler regimes (q31, 176; pareto / power
are the documented expm1 band); 151 functions over 0-d / empty / one-element inputs (q32, 1,208); 40 self-overlap
statements x six dtypes (q33-q35, 427). Directions 16 and 30 marked done, 31 partly, 32 (aliasing round 2) queued.
…ter (value cycling, boolean masks, N-D index shape, aliasing)

The 2026-11-08/09 NumPy parity QA journey (docs/bugs/parity-qa-2026-11-08.md), hour three: the second aliasing
round reached a.flatiter (np.FlatIterator, the NumPy-faithful flatiter; NDArray.flat is the documented raveled
array and is not part of these). Four new [OpenBugs] pins in test/NumSharp.Tests/OpenBugs.ParityQb.cs, each
verified failing on net10.0 Release for the documented reason; nothing in Core is changed.

QB-22 (S2) flatiter assignment does not cycle, cut or flatten its value.
  a = np.zeros(6); a.flat[::2] = [1, 2] -> NumSharp ValueError "cannot assign 2 input values to the 3 output
  values where the mask is true", NumPy [1, 0, 2, 0, 1, 0]. The same for index arrays, boolean masks, a longer
  value (NumPy cuts it), a 2-D target and an empty value (NumPy: a no-op). NumPy's iter_ass_subscript
  (iterators.c) walks the value flat and restarts it when it runs out (np.put's rule).

QB-23 (S1) flatiter reads a boolean index array as the integers 0 / 1.
  np.arange(6).reshape(2, 3).flat[[T, F, T, F, T, F]] -> NumSharp [1, 0, 1, 0, 1, 0], NumPy [0, 2, 4]; a mask of
  the wrong length is accepted (NumPy's IndexError "boolean index did not match indexed flat iterator ..."); the
  setter writes every value into positions 0 and 1. NumPy: iter_subscript_Bool / iter_ass_sub_Bool.

QB-24 (S3) flatiter indexing with an N-D index array returns a 1-D result.
  np.arange(6).reshape(2, 3).flat[[[0, 5], [1, 2]]] has shape (4,) in NumSharp, (2, 2) in NumPy
  (iter_subscript_int allocates with the index's dimensions).

QB-25 (S1, aliasing) flatiter assignment copies an overlapping value / index where NumPy reads through them.
  a = np.arange(10); a.flat[1:] = a[:-1] -> NumSharp [0, 0, 1, 2, ...], NumPy all zeros; a.flat[::-1] = a ->
  NumPy [0, 1, 2, 3, 4, 4, 3, 2, 1, 0]; a.flat[[1, 2, 3, 4]] = a[:4] -> NumPy [0, 0, 0, 0, 0, 5, ...] (the opposite
  of ndarray fancy assignment, QB-19); a.flat[a[:3]] = 7 over [2, 0, 1, 3] -> NumPy IndexError "index 7 is out of
  bounds for size 4", NumSharp [7, 7, 7, 3]. NumPy's iter_ass_subscript never copies; the owner may prefer to
  document these (order-dependent aliasing).

Coverage logged: aliasing round 2 (q36, 210 probes: copyto where=, ufunc out= + where=, clip bounds, scans into
aliased outs, putmask / place / put cycling, compress / choose / take, reductions into a column of their operand,
matmul / dot / einsum / outer / concatenate into an operand, in-place view methods) found no ndarray-level issue
beyond QB-19..QB-21; partition's arrangement between anchors is documented as implementation-specific.
…ne is found in one read (edge selection) - np.argpartition(x, -5) over 51,200 float32 206 -> 23 us (NumPy 91-96), np.partition 170 -> 17 us, 8-14x at 1M; Postprocess.TopK takes np.argmax for k = 1 and selects with np.argpartition for a small k - a vocabulary-row top-5 798 -> 47-59 us, top-1 799 -> 9 us

Found by the EDMTranslator evaluation (K:\source\EDMTranslator-bench, overhead/logits_post.{cs,py}): over one
51,200-wide float32 decoder row NumSharp beat NumPy on argmax, max, sum, softmax and argsort, but lost on
np.argpartition at kth = V - 5 (222 vs 91 us, 0.41x: NumPy hands a single kth of a 32/64-bit type to x86-simd-sort's
AVX2 argselect) and on the bridge's Postprocess.TopK(k=5) (801 us vs NumPy's argpartition idiom 96 us, 0.12x: it
argsorted the whole row). A top-k is a kth within a few positions of an end, where the quickselect still partitions the
whole line pass after pass, moving the values and an int64 index column.

Edge selection (src/NumSharp.Core/Utilities/QuickSelect.EdgeSelect.cs; QuickSelect becomes partial):
  - Gate (IsEdgeSelect<T>, a few compares per line): one kth; m = the kth's side (n - kth for the top, kth + 1 for the
    bottom) <= 64 (NS_EDGE_SELECT_MAX lowers it, 0 = off); n >= max(256, 32 m, m^2); one of the ten dtypes with
    Vector<T> lanes (bool, char, Half, decimal, complex keep the quickselect); NOT the lines NumPy's own linear
    shortcuts serve with NumPy's tie choice (kth < 3 -> DumbSelect, a float kth == n - 1 -> the last-maximum pass).
  - TryEdgeSelect<T, TSide>: one read keeps the m best sorted worst-first and rejects whole Vector<T>s against the worst
    kept (GreaterThanAny / LessThanAny); an admitted element evicts the worst and lands below its equals, so among equal
    values the LOWER index is kept. It declines, the line untouched, on any NaN (the float kernels' compaction keeps its
    NaN-tail policy) or past m (log2 n + 8) admissions (a line sorted the wrong way for its side). A first budget of
    4 n / m let an ascending line pay 0.8 n slow admissions first: 2x slower than the quickselect (51,200: 239 -> 482 us)
    - the budget now follows the random-data expectation m (ln(n / m) + 0.58).
  - argpartition (TryEdgeArgPartition): writes the permutation straight to the output - every unselected index
    ascending (EdgeFillIota: a Vector<long> iota with m holes, scalar-peeled to vector alignment, which the bottom side's
    fill starting m elements in needed: 39.9 -> 35.3 us) and the kept ones from kth outwards in value order. No gather,
    no index column, no scatter.
  - partition (TryEdgePartition): in place, O(m) writes after the read - each block element that was not kept moves
    into a position a kept value left (one 64-bit mask marks the block), then the block takes the kept values sorted.
    Whole-T moves, so a zero keeps its sign.
  - AxisPartition: PartLine<T> (contiguous branch), PartLineF32/F64, ArgPartLine<T> and ArgPartLineF32/F64 try it
    first when the line is contiguous and there is one kth.
  - NumPy has no such path (its introselect_ says "FIXME: useful to check if kth is near the end?"); the arrangement
    around a single kth was never NumPy's here (x86-simd-sort's on NumPy's side), and only the kth value and the
    two-sided invariant are contractual - what the oracle compares (G12, issue #623).

Measured (Release, .NET 8, pinned to CPUs 2 and 4, warm twice, best of 9 windows; old = journey5 e25b752, whose
sorting code equals 882a66a; the two trees alternating twice, best run per side; random randn * 3 data):

                                          argpartition us                partition us
    float32 n=256       top m=5        3.01 ->   2.44  (1.2x)       2.59 ->   2.05  (1.3x)
    float32 n=1,024     top m=5        6.34 ->   2.65  (2.4x)       5.57 ->   2.18  (2.6x)
    float32 n=4,096     top m=5       24.19 ->   3.95  (6.1x)      20.84 ->   3.18  (6.6x)
    float32 n=4,096     top m=64      24.31 ->  14.93  (1.6x)      20.69 ->  12.85  (1.6x)
    float32 n=51,200    top m=2      205.99 ->  18.92 (10.9x)     177.13 ->  15.70 (11.3x)
    float32 n=51,200    top m=5      205.60 ->  22.92  (9.0x)     170.40 ->  16.71 (10.2x)
    float32 n=51,200    top m=64     210.57 ->  54.07  (3.9x)     168.52 ->  39.20  (4.3x)
    float32 n=51,200    bottom m=5   277.99 ->  35.33  (7.9x)     240.59 ->  17.96 (13.4x)
    float32 n=1M        top m=5     5703    -> 596     (9.6x)    4357    -> 563     (7.7x)
    float32 n=1M        bottom m=64 6664    -> 497    (13.4x)    5455    -> 555     (9.8x)
    float64 n=51,200    top m=5      232.24 ->  26.97  (8.6x)     187.72 ->  26.85  (7.0x)
    int32   n=51,200    bottom m=5   662.75 ->  18.09 (36.6x)     544.88 ->  13.38 (40.7x)
    int64   n=51,200    top m=5      189.27 ->  22.32  (8.5x)     111.39 ->  22.80  (4.9x)
    uint8   n=51,200    bottom m=5   321.42 ->  14.77 (21.8x)     230.97 ->   5.29 (43.7x)
  NumPy 2.4.2 on the same pins: argpartition n=51,200 top m=5 91-96 us, partition 112 us; n=1M 3.8-7.8 ms / 2.8-3.1 ms.
  Outside the gate (a middle kth, the float top m=1 last-maximum pass, m=64 below n=4,096): 0.96-1.08x, noise.
  Same tree, NS_EDGE_SELECT_MAX=0 vs on (three rounds each): an ASCENDING line, top m=5 (every element beats the kept
  set until the budget bails out): n=51,200 222.5 vs 223.2 us, 1M 4966 vs 5052 us (parity); n=256 2.90 vs 3.44 us and
  n=1,024 5.74 vs 6.24 us (the half microsecond the bail-out scan costs on a tiny sorted line). The gate's crossover:
  n=1,024 m=32 1.24x, n=4,096 m=64 1.57x, n=2,048 m=64 0.95-0.99x (kept out by the m^2 term).

Postprocess.TopK (src/NumSharp.Interop.OnnxRuntime/Postprocess.cs; the ML.NET bridge exposes the same type): still
compositions of np.* calls, no kernel in the bridge.
  - k == 1, largest, a real dtype: np.argmax(keepdims) + take_along_axis (argmax returns the first NaN, which TopK ranks
    above every number, lower index first: the same answer).
  - 2k <= n and at least SelectionMinSize = 4,096 elements: TrySelectTopK runs np.argpartition along the (moved) last
    axis; when every slice holds exactly k elements at or beyond its k-th value (one np.count_nonzero over the whole
    array) argpartition's set is the ONNX TopK set, else TiedSet rebuilds it - everything strictly beyond the k-th value
    plus the equal ones in index order up to k (count_nonzero / cumsum / nonzero, exactly k per slice); then only the k
    are ordered (indices ascending, a stable sort of their values, descending through the whole-axis route's reversal)
    and moved back as fresh C-contiguous arrays. Declines to the whole-axis sort (unchanged) on a NaN in a largest-first
    call (NaN outranks every number but fails every comparison), a NaN k-th value (a smallest-first slice with fewer
    than k numbers), complex and text dtypes.
  - Below 4,096 elements the selection's dozen np.* calls (~25-35 us fixed) lose to the sort (~13-16 ns per element):
    a (256, 10) top-3 read 37 us sorted against 74 us selected, so it keeps the sort.

    Postprocess.TopK (old = the whole-axis sort for every input)        old         new
    (1, 51,200) float32 k=1                                          799 us  ->   9.1 us  (88x, np.argmax)
    (1, 51,200) float32 k=5                                          798 us  -> 47-59 us  (13-17x; NumPy's idiom 96 us)
    (1, 51,200) float32 k=50                                         800 us  ->  69 us    (11.6x)
    (1, 51,200) float32 k=5, smallest                                762 us  ->  37 us    (20.8x)
    (8, 51,200) float32 k=5                                         6622 us  -> 385 us    (17.2x)
    (64, 1,000) float32 k=5                                          823 us  -> 112-147 us (5.6-7.3x)
    (1, 51,200) int32, ties straddling the k-th value, k=5           569 us  -> 218 us    (2.6x, TiedSet)
    (256, 10) float32 k=3                                             37 us  ->  36 us    (the sort, as before)

Tests:
  - test/NumSharp.Tests/Sorting/np.partition.EdgeSelect.Test.cs (10): kth value == kth over (i * 7919) mod n
    permutations (NumPy's answer by construction) at both ends for m = 1..65, every vector dtype signed and unsigned,
    uint64 above long.MaxValue, duplicates and constant lines, sorted lines (the budget falls back), a NaN line (NaN
    tail in encounter order), signed zeros' bit patterns, 2-D rows (edge) and columns (strided -> quickselect),
    in-place ndarray.partition, and the engagement signature (the unselected indices ascend).
  - test/NumSharp.Tests.Interop.OnnxRuntime/PostprocessTopKSelectionTests.cs (5): 51,200-wide rows vs ORT's own TopK
    (k = 1, 5, 50; values bit-exact), ties at the k-th value and ties wholly inside the k vs ORT, NaN (largest, k = 1,
    smallest, a slice with fewer than k numbers), the smallest side with int32/int64/float64/float16 and the k bound,
    axis 0.
  - Sorting 257/257 and Postprocess 25/25 on net8.0 + net10.0, both again under NUMSHARP_DEBUG_GUARD_PAGES=1 (net10.0);
    a 14,934-case sweep (10 dtypes x 11 lengths around the gate x 5 data kinds x both ends, NaN lines, 2-D) and a TopK
    differential against the old composition (14 dtypes incl. bool, char, decimal, float16; ties; NaN; axis 0;
    k = 1, 3, n/2, n/2 + 1): 1,488 checks, 0 differences.
  - Oracle FuzzMatrix gate: 223 passed, 15 skipped, 0 failed (net8.0 + net10.0). ONNX Runtime bridge suite: 365
    passed, 26 skipped, 0 failed (net8.0 + net10.0).
  - Unit suite with the CI filter: 23,197-23,198 passed; 5 failures on both frameworks that fail identically on a clean
    worktree of the base 882a66a (EmptyLevels_EndTheDims, NaNPriority_MatchesNumPyBits conv complex128,
    ShortRows_SeveralNaNs_KeepTheRunningSumsNaN, SumProd_NaNPayloads_AreNumPys, Trapezoid_NaNSpacing_IsTheProducts-
    FirstOperand - the parity-QA pins of the commits before it), plus 2 on net10.0 only under the full parallel suite
    (InlineReshapeAndResize_AllocateWhatAdoptingTheDimsDoes counts allocations, T2_NonZero_Is_Much_Slower_Than_NumPy
    times NumPy) that pass alone on both frameworks; none touches partition.

Docs: .claude/rules/sorting.md (the edge selection), the bridge README and docs/website-src/docs/interop/onnxruntime.md
(TopK's routes; the new test class in the gate table).
…rg)partition's one-kth-near-an-end path, its gate, bailouts and measured wins

The specialized-path skill's references/catalog.md lists every specialized path that ships, by kind, so the next
optimization starts from an exemplar. d255dcf added one to the "Algorithm swap" table, next to the quickselect it
sits in front of:
  - where: Utilities/QuickSelect.EdgeSelect.cs, tried first by AxisPartition's contiguous line kernels;
  - gate: m (the kth's side) <= 64 (NS_EDGE_SELECT_MAX, 0 = off), n >= max(256, 32 m, m^2), the ten Vector<T>-lane
    dtypes, and not kth < 3 / a float kth = n - 1 (NumPy's own linear shortcuts keep NumPy's tie choice there);
  - the route: one read keeping the m best sorted with a Vector<T> reject, then an iota with m holes (argpartition) or
    O(m) moves (partition), and the bailouts (NaN; more than m (log2 n + 8) admissions);
  - measured: 51,200 float32 top-5 argpartition 206 -> 23 us (9x; NumPy's AVX2 argselect 91-96 us), partition 10x,
    int32 bottom-5 37-41x, 1M 8-14x; an ascending line at parity once the budget followed the random-data expectation
    (a first budget of 4 n / m doubled it); Postprocess.TopK rides it (798 -> 47-59 us).
… number of indices throws ArgumentException with .NET's own text, NumPy's ValueError "incorrect number of indices for array"; journal rows for the object, method-form and argpartition-tie sweeps

The 2026-11-08/09 NumPy parity QA journey (docs/bugs/parity-qa-2026-11-08.md), hour three.

QB-04 (S4, the item() error types; still paused on the journal's Open question 1) gains a symptom found by the
array-object sweep (q39): np.arange(6).reshape(2, 3).item(1, 1, 1) throws ArgumentException "Incorrect number of
indices for array with 2 dimensions." where NumPy 2.4.2 raises ValueError "incorrect number of indices for array" -
the type AND the text differ (three throw sites in Manipulation/NDArray.item.cs). The pin
ParityQb_QB04_ItemOfANonSizeOneArray_RaisesValueError asserts it too; re-verified failing on net10.0 Release.

Coverage logged without new findings:
- q39 (81 probes): fill (overflow, NaN / inf into ints, complex into float, bool, float16 / float32 overflow, 0-d /
  N-d values, broadcast / read-only targets), item, setflags, resize (views, references, refcheck, F order),
  view (size changes and their refusals), getfield / setfield, byteswap, tobytes orders, real / imag setters, the
  shape setter, squeeze / swapaxes.
- q40 (432): 55 ndarray METHOD forms x 8 stress inputs against NumPy's methods: only the known leading-signed-zero
  scan pins differ.
- q41 (1,296): np.argpartition's index at the kth slot among tied values differs on 300 probes (every dtype, lines
  of 4+); documented as implementation-specific (host-dependent x86-simd-sort selection), recorded as an
  observation because quantile.md's "keep NumPy's choice among ties (... the argpartition index)" reads wider than
  the dumb_select / maximum shortcuts it covers.
New observations: item() returns the element's CLR type (unboxing (double)a.item() of a float32 array throws), the
stacking functions have no out= / dtype= / casting=.
…al of a broadcast x return C-ordered results where NumPy's K-order ufunc results keep the input's layout

The 2026-11-08/09 NumPy parity QA journey (docs/bugs/parity-qa-2026-11-08.md), hour three: the layout sweep
(direction 13) ran the 151 functions of the degenerate-input sweep over 11 view layouts (q42, 1,661 probes) and the
result strides of 52 compositions over 8 layouts (q43, 416 probes).

QB-26 (S3, layout) - pinned as ParityQb_QB26_IsrealAndPolyval_KeepTheInputsLayout, verified failing on net10.0
Release; nothing in Core is changed.
  np.isreal(np.asfortranarray(np.arange(12.).reshape(3, 4))) has strides (4, 1); NumPy 2.4.2 (1, 3), because its
  isreal is imag(x) == 0, a comparison ufunc whose fresh result takes NpyIter's K order. DefaultEngine.Isreal
  (Backends/Default/Logic/Default.TypeCheck.cs) answers a real dtype with NDArray.ones(dimensions, bool) in C order
  (transposed float and F integer inputs too). np.polyval([1, 2], np.broadcast_to([3., 1, 2], (4, 3))) has strides
  (24, 8); NumPy (8, 32), because polyval starts from zeros_like(x), whose K order of a row broadcast is F (an F or
  transposed x already matches). Values match in both.

Everything else in the two sweeps matches NumPy, but for cov / corrcoef 1-ULP differences on stepped layouts (the
documented managed-GEMM bound), np.gradient's tuple (compared component by component in q43: equal) and item()'s
QB-04 type. Direction 13 marked done.
…t a length-1 axis return a new object where NumPy returns the input itself

The 2026-11-08/09 NumPy parity QA journey (docs/bugs/parity-qa-2026-11-08.md), hour three: a new technique,
RESULT VIEWNESS AND IDENTITY. q44 (366 probes) asked 61 view-or-copy functions (ravel / reshape orders, squeeze,
transposes, flips, rot90, expand / atleast, diagonal / diag, real / imag, the as* family, astype, view, broadcasts,
splits, unstack, take / compress, require, roll, triu, indexing forms) over 6 layouts whether the result shares memory
with the input, is writeable and owns its data; q45 (165) asked 33 calls whether they return the input OBJECT.
Every view / copy / writeable / owndata answer matches NumPy 2.4.2 except:

QB-27 (S3, identity / flags) - pinned as ParityQb_QB27_ArrayCopyFalseAndSqueeze_ReturnTheInputItself, verified
failing on net10.0 Release; nothing in Core is changed.
  np.array(a, copy: false) returns a different object that shares a's storage AND reports owndata True (two
  objects claiming one buffer); np.squeeze(a) / a.squeeze() of an array with no length-1 axis returns a new view
  reporting owndata False. NumPy returns a itself in both (PyArray_CheckFromAny hands the array back;
  PyArray_Squeeze returns self when unit_dims == 0). np.asarray, np.asanyarray, np.require, np.atleast_Nd,
  np.ascontiguousarray / asfortranarray and astype(copy=False) already return the input itself.
…sed as an index; NumPy's own test_indexing regressions (gh-26542, gh-26844, flatiter) fail on QB-19 / QB-20 / QB-23 / QB-24

The 2026-11-08/09 NumPy parity QA journey (docs/bugs/parity-qa-2026-11-08.md), hour three.

QB-28 (S2) - pinned as ParityQb_QB28_MultiDimensionalIntegerArray_IsAnIndexArray, verified failing on net10.0
Release; nothing in Core is changed.
  np.arange(10)[new long[,] {{1, 2}, {3, 4}}] raises IndexError "only integers, slices (`:`), ellipsis (`...`),
  numpy.newaxis (`None`) and integer or boolean arrays are valid indices"; NumPy's np.arange(10)[[[1, 2], [3, 4]]]
  is the (2, 2) array [[1, 2], [3, 4]] (and NumPy's own test_indexing.py::test_index_is_larger,
  a[[[0], [1], [2]], [0, 1, 2]] = [2, 3, 4], needs it inside a tuple). NumSharp takes a long[] / int[] as a 1-D
  index array and a bool[,] as a 2-D mask, but not an integer array of rank > 1; np.array(long[,]) works. Related
  trap recorded as an observation: a jagged long[][] IS an object[] by C# array covariance, so it binds the
  indexer's params object[] as a TUPLE of per-axis indices, where NumPy 2.x reads a nested list as one index array.

Evidence added to earlier findings: NumPy's own _core/tests/test_indexing.py, ported as probes (q50: its flatiter
half, 46 probes; q51: its ndarray half, 45 probes), fails exactly on the pinned bugs - test_gh_26542 /
test_gh_26542_2d (a[[2, 1, 0]] = a must be [2, 1, 0]; NumSharp [0, 1, 0]) on QB-19, test_gh_26844
(a[2:][a[:-2]] = 3 raises IndexError in NumSharp) on QB-20, test_flatiter_indexing_boolean(_assign) on QB-23 and
test_flatiter_indexing_fancy on QB-24; test_memory_order is the known FancyIndex layout pin. Everything else in
those tests matches.

Coverage logged without new findings (q46-q49, 306 probes): fill_diagonal (wrap, cycling, 3-D, views, casts),
unravel_index / ravel_multi_index (orders, modes, broadcasting, refusals), select, piecewise, the tri / diag / mask
index helpers; 22 view routes over temporaries surviving forced compacting collections and 200 MB of churn; a
broad numpy.ma sweep (81 calls, data and mask); the TODO-marked indexer paths (bool indices, newaxis in setters,
non-contiguous subspace values, squeeze of slices).
…mem_overlap tests ported (25,209 checks), no new findings; NumSharp matches NumPy's reads-own-writes 1-D slice copies
…mial-class ports (990 probes), no new findings; direction 17 done
…s float32 overload (an empty array where NumPy gives three float64 elements); uint64 x int64 sweep (92 probes) otherwise clean
…umPy: three float64 elements; NumSharp: the float32 overload, empty)
… of np.mod / np.remainder / np.linspace (float results for integer arrays, integer bounds past 2^24 rounded to float32)

The 2026-11-08/09 NumPy parity QA journey (docs/bugs/parity-qa-2026-11-08.md), hour four: a new heuristic, C#
OVERLOAD CAPTURE, grown from QB-02 - which np.* overload does a bare C# numeric literal bind?

QB-29 (S1 values, S3 dtype) - pinned as ParityQb_QB29_IntegerLiterals_DoNotBindTheFloatOverloads, verified failing
on net10.0 Release; nothing in Core is changed.
  C#'s better-conversion rule prefers float over double, and over the user-defined conversion to NDArray, so the
  overloads np.mod(NDArray x1, float x2), np.remainder(NDArray x1, float x2) and np.linspace(float start, float stop,
  ...) capture every int / long / uint / short / byte argument:
  - np.mod(np.array([7, -7]), 3) is float64 [1.0, 2.0] (NumPy int64 [1, 2]: the Python int is weak); int8 / uint8 /
    bool arrays give float32; np.mod([5], 0) is NaN (NumPy's integer loop gives 0);
    np.mod([16777217, 33554435], 16777217) is [1.0, 3.0] (NumPy [0, 1]: the divisor became 16777216f). np.remainder
    the same; the % operator is right.
  - np.linspace(-20000001, 20000001, 3) is [-20000000, 0, 20000000] (NumPy keeps the Python ints exactly);
    np.linspace(0, 100000001, 5, endpoint: false) steps by 20000000 instead of 20000000.2 - every integer bound past
    2^24 is rounded to float32 first.
  np.power / fmod / floor_divide / float_power / the shifts bind their NDArray overload (the literal stays weak) and
  NDExpr.Const has integer overloads, so they are fine. np.arange's float overload is QB-02.

QB-02 widened (same root): a C# ulong argument has no overload and binds np.arange's float32 one, so
np.arange(18446744073709551610UL, 18446744073709551615UL, 2UL) is an EMPTY float32 array where NumPy's Python ints
give three float64 elements; the QB-02 pin now asserts it too (re-verified failing).

Coverage logged without other findings: uint64 x int64 through 69 functions (q60; the ufuncs and comparisons at
2^53 + 1, set routines, products, statistics, histograms, gcd, shifts past the width) and the quantile family over
complex and bool inputs (q59).
…/ floor_divide / fmod / float_power / shifts read a boxed C# number as a strong array (np.power(int8, (object)2) is int32; NumPy's Python int is weak, int8) - pinned and verified failing; np.full's C#-typed default dtype noted as a policy split
Nucs added 19 commits November 13, 2026 09:54
… - result structs for NumPy's flag-dependent returns (np.intersect1d return_indices, np.average / np.ma.average returned, np.indices sparse), np.ma.unique's one typed form, np.ma.mask_rows / mask_cols' ignored axis, the polynomial facades' typed power / tolerance / degree, and the subclass weaver's out-parameter fix (no result struct's Deconstruct is woven)

Batch 11 (api-parity/batch-11 2157872), cut from journey5 d752ad2, which had not moved: the owner approved fixes 1, 2
and 4 of 4 (not fix 3, np.frombuffer's dtype twins) and decided its three questions - the dtype functions stay as they
are ("Keep as is"), the polynomial facades are typed only, NumPy's flag-dependent returns become result structs (np.ma
keeps its own typed result). On the way: the subclass weaver wove every result struct's Deconstruct (an `out NDArray`
read as an operand; a reused deconstruction temporary holding a matrix made the next deconstruction hand back the
previous statement's arrays), and NumPy's positional `returned` silently rebinds an old positional keepdims (5 oracle
calls, now named). Gates on the batch tree: unit net10.0 23,749 / net8.0 23,748 passed (the five known Debug-only
failures), the NumPy oracle 387 / 387 on both frameworks, the analyzer suite only its OpenBugs pin, no OpenBugs pin
flipped (the SumIntoOut timing pin is flaky); gap map 1023 names with NumPy's signature, 287 to change.
…s and np.append's one signature (NumPy's Python-object fill values converted where NumPy's loops convert them; a C# number appended as NumPy's array of the Python number), np.lib.stride_tricks.as_strided, np.row_stack and np.vstack / np.row_stack over object arrays of lists, the masked array's base / device / to_device / getfield / setfield, and NumPy's constructors for np.NDIndex, np.NDIterator and NDCharArray

The owner approved all 9 fixes of batch 12 and decided its one question (docs/plans/API_PARITY_DIFF.md, picked and
validated on journey5 cc8fe6a: a C# 12 / net8.0 compile probe, reflection over the overloads, a caller census of the
whole solution, NumPy 2.4.2's answer for every call as the C# fix spells it - scratchpad batch12_numpy_evidence*.txt,
dbg12/numpy12*.txt). Every expected value, dtype and text in the tests is NumPy 2.4.2's.

The owner's answers, recorded for the next rounds:
- np.histogram / np.histogram_bin_edges' bins: "C keep four" - the int, string, double[] and NDArray bins forms stay;
  their [O4] entries are on purpose (docs/plans/API_DIFF_GAP.md).
- Fix 4's row_stack: "support that too" - np.vstack / np.row_stack also take NDListArray (np.char.split's object arrays
  of lists).

Fix 1: np.interp(x, xp, fp, left=None, right=None, period=None)
- One signature with NDArray left / right: the double? and Complex? overloads went (a C# double, int or Complex
  converts implicitly; a nullable variable is passed as `v is { } d ? d : null`, C# has no lifted conversion from
  double? to a class). TensorEngine.Interp takes NDArray left / right too (its object parameters took only a double or
  a Complex).
- The engine converts the fills as NumPy's compiled loops do, only after the operand and length checks, left before
  right (a bad length reports before a bad fill). The real loop is CPython's PyFloat_AsDouble (InterpFillAsDouble): a
  C# literal is its Python object (a number converts; a Complex is "must be real number, not complex", a string "must
  be real number, not str"), any other 0-d array that is not explicit a NumPy scalar (__float__: an np.complex128 gives
  its REAL part, np.str_ / np.bytes_ refused), an explicit array ndarray's float() (one or more dimensions: "only
  0-dimensional arrays can be converted to Python scalars"; a 0-d complex: "float() argument must be a string or a real
  number, not 'complex'"; 0-d text parsed). The complex loop is CPython 3.12's PyComplex_RealAsDouble / ImagAsDouble
  (InterpFillAsComplex): a Python complex or an np.complex128 scalar keeps both parts, anything else the real loop's
  value with a zero imaginary part (so a 0-d complex ARRAY is refused there too).
- A period now discards the fills in the complex loop as well (NumPy's wrapper sets left = right = None): they were
  kept there, unreachable in values, but a fill that cannot convert must not raise under a period.

Fix 2: np.append(arr, values, axis=None)
- One signature (NDArray values): the object overload went - a C# array, number or string binds the NDArray one; a
  List is passed as np.array(list).
- NumPy's asanyarray(arr) / ravel(values) build the ARRAY of a Python scalar: NDArray.PythonScalarAsArray gives a C#
  number literal the Python number's default dtype (int64, uint64 past int64, float64, complex128, discovered as
  np.result_type of the lone literal) whatever its C# width. np.append(int32_array, 5) and np.append(uint8_array, 300)
  are int64 (they were int32), np.append(float32_array, 1.5f) float64, np.append(5, int32_array) int64; a bool, a
  string, a Half / char and a decimal keep their arrays' dtypes.

Fix 3: np.lib.stride_tricks.as_strided(x, shape=None, strides=None, subok=False, writeable=True)
- NumPy's BYTE strides over x's buffer window. The array interface's defaults: x's shape, and x's strides unless x is
  C-contiguous (then the C strides of the given shape). NumPy's error order: "mismatch in length of strides and shape"
  before "negative dimensions are not allowed".
- The statements are NDArray.as_strided (Manipulation/NDArray.StrideTricks.cs), a [SubclassAware] computation: subok
  reads the caller's class, and the subclass weaver hands a function that computes in its own body a PLAIN view of a
  numpy.matrix (found by the subok test; docs/plans/api-parameter-parity.md finding 69). A matrix finalize refusal
  releases the plain view.
- The view is writeable even with a zero stride, as NumPy's: NumSharp's Shape computes a zero-stride layout read-only
  (its broadcast protection, which NumPy applies only in broadcast_to / broadcast_arrays), so the bit is set back for a
  writeable x, and UnmanagedStorage.Alias clears it again for a read-only x.
- [Misaligned] by design: a stride that is not a multiple of the item size, and a view reaching outside x's buffer,
  are ValueErrors where NumPy reads arbitrary memory (NumPy's docs call such calls unsafe).

Fix 4: np.row_stack (NumPy 2.4's deprecated alias of vstack) and the NDListArray form
- np.row_stack(params NDArray[]), (NDArray[] tup, DType dtype, string casting) and (NDListArray[] tup) run vstack's
  statements; the deprecation is in the remarks (NumSharp has no warnings channel).
- np.vstack(NDListArray[]) / np.row_stack(NDListArray[]) (NDListArray.VStack): every part's ids mapped into the FIRST
  part's heap (its own lists keep their identity - NumPy's shared object pointers; another family's lists appended there
  once), np.vstack of the int64 ids (NumPy's shape rule and texts), a new array sharing the heap. A sequence parameter:
  a second params form would make np.vstack() ambiguous.

Fix 5: MaskedArray.base / device / to_device
- @base is the stored data object (np.ma.array(d).@base IS d without a copy, NumPy's `ma.array(d).base is d`; never
  null), device is "cpu", to_device(device, stream) runs ndarray's checks on the data and returns this.

Fix 6: MaskedArray.getfield(dtype, offset=0) / setfield(val, dtype, offset=0)
- getfield: a masked VIEW of the data's field carrying the same mask memory (a masked write through either reaches the
  other), the hard-mask flag and a converted COPY of the stored fill (NumPy's __array_finalize__); setfield: ndarray's
  setfield on the data alone (masked slots and a hard mask included, the mask unchanged).

Fix 7: new np.NDIndex(params long[] shape) - NumPy's np.ndindex(*shape) class constructor (new np.NDIndex(2, 3)).

Fix 8: new np.NDIterator(op, flags = null, op_flags = null, op_dtypes = null, order = 'K', casting = "safe",
op_axes = null, itershape = null, buffersize = 0) - NumPy's defaults on the class constructor.

Fix 9: new NDCharArray(shape, itemsize = 1, unicode = false, buffer = null, offset = 0, strides = null, order = 'C') -
NumPy's chararray constructor: np.char.chararray's statements (CharModule.ChararrayData), the plain array's storage
taken over; a str filler whose store fails now releases the array it was written into. The UnmanagedStorage
constructor stays (NumSharp's plumbing).

Found by the validation and pinned (OpenBugs.ApiParity.cs; api-parameter-parity findings 70-71):
- np.array("x") / np.asarray("x") are unmarked 0-d arrays (NumPy scalars to NumSharp): np.isscalar(np.array("x")) is
  True (NumPy: False), and np.interp refuses left=np.array("5") as an np.str_ where NumPy parses it to 5.0.
- np.concatenate reports a LATER 0-d array with "zero-dimensional arrays cannot be concatenated" where NumPy reports the
  rank mismatch ("all the input arrays must have same number of dimensions, ..."); np.append(a2d, 5, axis: 0) reaches it.

Callers migrated: np.append's own tests of the removed object overload (Scalar_Value and Dtype_Byte check the dtypes
now; the two lifetime tests pass the NDArray), the oracle's double? / Complex? interp fills (OpRegistry,
StringOracleOps.Shape), NumPyKeywordParity.Batch3's hstack / vstack signature check (vstack and row_stack carry the
NDListArray form). Oracle gates: np.row_stack is vstack's equivalent alias (OracleSurfaceCoverageTests), the leak
catalogue measures as_strided and the masked getfield / setfield / to_device.

Docs: .claude/CLAUDE.md (interp, row_stack and the stride tricks in the API index; float-math.md's row),
.claude/rules/float-math.md (np.interp's fill values), creation-manipulation.md (as_strided, row_stack, the NDListArray
vstack, np.append's literal dtypes), masked-arrays.md (the inherited ndarray members), char-chararray.md (the
NDCharArray constructor, the NDListArray vstack), iteration.md (the NDIndex / NDIterator constructors);
docs/plans/API_DIFF_GAP.md (the batch-12 refresh), docs/plans/api-parameter-parity.md (findings 69-71, state log); the
regenerated gap map (API_PARITY_DIFF.md, numpy-signature-diff.md / .csv, numpy-signature-marks.md).

Gates: the solution builds; the C# 12 / net8.0 probe after-mode (approved fixes 1-9): 34 tagged lines, every approved
spelling compiles, the removed object append fails, the 3 unexpected lines are the members left out on purpose (the
masked flags / sharedmask / setflags); unit suite net10.0 23,771 passed / 6 failed and net8.0 23,770 / 6 (the five known
Debug-only failures - Trapezoid_NaNSpacing, SumProd_NaNPayloads, NaNPriority conv complex128, EmptyLevels_EndTheDims,
ShortRows_SeveralNaNs - plus HVStack_Signatures, updated for the NDListArray form and green on both frameworks with the
batch's other classes, 70 / 0); oracle 385 passed / 2 failed on both frameworks, the 2 being the leak-audit and surface
gates of the new names, green after the catalogue entries and the row_stack alias (2 / 2 on both); analyzer 501 / 1 on
both (its known ManagedArrayReturns_AreNotGated pin); OpenBugs 208 tests, the same 34 passing as batch 11, the 2 new
batch-12 pins failing; the documentation sweep over the 26 changed C# files: 0 gaps. Gap map regenerated: 1310 -> 1315
NumPy names with a C# spelling, 1023 -> 1029 with NumPy's signature, 287 -> 286 to change (M 44 -> 43, B 15 -> 13,
Q 1 -> 3, O 249), absent names 76 -> 69; the coverage artifact's headline 366 -> 368, all NumPy APIs 70.1% -> 70.3%.
… - np.interp's and np.append's one signature (the fills converted where NumPy's loops convert them; a C# number appended as NumPy's array of the Python number), np.lib.stride_tricks.as_strided, np.row_stack and the NDListArray vstack / row_stack, the masked array's base / device / to_device / getfield / setfield, and NumPy's constructors for np.NDIndex, np.NDIterator and NDCharArray
…f a string is an explicit array, np.concatenate's rank check in NumPy's order, np.delete / np.insert's typed forms, NumPy's np.broadcast class constructor (over list arrays too), NpzFile's NumPy names, the np.lib.array_utils and np.lib.format modules, np.ma.MaskedArray, np.arange's NumPy signature over PyArray_ArangeObj (float overloads now float64), NumPy's repeat / item errors, the two [Obsolete] members deleted, and Shape-typed np.unravel_index / np.ravel_multi_index

The owner approved fixes 1-4 and 7-15 of batch 13 (not 5: np.where without its object forms; not 6: np.save without its
Array form), with three comments: fix 7 "Also NDListArray i think", fix 8 "reduce the case of the properties, we must
match numpy", fix 12 "but keep existing overloads". Picked and validated on journey5 dc78dad: a C# 12 / net8.0 compile
probe, NumPy 2.4.2's answer for every call as the C# fix spells it (scratchpad batch13_numpy_evidence*.txt), a caller
census. Every expected value, dtype and text in the tests is NumPy 2.4.2's (scratchpad dbg13/*_numpy*.txt).

Fix 1: np.array(string) / np.array(string, DType) / np.asarray(string) mark their 0-d result an explicit array
- As np.array<T>(T) does (MarkExplicitZeroDimArray): np.isscalar(np.array("x")) is False (it was True, the result read as
  an np.str_ scalar), and np.interp's float() conversion parses np.array("5") as NumPy's does. The batch-12 pin
  ApiParity_ArrayOfString_IsAnExplicitArray is a regression pin now.

Fix 2: np.concatenate checks the FIRST array for 0-d, then every array's rank (PyArray_ConcatenateArrays' order)
- A later 0-d array is NumPy's rank mismatch, the first mismatch in order reported: IncorrectShapeException (the house
  type of that ValueError) with NumPy's "all the input arrays must have same number of dimensions, but the array at
  index 0 has 2 dimension(s) and the array at index 1 has 0 dimension(s)"; a 0-d first array keeps "zero-dimensional
  arrays cannot be concatenated". Reached through np.append(a2d, 5, axis: 0). The batch-12 pin is a regression pin.

Fix 3: np.delete(arr, obj, axis) - NumPy's two typed forms, Slice and NDArray, plus one long form
- The int / int[] / long[] / bool[] overloads went (their arguments convert to NDArray; a 0-d integer is NumPy's
  single-index path). The pick's validation said "all compile unchanged", but a C# int converts to Slice AND to NDArray
  (two user-defined conversions), so np.delete(a, 1) was CS0121 with only the pair: one `long obj` form stays, which
  every C# integer binds by a standard conversion and which forwards the Python int as a 0-d literal (the same answers).
  The before-mode probe could not see it (the int overload still existed there); finding 72 records the lesson.
- A bool scalar obj is NumPy's ValueError "boolean array argument obj to delete must be one dimensional and match the
  axis length of 6" (it was an ArgumentException with " (Parameter 'obj')").
- NDArray.FunctionBase's internal long / int[] / long[] / bool[] delete paths went (the NDArray path answers alike).

Fix 4: np.insert(arr, obj, values, axis) - Slice and NDArray obj with NDArray values, plus one long obj form
- The int / long[] / int[] obj forms and every `object values` form went (12 -> 3); the long form for the same reason
  as delete's. A C# number value is NumPy's Python scalar (range-checked, a string parsed, (NDArray)null None); a C#
  List is passed as np.array(list). NDArray.FunctionBase's internal long / int[] / long[] insert paths and
  ScalarValueAsArray went.

Fix 7: new np.Broadcast(params NDArray[] arrays) is public - NumPy's class IS the function
- The parameterless public constructor (shape threw, size 0) went: new np.Broadcast() binds the params form with no
  operand, NumPy's np.broadcast(): shape (), size 1, numiter 0, one empty step ([()]).
- The owner's "Also NDListArray": np.broadcast(NDListArray[]) / new np.Broadcast(NDListArray[]) broadcast
  np.char.split's object arrays of lists over their ids (NumPy's object pointers, so the shape rule and texts are the
  array constructor's) and step through tuples of the lists (List<object> each). iters is refused for list operands
  (its flat iterators would read ids as numbers); a list array and a numeric array do not mix in one object.
  NDListArray.ThrowIfDisposed is internal for the constructor's check.

Fix 8: NpzFile - NumPy's names (the owner: "reduce the case of the properties, we must match numpy")
- NpzFile(fid, own_fid, allow_pickle, max_header_size) and NpzFile(string fid, allow_pickle, max_header_size); files,
  f (null after close), zip (null after close), fid (the stream only when owned, null after close), allow_pickle,
  max_header_size, keys(), values(), items() ((key, value) tuples), get(key, default), close(). The IReadOnlyDictionary
  Count / Keys / Values are explicit; ContainsKey, TryGetValue, the indexer, GetRawBytes, IsArray and foreach stay.
  Files / F / Zip / AllowPickle / MaxHeaderSize / Close() went. np.load passes own_fid.

Fix 9: np.lib.array_utils (NumPy 2.x's public module, absent before)
- byte_bounds(a) -> (long low, long high) from __array_interface__'s arithmetic (C-contiguous: size * itemsize; else
  (n - 1) * stride per axis to the low or the high end, plus one item); normalize_axis_index(axis, ndim, msg_prefix) =
  Shape.check_and_adjust_axis; normalize_axis_tuple(NDAxis axis, ndim, argname, allow_duplicate) with the kept int and
  int[] forms = Shape.normalize_axis_tuple.

Fix 10: np.lib.format (absent before) over NpyFormat
- magic, read_magic ((major, minor)), dtype_to_descr (dtype.str for a legacy dtype, byte order kept; '|O' for a
  StringDType), descr_to_dtype (np.dtype(descr), NumPy's statement for a string), drop_metadata,
  header_data_from_array_1_0 (IDictionary, the shape a copy), write_array_header_1_0 / _2_0, read_array_header_1_0 /
  _2_0 ((long[] shape, bool fortran_order, DType dtype): the header's own descriptor, '>f4' included), write_array,
  read_array, and the constants. NumPy's ValueErrors with NumPy's texts. Not offered: open_memmap, isfileobj.
- NpyFormat: HeaderData carries the descr text (DescrText); WrapHeader's header-too-long refusal is NumPy's ValueError
  "Header length N too big for version=(1, 0)" (it was an InvalidOperationException), which the guess-version writer
  catches to try 2.0 (NumPy's `except ValueError`).

Fix 11: np.ma.MaskedArray(...) - the class call (NumPy: ma.MaskedArray is ma.masked_array)
- masked_array's parameters, forwarding to it; the oracle's ma surface classifies it as an alias of ma.masked_array.

Fix 12: np.arange - NumPy's signature, the existing overloads kept (the owner's "but keep existing overloads")
- NEW arange(NDArray start = null, NDArray stop = null, NDArray step = null, DType dtype = null, string device = null):
  array_arange's argument reading (a lone stop becomes the start; neither is "arange() requires stop to be
  specified."), then UnmanagedStorage.ArangeObj(NDArray, NDArray, NDArray, DType), the generic port of
  PyArray_ArangeObj: the dtype int64 promoted with each argument's own descriptor (DTypePromotion.PromoteTypes over the
  np.asarray dtype of a literal), and _calc_length on the arguments' own arithmetic - Python numbers through PyScalar
  (CPython 3.12, exact ints), a NumPy scalar or array through NumSharp's operators (NEP 50), as NumPy computes on the
  Python objects it was handed (finding 73). Complex: the smaller ceiling of the quotient's two parts. PyFloat_AsDouble:
  a Python complex is "must be real number, not complex", an np.complex128 its real part, an array with dimensions
  "only 0-dimensional arrays can be converted to Python scalars"; truth values of a non-size-1 next / val are NumPy's
  ValueErrors; any OverflowError of the length is "Maximum allowed size exceeded". The operation temporaries are the
  call's own ([NDScoped]).
- The float overloads return float64 (QB02): a C# float is a Python float, np.arange(0f, 1f, 0.1f)[3] is
  0.30000000447034836 (NumPy's np.arange(np.float32(0), np.float32(1), np.float32(0.1))). They were float32 computed in
  float32.
- NEW arange(ulong stop) / arange(ulong start, ulong stop, ulong step = 1): a C# ulong is a Python int by its value (it
  bound the float overload, whose float32 rounding made np.arange(18446744073709551610UL, 18446744073709551615UL, 2UL)
  empty; NumPy: three float64 elements).
- Probed against NumPy over 79 rows (dbg13/arange13*.txt): 74 equal. The other 5: np.arange(5, null) binds the kept
  (double stop, DType dtype) overload (float64; NumPy int64) - [Misaligned], the consequence of keeping the overloads;
  and NumPy's integer setitem range-checks a NumPy SCALAR element where NumSharp casts it (np.arange(np.int64(1000),
  1002, dtype=np.int8) is NumPy's OverflowError) - the NumPy-scalars plan's stage S3, pinned as
  OpenBugs.ApiParity.ApiParity_Arange_NumPyScalarStores_AreRangeChecked.

Fix 13: NumPy's error texts and types (QB03, QB04)
- np.repeat with a negative entry in a count ARRAY: ValueError "repeats may not contain negative values." (period
  included; ComputeCounts, shared by the flat and the axis routes). The broadcast count keeps its documented wording.
- ndarray.item() / item<T>() of a non-size-1 array: ValueError "can only convert an array of size 1 to a Python scalar"
  (it was IncorrectSizeException); item(i, j) / item(i, j, k) / item(params long[]) with the wrong count: ValueError
  "incorrect number of indices for array" (it was an ArgumentException with another text). The masked array's item
  forwards to ndarray's.

Fix 14: the two [Obsolete] members are deleted (the owner's rule: removed, never left [Obsolete])
- NDArray.Normalize() (no NumPy function) and AxisOutOfRangeException (never raised: every axis check raises AxisError).
  References cleaned: the leak catalogue's Normalize entry, ExceptionsDocTests' base-type row, the oracle's error-name
  tables, the website's exceptions page and API toc, .claude/rules/axis-arguments.md.

Fix 15: np.unravel_index / np.ravel_multi_index take a Shape (QB56)
- unravel_index(NDArray indices, Shape shape, char order) / (long indices, Shape shape, char order); the three
  ravel_multi_index forms take Shape dims. NumPy's idiom np.unravel_index(np.argmax(a), a.shape) binds a.shape
  (long[]), a.Shape, an int[] or a tuple.
- TensorEngine.UnravelIndex / RavelMultiIndex take NumPy's intp dims as long[] (they were int[]): a (2**40, 3) shape
  unravels. The dims products (UnravelSize, RavelStrides) detect an overflow exactly with Math.BigMul (the divide-back
  test would throw on long.MinValue / -1, which int64 dimensions reach). Default.Histogram.Dd converts its int bins.

Pins: QB02, QB03, QB04 and QB56 (OpenBugs.ParityQb.cs), the batch-12 ApiParity_ArrayOfString /
ApiParity_Concatenate_LaterZeroDimArray pins and OpenBugs.cs NpArrayOfString_IsAnExplicitZeroDimArray (fix 1 closed it
too; the OpenBugs run found it passing) are regression pins now (the concatenate one expects IncorrectShapeException, the
fix-2 page's type). New pin: ApiParity_Arange_NumPyScalarStores_AreRangeChecked. [Misaligned]:
Arange_IntStopWithANullDtype_BindsTheKeptDoubleOverload.

Layering (the R1 facade ratchet): np.lib.format.dtype_to_descr only forwards; NumPy's dtype.str rule for a non-native
descriptor lives in the IO layer (NpyFormat.DescrOf).

Callers migrated: 46 np.insert lines with `(object)` values or object-typed values (the literal itself, (NDArray)null for
None, OptNd in the char-behavior replay); 21 lines of NpzFile's Files / Count / Keys / Close in the unit, interop and oracle tests,
the NeuralNetwork example and its verification scripts, and the website's NDArray page; the engine test's int[] dims;
the float32 arange tests (np.arange.BattleTests / NumPyPort: NumPy's float64 now); the item() exception tests (Batch8,
ExceptionsDocTests and the website's exceptions / NDArray / getting-and-setting pages); ClipEdgeCaseTests builds its
float32 operand with the dtype (np.arange(10f) is float64 now); the delete lifetime pin builds its long[] index outside
the measurement (a long[] obj now converts at the call site, the caller's temporary).

Oracle: LeakCatalogue entries for np.lib.array_utils.*, np.lib.format.* and NpzFile.keys / values / items / get (the
Close entry renamed close); OracleSurfaceCoverageTests.MaAliases ["MaskedArray"] = "ma.masked_array".

Tools: tools/api-parameter-parity/signature_diff.py registers np.lib.array_utils and np.lib.format as facades;
coverage/object_surfaces.py drops NpzFile's five spelling adapters (Files / AllowPickle / MaxHeaderSize / Zip / Close:
the members match exactly now, and the signature-parity join refused the stale close mapping);
coverage/test_object_surfaces.py reads keys() available and files exact, and its MaskedArray credit count is 82 (stale
since batch 12's base / device / to_device / getfield / setfield, setfield no longer "missing").

Tests: test/NumSharp.Tests/APIs/NumPyKeywordParity.Batch13.Test.cs (29 tests, NumPy 2.4.2's values for every fix).

Docs: .claude/rules creation-manipulation.md (np.arange's signature and kept overloads, the concatenate order, the
delete / insert forms, np.broadcast's constructor and list operands, repeat's text), io-printing.md (NpzFile's names,
np.lib.format), masked-arrays.md (ma.MaskedArray), selection-indexing.md (Shape dims, long[] engines), axis-arguments.md
(np.lib.array_utils), char-chararray.md (np.broadcast over NDListArray); .claude/CLAUDE.md's index; docs/plans
API_DIFF_GAP.md (the batch-13 refresh, the module table), api-parameter-parity.md (findings 71 fixed, 72, 73, the state
log), the regenerated API_PARITY_DIFF.md / numpy-signature-diff.md / .csv / numpy-signature-marks.md.

Gates:
- C# 12 / net8.0 compile probe after-mode (approved 1-4, 7-15): 38 tagged lines, 0 unexpected (fix 12's two
  positional-dtype lines untagged: the owner kept those overloads); np.delete(a, 1) / np.insert(a, 1, 9) compile.
- Unit suite (CI filter): net10.0 23,806 passed / 6 failed and net8.0 23,805 / 6 - the five known Debug-only failures
  (Trapezoid_NaNSpacing, SumProd_NaNPayloads, NaNPriority conv complex128, EmptyLevels_EndTheDims,
  ShortRows_SeveralNaNs) plus Clip_Float32_PreservesDtype, fixed after the run and re-run green on both frameworks.
- NumPy oracle suite: 386 / 387 on both frameworks, the one failure the R1 ratchet on np.lib.format.cs, fixed after the
  run (NpyFormat.DescrOf) and re-run green on both; the leak / surface / undisposed-intermediate gates 36 / 36.
- Analyzer suite 501 / 502 on both (its OpenBugs pin ManagedArrayReturns_AreNotGated).
- OpenBugs: 35 passing vs batch 12's 34 - the one flip NpArrayOfString_IsAnExplicitZeroDimArray, now a regression pin;
  no pin regressed.
- Doc sweep over every changed C# file: 0 gaps. Coverage tool tests: 100 / 100.
- Gap map (dc78dad + batch 13): 1315 -> 1336 NumPy names with a C# spelling, 1029 -> 1047 with NumPy's signature,
  286 -> 289 to change; O 249 -> 250, M 43 -> 44, X 48 -> 47, L 17 -> 16, B 13 -> 12, T 15 / Q 3 unchanged; absent
  names 69 -> 63; modules with no facade 14 -> 12; the coverage artifact unchanged (numpy.lib rows are out by the
  owner's dashboard rule).
… - np.array of a string is an explicit array, np.concatenate's rank check in NumPy's order, np.delete / np.insert's typed forms (plus the one long obj form C# needs), NumPy's np.broadcast class constructor (over NDListArray too), NpzFile's NumPy names, the np.lib.array_utils and np.lib.format modules, np.ma.MaskedArray, np.arange's NumPy signature over PyArray_ArangeObj beside the kept overloads (float overloads float64, ulong forms), NumPy's repeat / item ValueErrors, NDArray.Normalize and AxisOutOfRangeException deleted, and Shape-typed np.unravel_index / np.ravel_multi_index
…0, the local-dev default every assembly and package version derives from

WHY
The owner, 2026-11-13: "By those same rules, lets prepare release notes and for release of 0.81.0." The release
branch release-0.81.0 is cut from journey5's tip (fad1563, the API-parity batch 13 merge; 210 commits past v0.80.0,
178 of them non-merge). A local build and pack of this branch must say 0.81.0, as the release pipeline will.

WHAT (1 file, 1 line)
- Directory.Build.props: VersionPrefix 0.80.0 -> 0.81.0. The release pipeline still overrides it from the tag with
  -p:Version, so a v0.81.0 tag packs 0.81.0 whatever this says; the default makes a local build, a local pack and the
  dashboards' "NumSharp assembly" line agree with the release.

Nothing else names the release wrongly (checked with git grep over everything but docs/releases, docs/plans,
docs/bugs and refs):
- every mention of 0.81.0 already means this release (NumSharp.Interop.TorchSharp "released from 0.81.0" in its
  csproj, build-and-release.yml, .claude/rules/interop-packages.md and the root CLAUDE.md);
- the "New in 0.80.0" XML docs that a diff since v0.80.0 shows are edited lines of members 0.80.0 shipped (the same
  text sits in v0.80.0), not new members mislabelled;
- the examples reference published packages (0.70.0 / 0.80.0), which keep restoring; there is no TorchSharp example
  project that would need 0.81.0 before it exists (examples/NumSharp.Interop.pythonnet.Examples/09-pytorch.cs goes
  through pythonnet, not the TorchSharp bridge).
…since API-parity batch 1 made np.put one NumPy signature (a8bedd2, 2026-11-11), but its [OpenBugs] tag stayed behind; the 0.81.0 release check found it passing in Release and Debug on net8.0 and net10.0

WHY
The 0.81.0 release notes count the parity-QA findings this release fixes. The untagged ParityQb pins said 8 (QB-02,
QB-03, QB-04, QB-43, QB-48, QB-49, QB-56, QB-60), but a run of the whole class (dotnet test --filter
"FullyQualifiedName~ParityQb", Release net10.0, journey5 fad1563 + the version bump) passed 9 of 59: the ninth is
ParityQb_QB59_PutWithAScalarIndex_Binds, still tagged [OpenBugs(IssueUrl = "parity-qa-QB59")].

QB-59 was np.put(a, <scalar index>, v) being ambiguous (CS0121) between put(NDArray, NDArray, NDArray, string) and
put(NDArray, long, object, string). API-parity batch 1 (fix 6) removed the second overload: np.put is ONE signature,
NumPy's (a, ind, v, mode) with NDArray ind and v, so a C# integer index and value both convert to it and the call
binds. The batch's commit noted that np.put(a, 1, 1000) "did NOT COMPILE before"; the pin, which asserts NumPy's
answer through `dynamic`, started passing then and nobody removed the tag.

WHAT
- test/NumSharp.Tests/OpenBugs.ParityQb.cs: the [OpenBugs] tag is removed from ParityQb_QB59_PutWithAScalarIndex_Binds,
  and its <summary> gains the FIXED paragraph the other fixed pins carry (QB-48's form): what fixed it, when, and why
  the tag lagged. The body is unchanged.
- docs/bugs/parity-qa-2026-11-08.md: QB-59's entry gains a "FIXED 2026-11-11 by API-parity batch 1" bullet.

VERIFIED (.claude/rules/tests.md: a pin loses its tag only after it passes in Release AND Debug on both frameworks)
- dotnet test --filter "FullyQualifiedName~ParityQb_QB59": Release net8.0, Release net10.0, Debug net10.0 and Debug
  net8.0 each "Passed: 1".
- The whole ParityQb class on Release net10.0: 9 passed (QB-02, QB-03, QB-04, QB-43, QB-48, QB-49, QB-56, QB-59,
  QB-60), 50 failed (the open findings).
…ry commit since v0.80.0 - this is the CITED revision (the ledger a later sweep reads its bucket 1 from); the next commit strips the hashes as the published notes carry none

WHY
The owner, 2026-11-13: "By those same rules, lets prepare release notes and for release of 0.81.0." The same rules
as 0.80.0: CHANGELOG_STYLE (.claude/skills/changelog/references/CHANGELOG_STYLE.md) for the format, the changelog
skill's 5-bucket sweep for the content, the owner's coverage-line wording, a top-10 GitHub list for the new np.*
functions, every figure read from inventories run on the final code, and no hashes in the published file (§6: cite
while drafting, strip before publishing, the last cited revision stays in history - this one).

THE SWEEP (v0.80.0..release-0.81.0: 179 non-merge commits, every body read, oldest first, ten size-bounded batches)
- Cited (a line carries the hash): 41 distinct commits, 130 citations.
- Package section §7A (NumSharp.Interop.TorchSharp, no hashes by rule): 516d003, d4038f1, 36e30b4.
- Dashboards & Docs §7B (no hashes by rule): 0fe1bb5 (coverage generator 1.17.0).
- Folded into a cited line: ecd7023 (the keeporder branch commit whose tree 18314ca re-ported), 81fce22, b776ed2,
  1c9cd45 (cited as follow-ups).
- Excluded: the second parity-QA journey's ~110 pins and journal rows (one Testing & Tooling line), the consumer
  [OpenBugs] pins (Bitmap, Bigtree, SharpCV, KokoroSharp, OptunaSharp, ONNX Runtime XNNPACK before its fix, the
  F-order memmap), docs-only commits (specialized-path catalog, the internal surface map, the API-parity change-list
  regenerations, the integrations diagram), the einsum [Misaligned] decision record, coverage-tool fixes, the version
  bump.
- Judgement calls, by the rules: 2c3fd8a (the ONNX Runtime Run tier's -30% overhead) and 9641366 (Mono's
  Math.Truncate cast, 4.5-5.7x on an Android phone) carry no NPY/NS ratio, so they are not Performance (§8): the
  first is a clause of the ONNX Runtime fix line, the second its own Parity & Fixes line (a consumer-found slowdown,
  fixed with NumPy's bytes proven over all 2^32 float32 inputs). 33fffa1 (np.prod) is a parity-for-speed trade: a
  Parity line and a Breaking line, no Performance line. 103fd0d's partition timings are a fix's side effect, not a
  perf commit.
- findings/bigtree-consumer needs no scope decision: its one commit is in the range as the cherry-pick 7f1c8fd.

THE FIGURES (every one read on this branch's code, 2026-11-13)
- Coverage generator (python coverage/generate_coverage.py, generator 1.17.0): headline 539 / 560 (96.2%; 0.80.0's
  published artifact inventory/2026-11-08_d523eb48: 533 / 560, 95.2%); NumPy-identical signatures 368 / 560 (0.80.0:
  244); oracle-verified 540 / 560; all NumPy APIs 2,742 / 3,902 (70.3%; 0.80.0's generator: 2,017 / 4,844, 41.6%);
  921,524 oracle contracts. Its gates: coverage's Python tests 100 / 100, the dashboard's node tests 28 / 28.
- Tests & oracle inventory: 21,097 unit tests (0.80.0: 20,506), 1,122,288 oracle test cases (1,119,305) over 1,455
  operations (1,448).
- Benchmark coverage audit: 441 / 521 (0.80.0's published benchmark-coverage/2026-11-08_d523eb48: 452 / 518). The
  diff: the 11 names 0.80.0 credited through the function they alias (covered_alias: acos, asin, atan, atan2, pow,
  bitwise_invert, bitwise_left_shift, bitwise_right_shift, cumulative_sum, cumulative_prod, row_stack) are functions
  of their own now with no benchmark, and fromiter / fromfunction / ndim are new rows without one.
- New np.* functions: 17, each absent from v0.80.0's np facade and declared at HEAD (round, acos, asin, atan, atan2,
  pow, bitwise_invert, bitwise_left_shift, bitwise_right_shift, cumulative_sum, cumulative_prod, ndim, shape, astype,
  fromiter, fromfunction, row_stack), plus the constant little_endian.
- The top 10 by GitHub use (legacy code search, `"np.<name>" extension:py size:<40000 NOT site-packages
  in:file,path`, keyring gh login, 6.5 s apart; then page 1 sampled for the 13 contenders and page 10 for np.pow and
  np.fromiter, a hit literal only when a text-match fragment holds np.<name> followed by a non-word character):
  np.round ~764,000 (99 % literal), np.shape ~397,000, np.ndim 60,672, np.fromiter ~43,600, np.astype 32,896,
  np.row_stack 11,856, np.pow ~9,300 (raw 308,736 is np.power's prefix match: 15 % literal on page 1, 3 % on page 10,
  the page-10 share used as for 0.80.0), np.fromfunction 6,752, np.atan ~2,840, np.atan2 ~2,000; then np.acos ~950,
  np.asin ~600, np.cumulative_sum 535, np.cumulative_prod 367 and the three bitwise names under 110.
- Exceptions: 47 new public exception classes (a name diff of the Exceptions classes between v0.80.0 and HEAD: the
  44 of 30b29b4 plus NotImplementedError, ImportError and ModuleNotFoundError of b4269b9); AxisOutOfRangeException
  is gone.
- The parity-QA count: 9 of the 62 findings fixed (the 9 ParityQb pins that pass, the previous commit untagging the
  ninth, QB-59).
- The tallies, counted from the sections by the house rules (Parity & Fixes: single lines plus umbrella sub-bullets;
  Performance and Breaking: top-level lines): 26 fixes, 2 speedups, 17 breaking changes; the commit count is
  `git rev-list --count v0.80.0..HEAD` + 1 (this commit), computed in the commit command.

WHAT THE NOTES SAY (docs/releases/RELEASE_0.81.0.md, the 0.80.0 layout: intro, Overview, a visible one-liner per new
package, then the Detailed Breakdown inside <details> with the package's §7A elaboration first)
- Intro: a three-line draft in the owner's voice (coverage, what the release brings, TorchSharp), to be confirmed.
- New NuGet package: NumSharp.Interop.TorchSharp - modes, export, import, dtypes, checks, the state-matrix proof and
  its TorchSharp floor, every verb spelled as the code declares it (AsTensor, ToTensor, AsNDArray, ToNDArray,
  MayShareMemory, ToScalarType, ToDType, LiveExports / LiveImports, TorchCodecMode Auto / View / Copy).
- New APIs: NumPy's scalars; Python's and NumPy's exceptions; the 17 functions and the three np.lib modules; NumPy's
  one signature (NDAxis, None, creation, keywords, result structs, numpy.ma, finfo); unsealed State / poly1d.
- Performance: the edge selection (x4.0->x6.7, NumPy's best times; argpartition was x0.44) and Postprocess.TopK
  (x1.6->x2.0, was x0.12).
- Parity & Fixes, Testing & Tooling, Breaking Changes as tallied above.

Verified: every cited hash resolves and lies in v0.80.0..HEAD; no em or en dash; 28.6K characters, far under the
release body's 122,500 budget.
…shed notes do (CHANGELOG_STYLE §6) - every trailing citation cut, the commit count restated; 749ecee stays in history as the cited ledger

WHY
CHANGELOG_STYLE §6 (the owner, 2026-11-08, for 0.80.0: "Remove all commit hashes from our new release notes"): cite
while drafting and sweeping, strip every citation before the notes become the release body and the PR post, keep the
last cited revision in git history for the next sweep's bucket 1 (Cited). 749ecee is that revision for 0.81.0.

WHAT (docs/releases/RELEASE_0.81.0.md)
- Every hash was a trailing citation, ` - `h1`, `h2` (+ `h3`)` before a line's final period; each is cut and the
  period kept, no other word changed (the strip script is in the session scratchpad: strip_hashes.py).
- The Overview's tally line keeps its wording and states the release's commit count as `git rev-list --count
  v0.80.0..HEAD` + 1, computed in the same shell command as this commit, so it includes this commit.

Verified by the script before it wrote: no hash left, the line count unchanged, no changed line ending in a dangling
separator or a doubled period, exactly one tally line; no em or en dash in the file.
…mes the oracle pin on journey5 (issue #634) - its 14 commits (the one-file pin, OpenBLAS 0.3.34.106.0, NumPy 2.5's linalg / random / ma / fft / strings / sort / reductions / dtypes / core changes, the regenerated corpora, the coverage generator and the docs) merged over the 200 journey5 commits since its base 882a66a, the 36 conflicts resolved by keeping journey5's API and adding 2.5's behaviour; the corpora are regenerated under 2.5.4 with journey5's generators in the next commit

WHY
The owner, 2026-11-13, on the 0.81.0 scope page: "Merge 2.5.4 first" - "merge to our branch journey that will ship
0.81.0". journey5-2.5.4 (2026-11-09, local only) forked at 882a66a; journey5 has since landed the API-parity rounds
1-13, NumPy's scalars, the Python / NumPy exception classes, QB43's introselect, the sequential np.prod, TorchSharp
and the coverage generator 1.17.0. A trial merge reported 36 conflicting files.

HOW EACH CONFLICT WAS DECIDED (journey5 = ours, the 2.5.4 branch = theirs)
- Signatures are journey5's (the API-parity rounds' one NumPy signature), behaviour is NumPy 2.5.4's:
  * np.sort / np.argsort / ndarray.sort: journey5's MemoryError cref with 2.5's keyword-only descending=.
  * numpy.ma (NDMaskedArray.cs, 14 blocks): MaskedArray.sort / argsort and ma.sort / ma.argsort keep journey5's
    (axis, kind, order, endwith, fill_value, stable) and gain descending=, matching NumPy 2.5.4's signatures
    (MaskedArray.sort(axis=-1, kind=None, order=None, endwith=True, fill_value=None, *, stable=False,
    descending=False)); a masked operand refuses stable / descending (2.5.4's RefuseSortKeywords) - since NumPy 2.5
    ma.argsort passes them on to the method, where 2.4 passed stable=None - and a plain operand hands both to
    np.sort / np.argsort with kind / order. astype keeps journey5's order / casting / copy and its casting check
    before the allocation, with 2.5's caught fill conversion (CheckFillValueOrDefault); masked_object passes shrink
    to mask_or by name (journey5's copy slot); ma.dot keeps journey5's out with 2.5's product refusal text.
  * np.finfo (rebuilt from journey5's file, since the textual merge interleaved both versions): NumPy 2.5's
    TypeError "dtype must not be None" (the DType / NDScalarType / string forms; a null string was float64's limits)
    and ValueError "data type dtype('int32') not compatible with finfo" (2.5.4's NotCompatible, naming the native
    descriptor; journey5's batch 7 had matched 2.4's "data type <class 'numpy.int32'> not inexact"). Probed on 2.5.4:
    "foo" / "" / np.str_ name dtype('<U'), an array dtype('O'), np.floating dtype('O').
  * The statistics facades (np.median / percentile / quantile): journey5's batch-4 NDAxis / q-array signatures; the
    only 2.5.4 change there was a doc entry, the reshape ValueError 2.5's _ureduce no longer raises (removed).
  * Linalg: 2.5.4's cond 'nuc' rename (factorX / factorInv) with journey5's PyArray_Return and astype(copy:); the
    lstsq all_finite remark appended to journey5's text; vdot's text operands go through 2.5's common descriptor
    (VdotTextAnswer) and keep journey5's PyArray_Return comment.
  * Strings: 2.5.4's texts (UTF-8 TypeError for S -> T, the dropped empty-text hang remark: 2.5 returns '') with
    journey5's Python classes (OverflowError, MemoryError); the two out-region view helpers go with the callers
    2.5.4's accumulate rewrite removed (out must have the input's dimensions since gh-32709), as does NDListArray's
    written-region view (out.view()).
  * Random: 2.5's spawn ValueError "n_children must be non-negative" with journey5's OverflowError for the uint32
    range; SeedSequence's nested-sequence text with journey5's ArrayMemoryError.
  * np.cumsum / np.cumprod docs: journey5's dtype name with 2.5's misfit-out text.
- Tests: both new OpenBugs pins kept (journey5's F-order memmap, 2.5.4's text -> longdouble; the latter duplicates
  journey5's LongDouble_Text_IsParsedByStrtold_NotByFloat from the scalar class calls - both stay until fixed);
  OpenBugs.Surface SU11, BitGeneratorFamily and StringDTypeComputeTests take 2.5's answers; the coverage-strength
  ledger comment both.
- Coverage: the pin read from numpy-version.txt with generator version 1.18.0 (the merged generator holds journey5's
  1.17.0 counting rules and 2.5.4's catalog changes); journey5's oracle note with the pin; 2.5.4's ufunc-family
  keying (the op before the first underscore: sources may hold underscores); oracle_map.json keeps journey5's alias
  entries (the Array-API names, numpy.char's re-exports) without numpy.row_stack, and overrides.json's aliases are
  empty - journey5 has every name they covered under NumPy's own spelling, and NumPy 2.5 removed row_stack.
- test/oracle/gen_surface_oracle.py: journey5's dropped carves (SU11, SU12, SU14, SU16, SU36, SU37) - its batch 7
  fixed minexp, which 2.5.4's branch still carved as SU12.
- .claude/rules/oracle.md and getting-and-setting-values.md: 2.5.4's pin wording with journey5's content (the
  cast_boundary mode; NumSharp's scalars, which exist since S1).
- The five conflicting corpora take journey5's files as placeholders: the next commit regenerates every corpus.

FOUND BEYOND THE CONFLICT MARKERS (both lines built, the unit suite named them)
- NDMaskedArray.CheckFillValueOrDefault caught OverflowException, NumPy's OverflowError being a .NET
  OverflowException when 2.5.4's branch was written; journey5 raises the Python class (a sibling under
  ArithmeticException), so the catch names OverflowError and FloatingPointError too.
- Generator.choice(size=None): 2.5.4 unpacked the draw with NDArray.Scalar, which since journey5's scalar seams is
  the READ-ONLY scalar; the random module's size=None draws stay writeable 0-d arrays (NDArray.WritableScalar) until
  the scalar plan's S8 (RandomResultOwnershipTests).
- 2.5.4's tests expected .NET's OverflowException / OutOfMemoryException (np.where's weak literal, StringDType zfill):
  now OverflowError / MemoryError. journey5's tests of 2.4's finfo texts (np.finfo.BattleTest, API-parity batch 7)
  and of ma.argsort ignoring stable=True (batch 9) now assert NumPy 2.5.4's answers (probed).

VERIFIED (land/numpy-2.5.4, Release, net10.0)
- NumSharp.Core and NumSharp.Tests build with 0 errors; the unit suite (TestCategory!=OpenBugs&TestCategory!=HighMemory)
  ran 24,114 tests: the 7 failures above, each fixed and re-run green (13 / 13 with the string-pin class). The full
  suites on both frameworks, the oracle gates and the coverage generator run after the corpus regeneration.
…5's generators - 107 generator jobs, 11 corpora changed - plus the TorchSharp bridge's state fixture, which the 2.5.4 branch never saw (identical states, now stamped numpy 2.5.4 / torch 2.13.0+cpu, its gate reading numpy-version.txt instead of a "2.4.2" literal), and the matrix-operand argsort entry taking NumPy 2.5's stable= / descending= with axis=None (8 re-emitted cases threw)

WHY
The merge before this commit (ccf8a7d) put NumPy 2.5.4's pin on journey5. The 2.5.4 branch regenerated its corpora on
2026-11-09 with the generators of its base 882a66a; journey5 has since added cases (the API-parity rounds' tuple axes,
the random API's Shape? signatures, the surface tier's new names and dropped carves, cast_boundary, the strings 'c'
rows) whose answers came from NumPy 2.4.2, and a whole fixture (the TorchSharp state matrix, recorded with NumPy 2.4.2
and asserted against that literal). The corpora are the gate, so every one is regenerated under the pin with the merged
generators (.claude/skills/oracle/references/regenerate.md, "Bumping the pinned NumPy", step 3).

HOW
The 2.5.4 session's parallel driver (regen_all.py, scratchpad), pointed at this worktree, with gen_oracle.py's one new
mode (cast_boundary) added: phase A = every generator whose output derives from no other corpus (12 at a time), phase
B = gen_matrix_operand_oracle.py and gen_relayout_oracle.py, which re-emit the other tiers' cases. NumPy 2.5.4 from
C:\Users\ELI\venvs\numpy-2.5.4 (Windows) and WSL's ~/np254 (the LP64 tiers, and gen_random_oracle.py's LP64 sub-run);
refs/numpy at v2.5.4 for gen_dtype_oracle.py. Two reruns: the WSL jobs (Git Bash had turned the /mnt/k source path into
a Windows one; rerun with MSYS_NO_PATHCONV) and gen_strings_oracle.py (NumPy 2.5.4's intermittent access violation in
the tma section, as .claude/rules/oracle.md records; the rerun completed: 281,526 cases = the branch's 281,496 + the 30
'c' rows journey5 added). A sweep of test/oracle for generators the driver did not run found one corpus left behind,
gen_torch_interop_states.py (below); the others are deliberate: gen_find_common_type_oracle.py stays NumPy 1.26.4's,
gen_onnx_models.py / gen_parquet_*_fixture.py write models and fixtures, random_api_oracle_map.py is a module,
soak_strings_oracle.py feeds the nightly soak.

WHAT CHANGED (every other corpus came out byte-identical to the merge's: NumPy 2.4.2 and 2.5.4 agree on journey5's
added cases there, and the branch's 2.5.4 corpora reproduce exactly)
- Fuzz/corpus/axis_args.jsonl: 30 tuple-axis cells (API-parity batches 5-6) re-answered by 2.5.4.
- Fuzz/corpus/surface.jsonl: 67 cases, most of them np.finfo's (finfo.dtype 27; eps / epsneg / max / ...: the refusal
  of a non-inexact dtype is 2.5's "data type dtype('int32') not compatible with finfo") and linalg.cross (3).
- Fuzz/corpus/random_api_host.jsonl (8) and random_api_mvn.jsonl (4): the corrected Generator BTPE binomial and
  multivariate_normal's 2.5 rules over journey5's Shape? signatures.
- Fuzz/corpus/linalg_parity.jsonl: 366 -> 370 cases (283 lines re-answered: qr, svd, norm, inv, det - NumPy 2.5's
  all_finite / cond rules over the current registry).
- Fuzz/corpus/matrix_operand.jsonl (3,860 -> 3,876), matrix_operand_host.jsonl, gaps/matrix_operand_gaps.jsonl,
  relayout.jsonl (13,862 -> 13,860), relayout_host.jsonl: re-emitted from the regenerated tiers. matrix_operand now
  re-emits the 2.5.4 branch's gen_sort_descending cases with matrix operands, among them 8 argsort cases with
  params {"axis": null, "descending": true[, "stable": true]} (NumPy answers a (1, 100) matrix).
- Strings/corpus/strings_tsort.jsonl: 6 NaN-NA unique(sorted=False) cases whose hash order NumPy varies run to run
  (the replay's order excuse, as the 2.5.4 branch's oracle commit noted).
- Unchanged by regeneration although conflicting in the merge: gaps/surface_gaps.jsonl (journey5's file is 2.5.4's).

THE MATRIX-OPERAND ARGSORT ENTRY (test/NumSharp.Tests.Oracle/Fuzz/OpRegistry.MatrixOperand.cs)
TryApplyFunctionForm's "argsort" read p["axis"].GetInt32(), written before NumPy 2.5's keywords existed: the 8 cases
above threw InvalidOperationException ("requires an element of type 'Number', but the target element has type 'Null'")
and MatrixOperand failed 8/3876. The entry now does what the base entry (OpRegistry.cs) does: a case carrying stable= or
descending= calls np.argsort(a, ParseAxis(p), stable:, descending:) - JSON null is NumPy's None, the flattening axis -
and every other case keeps its call. NumSharp's answer then matches NumPy's (1, 100) matrix in all 8. The <param>
doc of the function form names what each key reads.

THE TORCHSHARP STATE FIXTURE (test/NumSharp.Tests.Interop.TorchSharp)
- gen_torch_interop_states.py (added on journey5 after the 2.5.4 branch forked, so its regeneration never ran it)
  imports numpy_pin and refuses another NumPy, as every generator does since the pin commit; its docstring says to run
  it WITHOUT -I under a Python holding the pinned numpy and torch (the interop ecosystem venv make_env.py builds).
- Fixtures/torch_interop_states.json regenerated with C:\Users\ELI\venvs\numsharp-interop\ecosystem-py312 (numpy 2.5.4,
  torch 2.13.0+cpu): 386 array cases, 205 tensor cases, and every one of the 45 array states and 24 tensor states is
  IDENTICAL to the NumPy 2.4.2 / torch 2.12.1+cu126 recording - only the two version stamps changed.
- NumSharp.Tests.Interop.TorchSharp.csproj compiles numpy-version.txt in as the "NumPyVersion" assembly metadata with
  the same CheckNumPyPin build error as NumSharp.Tests.csproj, and links NumSharp.Tests' Utilities/NumPyPin.cs.
  StateCatalogSyncTests.Fixture_IsNumPy242 (StateOracle.NumPyVersion.Should().Be("2.4.2")) becomes
  Fixture_IsThePinnedNumPy (.Should().Be(NumPyPin.Version)): after a NumPy bump it fails until the fixture is
  regenerated, where the literal kept agreeing with the old fixture. StateOracle / StateMatrixExportTests / the csproj
  comment say "the pinned NumPy" instead of 2.4.2.
- NumPyPin.cs: its missing-metadata text names the assembly it is compiled into (a linking project must be told which
  csproj to fix); System.Reflection.Assembly is written in full because NumSharp.Tests has a NumSharp.Tests.Assembly
  namespace (the first build failed with CS0118). Its remarks name the linking project.
- .claude/skills/oracle/references/regenerate.md, step 3 of the bump: the torch fixture and its venv, and why
  gen_find_common_type_oracle.py is not part of a bump.

VERIFIED (land/numpy-2.5.4, Release)
- Oracle project, CI filter (TestCategory!=OpenBugs&TestCategory!=HighMemory): net10.0 388 passed / 1 failed
  (MatrixOperand, the 8 cases above) / 35 skipped before the entry fix; after it MatrixOperand, MatrixOperandHost and
  MatrixOperand_LibraryNeverReachesAMatrixRule pass on net10.0, and the whole project on net8.0: 389 passed, 0 failed,
  35 skipped. The *_KnownGaps tests ([OpenBugs]) still report their carved cases diverging (MO01-MO05), as they must.
- Unit suite, CI filter: net10.0 23,829 passed / 0 failed / 285 skipped (24,114); net8.0 23,828 / 0 / 285 (24,113).
- NumSharp.Tests' pin gates (Corpus_IsLoadedAndNonVacuous, Corpus_IsPinnedNumPy_OfThisHostsModel_AndNotVacuous) pass
  after the NumPyPin.cs edit.
- NumSharp.Tests.Interop.TorchSharp (TorchSharp 0.100.0, libtorch-cpu 2.0.1.1): 219 passed, 3 skipped, 0 failed on
  net10.0 and net8.0. Negative control: the NumPy 2.4.2 fixture copied over the build output fails
  Fixture_IsThePinnedNumPy ("they differ at index 2"); the 2.5.4 fixture passes it.
…ved it - the owner's answer when NumPy 2.5.4 became the pin ("Keep, kept on purpose", with "no obsoletion"): its three forms are np.vstack under NumPy 2.4's other name, with no warning, no [Obsolete] and no "prefer vstack", like the NumPy 1.x names NumPy 2.0 removed

WHY
NumPy 2.5 removed np.row_stack (probed on 2.5.4: AttributeError: module 'numpy' has no attribute 'row_stack'; on 2.4.2
it is vstack behind a DeprecationWarning; np.ma.row_stack stays in both). NumSharp added it in API-parity batch 12
(unreleased), with the remarks "DEPRECATED in NumPy 2.0: prefer vstack". On the 0.81.0 decision page (2026-11-13) the
owner chose to keep it, as 9e75f36 keeps np.maximum_sctype, np.issctype, np.float_ and the other NumPy 1.x names NumPy
2.0 removed ("keep them, document that they are okay"), and added "no obsoletion": NumSharp does not mark it obsolete or
steer callers away. (np.maximum_sctype was on the first draft of that page by mistake: NumPy removed it in 2.0, so it
already fell under 9e75f36's rule.)

WHAT
- src/NumSharp.Core/Creation/np.row_stack.cs: the header and the three forms' summaries say "NumPy 2.4's row_stack ...,
  np.vstack under its other name"; the remarks say NumPy 2.5 removed it (2.5.4's AttributeError text) and that NumSharp
  keeps it on purpose, with no warning and no [Obsolete], so code written against NumPy 2.4 or earlier compiles and runs
  unchanged. The documentation link moves to numpy.org/doc/1.26/... (the stable link answered 404: NumPy 2.x's
  documentation has no page for row_stack, probed for 2.0-2.4 and stable).
- src/NumSharp.Core/Char/NDListArray.Manipulation.cs (NDListArray.VStack's summary),
  test/NumSharp.Tests.Oracle/Fuzz/OracleSurfaceCoverageTests.cs (the alias entry's comment: vstack's cases stay its
  evidence) and .claude/rules/creation-manipulation.md (the row_stack bullet, with the owner's ruling and the 1.x
  precedent) say the same.
- No code changes: the functions, their tests and coverage stay as they are (NumPy 2.5.4's catalog has no row_stack row,
  and the merge already dropped numpy.row_stack from coverage/oracle_map.json).

VERIFIED
NumSharp.Core builds with the edited docs (the unit-suite build of this tree), and NumSharp.Tests.Oracle builds.
…the NumSharp-only row numsharp.np.row_stack - NumPy 2.5.4's catalog has no numpy.row_stack, so the C# gate's EquivalentAliases["row_stack"] = "vstack" named no reviewed alias and coverage/test_oracle_evidence.py failed under the 2.5.4 pin

WHY
The 0.81.0 inventory, run on the release branch under NumPy 2.5.4 (coverage/generate_coverage.py, then the coverage unit
tests), failed one test: CommittedCorpusTests.test_reviewed_aliases_mirror_the_csharp_oracle_gate, "'numsharp.np.row_stack'
not found ... EquivalentAliases[row_stack] names no catalog row". The test reads every alias of
OracleSurfaceCoverageTests.cs (the C# surface guard) and requires a reviewed alias in oracle_map.json for its row: the
numpy.<name> row when NumPy's catalog has one, else the NumSharp-only numsharp.np.<name> row. Until the pin moved,
row_stack was numpy.row_stack with the alias {"via": "numpy.vstack"}; the 2.5.4 merge (ccf8a7d) dropped that entry with
NumPy 2.5's removal of the function, but the C# alias stayed - and stays, since the owner keeps np.row_stack on purpose
(46e4c53) - so the row is now numsharp.np.row_stack (the generator already emits it: coverage.json lists it beside
numsharp.np.maximum_sctype) and needs its own reviewed alias.

WHAT
coverage/oracle_map.json: "numsharp.np.row_stack": {"via": "numpy.vstack", ...} beside numsharp.np.common_type_code,
the reason naming NumPy 2.4's alias, 2.5's removal and the owner's ruling.

VERIFIED
python -m unittest discover -s coverage -p test_oracle_evidence.py under NumPy 2.5.4: 20 tests OK (the failing test
passes). The full coverage suite, the dashboard node tests and the inventory re-run on the release branch after this
commit is merged there.
…agree again, and the present-tense "NumPy 2.4.2" claims journey5 added after the 2.5.4 branch forked name the pin - the detail pane's pin-agnostic "committed NumPy oracle contracts" is what coverage/test_dashboard.cjs now expects (1 of 28 failed), the page's oracle explainer says "the pinned NumPy release", and the TorchSharp page and interop rule name the regenerated fixture's NumPy 2.5.4 / PyTorch 2.13.0

WHY
The 0.81.0 inventory on the release branch (coverage generator, then the coverage unit tests, then
node --test coverage/test_dashboard.cjs) failed one dashboard test: "detail pane renders contracts, host pins, alias via
and the two gap readings" expected "<strong>1,200</strong> committed NumPy 2.4.2 contracts", but the page rendered
"committed NumPy oracle contracts replayed against NumSharp in CI". The 2.5.4 branch made that line of
docs/website-src/docs/coverage-support-dashboard.md pin-agnostic (de1acf1) without touching the node test (its
verification ran the Python coverage tests only), and the merge (ccf8a7d) took the page's line from the 2.5.4 branch and
the test from journey5, which had both at 2.4.2. A sweep of every "2.4.2" journey5 added after the fork point 882a66a in
docs/website-src, .claude, README.md, ARCHITECTURE.md, CONTRIBUTING.md and the Fuzz ledger then found three more
present-tense claims the 2.5.4 docs commit could not have seen.

WHAT
- coverage/test_dashboard.cjs: the sqrt detail-pane assertion expects the page's text ("committed NumPy oracle contracts
  replayed against NumSharp in CI"), with a comment on why the pane names no release (the page's NumPy pill does). The
  fixtures' numpy_version '2.4.2' values stay: they are test data.
- docs/website-src/docs/coverage-support-dashboard.md: the oracle explainer's "committed NumPy 2.4.2 output" (journey5's
  string-oracle wording of the paragraph) is "committed output of the pinned NumPy release (named at the top of the
  page)".
- docs/website-src/docs/interop/torchsharp.md: the layout and flag tables are held to NumPy 2.5.4 (the pinned release)
  and PyTorch 2.13.0, the releases d8de56f regenerated the fixture with (every state identical).
- .claude/rules/interop-packages.md: gen_torch_interop_states.py runs without -I under the interop ecosystem venv
  (numpy 2.5.4, torch 2.13.0+cpu; ~/.claude/python's NumPy 2.4.2 now fails its pin check), and
  StateCatalogSyncTests.Fixture_IsThePinnedNumPy holds the fixture to numpy-version.txt.

VERIFIED
node --test coverage/test_dashboard.cjs: 28 tests, 28 pass. The release branch's full inventory re-runs after this commit
is merged there.
…econd decision round - intro A, the Overview the owner picked, the 14 NumPy 2.5.4 commits swept in (a 9-line NumPy 2.5 Parity umbrella, the OpenBLAS bundle, descending=, three Breaking lines, the pin and regeneration Tooling line) and every figure re-read under NumPy 2.5.4 - this is the CITED revision (the sweep's ledger); the next commit strips the hashes

WHY
The owner's answers on the 0.81.0 decision pages (2026-11-13): "Merge 2.5.4 first" ("merge to our branch journey that will
ship 0.81.0"), landed as ccf8a7d / d8de56f / 46e4c53 / 9bf2d80 / 95e2a7a on journey5 and merged here; the intro
re-asked with no coverage row; np.row_stack kept ("no obsoletion"); and "ask about the overview for different things to
put there", answered on a third page. The notes must describe the code that ships and the NumPy it is measured against.

THE SWEEP (CHANGELOG_STYLE §1/§10, the 5-bucket triage over the 14 commits of journey5-2.5.4 plus the landing commits)
- Cited (new lines): a58584e (the one-file pin) and 5215aba (the regenerated corpora) and 69673bc (the coverage
  generator) -> Testing & Tooling, with d8de56f (+ 9bf2d80, 95e2a7a); 3e73c62 linalg, ea39e34 random, 696c938 ma,
  a6d12ea fft, 72da011 strings, dc529b7 sort, 0f2578f reductions, e85c7e8 dtypes, 7d245c0 core -> the "NumPy 2.5's
  behavior" Parity umbrella (9 sub-lines), and the breaking halves of 3e73c62 / ea39e34 / dc529b7 / 0f2578f /
  e85c7e8 / 72da011 -> three Breaking lines (NumPy 2.5's answers, refusals, text rules); dc529b7 + 696c938 ->
  the descending= line in New APIs; d691e24 -> a NumSharp.Interop.OpenBLAS Parity line (a released package, so not
  §7A); ccf8a7d cited where its resolutions carry behavior (numpy.ma's descending refusal, finfo's 2.5 texts).
- Excluded: de1acf1 (docs only), the merge commits.
- Judged against 0.80.0 (§2): StringDType and NPY_SORTKIND shipped in 0.80.0; NPY_SORTKIND is internal, so its flag-set
  change is no Breaking line; StringDType's byteswap / NUL / view rules are. np.where's literal overflow is a Parity fix
  (it wrapped), as 0.81.0's np.asanyarray(300, np.int8) line already is.

THE OWNER'S DECISIONS APPLIED
- Intro (draft A, the TorchSharp row shortened by the owner): "This release moves NumSharp to NumPy 2.5.4, gives its
  functions NumPy's own signatures and brings NumPy's scalars, every Python and NumPy exception class, and 17 new np.*
  functions." / "An NDArray now shares its memory with TorchSharp tensors, in both directions."
- Overview, kept: the coverage line with ONLY its scalars sub-line (the owner: "only scalars"), the NumPy 2.5.4 line,
  the layout line, and the closing tallies / package / oracle lines; added: "NumSharp on phones" and "New
  documentation"; dropped: the one-signature line and the parity-QA line (both stay in the detailed sections). The
  dropped +17-functions sub-line's GitHub top 10 moves onto New APIs' "New functions and modules" umbrella, as the
  decision page promised ("a dropped group's content stays in the detailed sections"); its np.row_stack carries
  "(kept, although NumPy 2.5 removed it)".

FIGURES (re-read on the release branch at 4c11efd under NumPy 2.5.4, run_inventory254.sh)
- coverage/generate_coverage.py: headline 538/559 (96.2%; NumPy 2.5.4's catalog has no row_stack), 368 NumPy-identical
  signatures, 539 oracle-verified, the whole API 2,747 of 3,908 (70.3%), generator 1.18.0; its 100 unit tests and the
  28 dashboard node tests pass (after 9bf2d80 and 95e2a7a, which the first run's two failures produced).
- test/inventory: 21,115 test declarations, 1,122,248 oracle test cases over 1,455 operations (journey5 had 1,122,288:
  the relayout pair is 692 smaller since the 2.5.4 branch's own regeneration, the sort / strings / matrix-operand tiers
  larger; every .jsonl delta accounted for).
- benchmark audit: 441 of 520 benchmarkable APIs (np.row_stack left NumPy's catalog, so the line's no-benchmark list
  drops it).
- Tallies from the file: Parity & Fixes 36 (single lines + umbrella sub-lines), Performance 2, Breaking 20 (top-level
  lines); the commit count is computed in the commit command (git rev-list --count v0.80.0..HEAD + 1).
- Exceptions: NumPy 2.5.4's exception classes equal 2.4.2's (numpy.exceptions, numpy._core._exceptions, linalg / ma /
  _iotools, both on CPython 3.12.12), so "every exception class of CPython 3.12 and NumPy 2.5.4" holds.
- TorchSharp §7A: the state matrix is held to NumPy 2.5.4's flags (d8de56f regenerated the fixture; every state equal).
… published notes do (CHANGELOG_STYLE §6) - every trailing citation of the NumPy 2.5.4 revision cut and the commit count restated; d25e81f stays in history as the cited ledger (it supersedes 749ecee)

WHY
The owner's rule for 0.80.0, kept for 0.81.0: "Remove all commit hashes from our new release notes". The previous commit
(d25e81f) is the cited revision a later sweep reads its bucket 1 from; this one is what build-and-release.yml's
create-release job publishes.

HOW
strip_hashes.py (scratchpad, the same script that made 1c79f14 from 749ecee): cuts each " - `h1`, `h2` (+ `h3`)"
trailing citation and every lone "(+ `h3`)", restates the Overview tally's commit count (git rev-list --count
v0.80.0..HEAD + 1, computed in the commit command so it counts this commit), and asserts no hash left, the line count
unchanged, no dangling " -" separator or doubled period, exactly one tally line. ASCII hyphens only (no U+2013 / U+2014),
checked on the cited revision.
@Nucs Nucs added this to the NumPy 2.x Compliance milestone Nov 13, 2026
@Nucs Nucs added architecture Cross-cutting structural changes affecting multiple components core Internal engine: Shape, Storage, TensorEngine, iterators api Public API surface (np.*, NDArray methods, operators) release A newly released version labels Nov 13, 2026
Nucs added 5 commits November 13, 2026 22:34
…'s OverflowError class - NumSharp raises the Python class since journey5's exception work (OverflowError is a sibling of .NET's OverflowException under ArithmeticException), so NullFill_EveryFillKind_ConvertsAsNumpysSetitem failed on every OS in PR #635's CI; the bridge's and Core's docs that still named OverflowException for that refusal say OverflowError

WHY
PR #635 (release-0.81.0 -> master), Build and Release run 43498104854: interop-test failed "ParquetNet: Test floor"
(net8.0 and net10.0) on ubuntu, windows and macos, one test each time:
  NullFill_EveryFillKind_ConvertsAsNumpysSetitem: "Expected refusal to be assignable to System.OverflowException
  because fill -1 (Int64) into maybe_u8, but NumSharp.OverflowError is not."
The fill converts through Core's PyArray_Pack port (ReductionInitial.PackStore), which raises NumSharp's OverflowError
since the Python / NumPy exception classes landed on journey5 (30b29b4 / ef236b5; 0.81.0's Breaking line "NumPy's
errors are Python's classes: catch (OverflowException) ... no longer see them"). The interop suites run only in CI's
interop-test job, and journey5 had no CI run since then, so the test kept the old class. The bridge is unreleased
(ParquetNet, IsPackable=false); the gate still runs it.

WHAT
- test/NumSharp.Tests.Interop.ParquetNet/NumericColumnParityTests.cs: the three uint8 out-of-range fills (-1, 300, -1.5)
  expect typeof(OverflowError) with NumPy's texts unchanged; FillCase's Refusal doc names the class and why a
  catch (OverflowException) does not see it. The decimal-overflow assertions (Throw<OverflowException>, the whole-file
  read) stay: that path is .NET's own decimal conversion, and it passed in the same run.
- src/NumSharp.Interop.ParquetNet/ParquetConvert.cs: the five <exception> crefs of the fill paths name OverflowError
  (ResolveFill -> PackStore raises it for an integer out of range and for an infinity into an integer).
- src/NumSharp.Interop.ParquetNet/ParquetLoadOptions.cs: NullFill's remark says ValueError / OverflowError.
- src/NumSharp.Core/Backends/Default/Math/Reduction/ReductionInitial.cs: the class remark's "house map" said NumPy's
  OverflowError IS OverflowException, stale since the exception work; it now names OverflowError and the catch footgun.

VERIFIED
NumSharp.Tests.Interop.ParquetNet (Release, the CI filter TestCategory!=OpenBugs): 39 passed, 0 failed on net8.0 and
net10.0.
…hrough HostLibm - macOS arm64's libm rounds acos(0.5) to 1.0471975511965976, one ULP under the UCRT's and glibc's correctly rounded 1.0471975511965979, so InverseTrig_Aliases_AreTheOriginals failed on PR #635's macOS leg (net8.0 and net10.0) while the alias identities it exists for held

WHY
PR #635 (release-0.81.0 -> master), Build and Release run 43498104854, test (macos-latest): the only failure of the unit
suite on both frameworks (23,575 / 23,576 passed) was
  InverseTrig_Aliases_AreTheOriginals: "Expected np.acos(np.array(new[] { 0.5 })).GetDouble(0) to be 1.0471975511965979,
  but found 1.0471975511965976".
The test (API-parity batch 1, a8bedd2) pinned NumPy's win-amd64 values with Should().Be, but acos / asin / atan / atan2
pass through the platform libm: bit-exact only where NumPy's wheel and .NET call the same CRT. The suite already has the
rule for exactly this (test/NumSharp.Tests/Utilities/HostLibm.cs, whose remarks name "macOS acos(0.5)" as a measured
artifact): exact bits on the pinned host (Windows), at most OffHostUlpBudget (4) ULP elsewhere, NaN / infinity / sign
always exact. journey5 had no macOS run since batch 1, so the pin never met Apple's libm.

WHAT
test/NumSharp.Tests/Math/np.ArrayApiAliases.Test.cs:
- InverseTrig_Aliases_AreTheOriginals: the alias property (np.acos / asin / atan bit for bit as arccos / arcsin /
  arctan over the specials) stays exact on every host; the three point values go through HostLibm.Matches with
  HostLibm.Describe messages, and a <remarks> records the macOS value and where it was seen.
- Atan2_Alias_IsArctan2: its atan2(1, -1) point value takes the same comparison (it agrees on all three hosts today;
  the same libm rule applies).

VERIFIED
NumSharp.Tests (Release): ArrayApiAliasesTests 5 / 5 on net8.0 and net10.0 (Windows, the pinned host: exact bits). The
macOS leg re-runs on PR #635.
…t - libtorch 2.10.0's osx-arm64 libtorch_cpu.dylib links OpenMP by Homebrew's absolute path, so every test of the current leg failed TorchSharp's own native load on macos-26-arm64 in PR #635; the TorchSharp page tells macOS users the same, and the workflow comment no longer calls the released bridge unreleased

WHY
PR #635, Build and Release run 43498104854, interop-test (macos-latest): "TorchSharp: Test current" (net8.0, net10.0)
failed in every test with
  System.NotSupportedException: This application or script uses TorchSharp but doesn't contain a reference to
  libtorch-cpu-osx-arm64, Version=2.10.0.0 ... Failed to load native component .../cpu/libLibTorchSharp.dylib
even after TorchSharp consolidated the libtorch-cpu-osx-arm64 natives (libomp.dylib included) beside it. Reading the
Mach-O load commands of the packages in the NuGet cache (no otool needed: a small LC_* parser) gave the cause:
- TorchSharp 0.107.0's libLibTorchSharp.dylib: minos 15.0, @rpath/libc10 / libtorch / libtorch_cpu, rpaths
  @loader_path and @executable_path - all present beside it after the consolidation;
- libtorch 2.10.0's libtorch_cpu.dylib: LC_LOAD_DYLIB /opt/homebrew/opt/libomp/lib/libomp.dylib (an ABSOLUTE Homebrew
  path), rpath /Users/runner/work/pytorch/... (the build machine's), and the package's own libomp.dylib carries that
  same absolute install name. dyld looks for the absolute path, so the shipped copy is never used, and on a runner
  without Homebrew's libomp (the macos-26-arm64 image has none) libtorch_cpu - and with it libLibTorchSharp - cannot
  load. The bridge never got to run: this is libtorch's packaging, and every macOS TorchSharp user meets it.
The leg was added on journey5 and never ran in CI before this PR; Windows and Linux pass both legs.

WHAT
- .github/workflows/build-and-release.yml, interop-test: a "TorchSharp: libomp (macOS)" step before the current leg's
  build, `brew list libomp || brew install libomp`, with the load commands that make it necessary in its comment; the
  section comment says the bridge is released from 0.81.0 (build-nuget packs it) instead of "UNRELEASED, so nothing
  packs it".
- docs/website-src/docs/interop/torchsharp.md, "TorchSharp versions": macOS users install OpenMP from Homebrew before
  the first tensor, and why the package's own libomp.dylib does not help.
- .claude/rules/interop-packages.md: the macOS trap, with the CI step and the failure it explains.

VERIFIED
The load commands above, read from C:\Users\ELI\.nuget\packages\{torchsharp\0.107.0,libtorch-cpu-osx-arm64\2.10.0}.
The macOS leg re-runs on PR #635 with the step.
….TorchSharp to install Homebrew's OpenMP first (`brew install libomp`), which libtorch links by Homebrew's path - found by PR #635's first macOS run; the commit count restated after journey5's three CI fixes merged in

WHY
PR #635's macOS interop leg showed that libtorch 2.10.0's osx-arm64 libtorch_cpu.dylib loads libomp from
/opt/homebrew/opt/libomp/lib/libomp.dylib (an absolute LC_LOAD_DYLIB), so TorchSharp itself cannot load on a Mac without
Homebrew's libomp (journey5 3c94845: the CI step, the TorchSharp page, the interop rule). The package is new in 0.81.0,
so its §7A "Depends" line is where a macOS user looks first. The other fixes merged with it (229ea98 ParquetNet's
OverflowError test, b518d3b the alias test's HostLibm comparison, 3c94845's CI step) are test / CI work with no
release-notes line (CHANGELOG_STYLE §11).

WHAT
docs/releases/RELEASE_0.81.0.md: the TorchSharp §7A "Depends" sub-line gains ", and on macOS install Homebrew's OpenMP
first (`brew install libomp`), which libtorch links by Homebrew's path."; the Overview's tally restates the commit count
(git rev-list --count v0.80.0..HEAD + 1, computed in the commit command). §7A carries no hashes, so the cited ledger
d25e81f needs no twin of this edit. The PR description is regenerated from this file (notes minus the H1).
@Nucs
Nucs merged commit 65ef305 into master Nov 13, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Public API surface (np.*, NDArray methods, operators) architecture Cross-cutting structural changes affecting multiple components core Internal engine: Shape, Storage, TensorEngine, iterators release A newly released version

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant