Skip to content

[0.80.0] Text dtypes, numpy.strings / numpy.char, numpy.polynomial, numpy.ma, np.emath, np.random parity, 48 new np.* functions, ONNX Runtime / ML.NET / System.Numerics.Tensors interop, NumPy-parity fixes - #633

Merged
Nucs merged 1571 commits into
masterfrom
release-0.80.0
Nov 8, 2026

Conversation

@Nucs

@Nucs Nucs commented Nov 7, 2026 •

Copy link
Copy Markdown
Member

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

🧭 Overview

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

📦 New NuGet Packages

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

Detailed Breakdown

Show All

📦 New NuGet Packages

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

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

📊 Dashboards & Docs

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

✨ New APIs & Modules

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

🧩 ndarray surface

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

⚡ Performance

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

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

🎯 Parity & Fixes

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

🧰 Testing & Tooling

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

💥 Breaking Changes

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

Nucs added 30 commits November 3, 2026 06:11
…ows - ONNX Runtime 1.16.0 / 1.31.0 / 1.31.1 and ML.NET 2.0.1 / 4.0.2 / 5.0.1, net8.0 and net10.0, all green

(cherry picked from commit a57a610122c9cab120fdc4928f7d0d01e3aab66c)
…trips but is a variable-length column to ML.NET's transforms and trainers (a vector size of 0 means variable length there); P13 becomes documentation, because the owner pins the zero-width round trip in BoundaryTests

(cherry picked from commit 7d68a49435818574cf3ed4527c10176d9048e1fd)
…P12 (qa/p12-feed-checks baafbaf1), P11 (qa/p11-disposed-guard 1ab5c9fc), P7 (qa/p7-runasync-symbols 5f6175ec), P1 (qa/p1-multi-array-view a6baabb2) - and qa/proposals-all (4851fd0f), which merges the four with their two doc-only conflicts resolved and is green on ORT 1.31.1 / 1.16.0 and ML.NET 5.0.1

- Section 5: each prepared recommendation gains a Branch line (what the commit does, what it leaves out, the
  test counts per leg); P2 points at P12's commit, which carries RequireTensorOutputs.
- Section 8: a table of the proposal branches (commit, base, files touched), where they conflict (XML docs of
  Run(dict) for P12 + P11, of RunAsync for P12 + P7; no code conflict), the order of the checks the merged code
  keeps in every Run form, the combined branch's results (ORT 1.31.1 net10.0 319 / 319; ORT 1.16.0 net8.0 298 +
  21 Inconclusive; ML.NET 5.0.1 net10.0 215 / 215), and how to take only some (cherry-pick in the order P12, P11,
  P7, P1).
- Section 7 (log): 23:57 to 00:29.

(cherry picked from commit 1404bd01c1c42f9a1483a1b98fe94f474a8fadc6)
…<string>("col") route with the column's own name (and GetColumn<string[]> for a vector-of-text column, where the cursor getter it used to name is refused by ML.NET), and the ML.NET page and README say how string labels read back after MapKeyToValue

Release QA finding F49 (docs/plans/interop-onnx-mlnet-release-qa.md; angle A31 - ML.NET text columns read back).

What the QA proved (scratch consumers/ml_text_labels.cs on Microsoft.ML 5.0.1, consumers/ml_text_routes.cs on the
2.0.1 floor, both from the packed 0.80.0-beta1 feed):
- ML.NET's first tutorial (Iris) has STRING classes. The page's "Key columns" paragraph told readers to append
  MapKeyToValue("PredictedLabel") "to read the label values back"; with string labels that makes PredictedLabel a
  TEXT column and ToNDArray refuses it - a reader following the page landed on the NotSupportedException.
- The refusal's route was a cursor loop over ValueGetter<ReadOnlyMemory<char>> (about 8 lines) where ML.NET's own
  ColumnCursorExtensions.GetColumn<string> (Microsoft.ML, present at the 2.0.1 floor) reads the column in one line:
  np.array(scored.GetColumn<string>("PredictedLabel").ToArray()) -> <U10, equal to the cursor route.
- For a VECTOR-of-text column (TokenizeIntoWords -> Vector<String>) the named getter is wrong: ML.NET throws
  InvalidOperationException "Invalid TValue: 'System.ReadOnlyMemory`1[System.Char]', expected type:
  'Microsoft.ML.Data.VBuffer`1[System.ReadOnlyMemory`1[System.Char]]'". GetColumn<string[]> + np.array reads a
  rectangular one as a (rows, width) U array (<U7 (3,3)).
- Both routes the old message named for SCALAR text worked as written (np.array(strings) -> <U10, the StringDType
  form compiles and runs), so the change is the route's length and the vector case, not a broken claim for scalars.

The change (no behavior change: the same exception type for the same columns):
- NDArrayMLNetInterop.UnsupportedImportMessage(type, columnName = null): a scalar text column gets
  np.array(view.GetColumn<string>("<name>").ToArray()) (fixed-width U) and the StringDType form; a vector-of-text
  column gets np.array(view.GetColumn<string[]>("<name>").ToArray()) ((rows, width) when every row has one length)
  and says a cursor reads it as VBuffer<ReadOnlyMemory<char>>. The dtype map (FromDataViewType, no column) writes a
  "<column>" placeholder. Documented (summary, remarks, params, returns).
- ToNDArray(IDataView, DataViewSchema.Column) maps through TryFromDataViewType and refuses with the column's name,
  instead of FromDataViewType's column-less text (the same NotSupportedException otherwise).
- docs/website-src/docs/interop/mlnet.md: the Dtypes paragraph says ToNDArray refuses text columns and gives both
  one-line routes; the Key-columns paragraph says numeric labels read back through ToNDArray and string labels make
  PredictedLabel a text column, read with GetColumn<string>. src/NumSharp.Interop.MLNet/README.md the same.

Tests (test/NumSharp.Tests.Interop.MLNet/ImportEdgeTests.cs):
- TextColumn_Throws now asserts the message carries np.array(view.GetColumn<string>("T").ToArray()) and that the
  route reads the column as <U6 with a non-ASCII value intact.
- VectorOfTextColumn_Throws_AndNamesTheVectorRoute (new): TokenizeIntoWords' column is refused naming
  GetColumn<string[]>("Tokens"); ML.NET refuses the scalar getter on it; the named route gives a (2, 3) U array.

Results: ML.NET 2.0.1 net10.0 and net8.0 211 / 211; 4.0.2 net10.0 211 / 211; 5.0.1 net10.0 211 / 211.

Not done (section 5, P15): converting text columns in the bridge itself (import to a U array; export of U /
StringDType arrays as text columns of the dictionary view) - a design for the owner.

(cherry picked from commit 8438ee3e0be07448a737c272a491bd148c16f292)
…nd qa/proposals-all's runs (the test project's default leg is 2.0.1, not 5.0.1) and records the current-version runs of qa/proposals-all with F49 merged (1d5638da): ML.NET 5.0.1 net10.0 / net8.0 and 4.0.2 net10.0, 216 / 216 each

- a6baabb2's message says "ML.NET 5.0.1 net10.0: 215/215": that run passed no -p:MLNetVersion, and
  test/NumSharp.Tests.Interop.MLNet's default MLNetVersion is 2.0.1 (the floor), so it was 2.0.1 on net10.0.
  Commit messages are never amended; section 5 (P1), section 8 and the log now say what ran.
- qa/proposals-all merged qa/interop-release at 8438ee3e (F49) as 1d5638da: the ML.NET page and README auto-merged
  (P1 edits them too), and the current versions are green: 216 = 210 + 5 (P1) + 1 (F49).
- The ORT test project's default is its floor 1.16.0; every ORT result quoted here passed the version explicitly.

(cherry picked from commit a907e55010d92de958e8c6e4b0b30ccdfe8eb682)
…(00:46 for F49, 00:53 for the version correction), not estimates

(cherry picked from commit 5a7c42eb3ea717e0be52ccc73e53d58265b533b1)
…rted - ConvertToOnnx declares every training column as an input, the unread label too, so feed it as zeros (a snippet that runs as written) - and quotes the graph error ORT gives when it is left out; the QA file records F50 (ML.NET's API surface over a view) and F51

Release QA angles A32 and A33 (docs/plans/interop-onnx-mlnet-release-qa.md).

F51 - the deployment route that uses both packages (scratch consumers/ml_to_onnx.cs: Microsoft.ML 5.0.1,
Microsoft.ML.OnnxConverter 0.23.1, Microsoft.ML.FastTree 5.0.1, Microsoft.ML.OnnxRuntime 1.31.1, the packed
0.80.0-beta1 bridges):
- Trained from NumSharp arrays through AsDataView, exported with mlContext.Model.ConvertToOnnx and served through
  session.Run, the scores equal ML.NET's own within 3e-8 (Concatenate + NormalizeMinMax + Sdca) and 7.2e-7
  (FastTree), live counters 0 in both bridges.
- The export declares every training column as a model input, the label included (f0..f3 and Label, each
  Float [-1,1]), and every column as an output (f0.output ... Score.output). Scoring never reads the label
  (scratch consumers/docs_mlnet_export.cs: another label value gives the same score bytes).
- Fed the features only, the released bridge lets ONNX Runtime fail inside the graph:
  "[ErrorCode:Fail] Non-zero status code returned while running Identity node. Name:'Identity3' ... Trying to get
  a TensorSeq, but got: (null)" - it names neither the label nor any input. Python onnxruntime 1.29.0 refuses the
  same feed before the run ("Required inputs (['Label']) are missing from input feed (['f0', 'f1', 'f2', 'f3'])."),
  and so does the P12 proposal branch (scratch probes/p12_mlnet_export.cs on qa/proposals-all:
  "required inputs ([Label]) are missing from the input feed ([f0, f1, f2, f3])."). P12's entry in section 5 now
  cites this as its strongest real case.

The docs change (docs/website-src/docs/interop/onnxruntime.md, the ergonomic-tier section): "A model exported by
ML.NET" - what ConvertToOnnx declares, to feed each declared input with the label as zeros of its shape, to fetch
outputs by name, and the error a left-out label produces today. The snippet ran verbatim against ML.NET's export
(Score.output float32 (6,1)).

F50 - what users call right after training, over NDArrayDataView (scratch consumers/ml_surface.cs, Microsoft.ML
5.0.1 + Microsoft.ML.AutoML 0.23.1): Preview; CreateEnumerable<T> into scalar and [VectorType(4)] POCOs; SaveAsText +
LoadFromTextFile and SaveAsBinary + LoadFromBinary bit-exact with NaN, -0, +-inf and subnormals; TrainTestSplit with
a sampling key (no group on both sides); Permutation Feature Importance with the slot names Concatenate builds; an
AutoML regression experiment (6 runs in 10 s, best LightGBM R2 0.983). All work; live views back to 0.

Also: work-list row A35 (ML.NET 6.0.0 previews against the suite) added; log lines.
(cherry picked from commit 13c559ddaa03ba8bfc92b9cd4a76af8fa56ab398)
…0.0, 6.0.0) becomes a restore error (NU1107) for users who upgrade Microsoft.ML to 6.0.0, due with .NET in November; the suite already passes on ML.NET 6.0.0-preview.26513.4 (net8.0 and net10.0, 211 / 211)

Release QA angle A35 (docs/plans/interop-onnx-mlnet-release-qa.md).

- nuget.org has Microsoft.ML 6.0.0-preview.26160.2 / .26457.2 / .26513.4 (arcade dates 2026-03-10, 2026-09-07,
  2026-10-13); ML.NET majors ship each November with .NET. Microsoft.ML 6.0's nuspec depends on
  Microsoft.ML.DataView 6.0 (>=).
- NuGet resolves cousin ranges that do not overlap as NU1107 (an error), proven with real packages one major
  lower: a stand-in package depending on Microsoft.ML.DataView [2.0.0, 4.0.0) beside Microsoft.ML 4.0.2 fails
  restore with 'error NU1107: Version conflict detected for Microsoft.ML.DataView. Install/reference
  Microsoft.ML.DataView 4.0.2 directly ...' (scratch rangeprobe/).
- dotnet test -p:MLNetVersion=6.0.0-preview.26513.4: 211 / 211 on net10.0 and on net8.0; the previews resolve
  inside the range today because NuGet sorts 6.0.0-preview.* below 6.0.0, so nothing warns before the GA.
- P16 lays out the options (widen to [2.0.0, 7.0.0), drop the upper bound - VBufferAccessor.cs:36-38 already
  falls back to a copy if VBuffer<T>._values disappears - or ship a 0.80.x after 6.0.0) and recommends widening
  now with a CI leg on the newest preview. Not changed here: the range is a packaging decision of the owner's,
  and a published version's range is immutable.

(cherry picked from commit e271d366e8b052b15ceafbca5d7bcbfa69d5b35f)
…-mlnet-range d1d10236: Microsoft.ML.DataView [2.0.0, 7.0.0), the advisory next-preview CI step) and the combined branch's new head f2612d90 (216 / 216 on ML.NET 6.0.0-preview)

(cherry picked from commit 2053e4f147817fde4fc11159603a901744cbf8cb)
…at FEEDS one (ORT's OrtValue.CreateTensorValueFromMemory over new BFloat16(bits), and ORT's session.Run) instead of the import text's "read the raw 16-bit patterns", and the Limits say float8 / 4-bit models do not load in ONNX Runtime's C# API itself; the QA file records F53 (.NET 11 RC3) and F54

Release QA angles A34 and A36 (docs/plans/interop-onnx-mlnet-release-qa.md).

F54 - element types NumSharp has no dtype for (scratch realdata/make_exotic_models.py builds Cast models with each
type as the output and as the input; consumers/ort_exotic.cs runs them through the packed bridge, ORT 1.31.1):
- float8 (E4M3FN, E5M2), int4 and uint4: ONNX Runtime's C# API cannot load such a model at all - new
  InferenceSession throws "[ErrorCode:InvalidArgument] Unregistered TensorElementType value of: DataTypeMax" / 19 /
  22 / 21 while reading the metadata (Python's onnxruntime loads them, refuses the outputs with "No corresponding
  Numpy type", and returns float8e4m3fn as raw uint8). The bridge never meets them.
- BFloat16: refused everywhere with the type named. As an OUTPUT the import text fits ("cast inside the graph, or
  read the raw 16-bit patterns with GetTensorDataAsSpan<BFloat16>()"); as an INPUT the input path reused the same
  text, whose reading route cannot feed anything.

The change:
- NDArrayOnnxInterop.UnsupportedInputMessage(TensorElementType) (new, internal, documented): BFloat16 -> "the bridge
  cannot feed it. Build this input with ONNX Runtime's own API - OrtValue.CreateTensorValueFromMemory over a
  BFloat16[] (new BFloat16(bits), the high 16 bits of each float32's bit pattern) - and run with
  session.Run(runOptions, inputNames, inputValues, outputNames), or give the model a float32 / float16 input and cast
  inside the graph." new BFloat16(ushort) exists at the 1.16.0 floor, so the route holds across the range. Complex ->
  "nothing can be fed"; any other -> the generic text with "so the bridge cannot feed it".
- InferenceSessionExtensions (the input preparation) uses it; the "input 'X' is declared BFloat16: " prefix is
  unchanged. Output-side refusals keep the import text.
- docs/website-src/docs/interop/onnxruntime.md and src/NumSharp.Interop.OnnxRuntime/README.md (Limits): the BFloat16
  feed route, and that a float8 / 4-bit model does not load in ORT's C# API.

Test: SessionRunEdgeTests.BFloat16_AsAModelInput_AndAsAModelOutput_IsRefusedByName now pins the feed route in the
text (and that the reading route is gone from it) and FOLLOWS it: bf16_in fed through
OrtValue.CreateTensorValueFromMemory(new BFloat16(bits)...) and ORT's own Run gives Y = [0, 1, 2] through the
bridge's ToNDArray. ORT 1.16.0 net10.0 292 passed + 20 Inconclusive; ORT 1.31.1 net10.0 312 / 312.

F53 (record only): both suites pass on .NET 11 RC3 (11.0.0-rc.3.26522.148, go-live): the net10.0 builds rolled forward
through a VSTest testhost hosted by the official runtime zip (SHA-512 checked, extracted into scratch; the diag log
names the RC3 runtime): ML.NET (Microsoft.ML 5.0.1) 211 / 211, ORT (1.31.1) 312 / 312.

(cherry picked from commit ae97b8c74749e33c787395054b964201a2afab46)
… as untrimmed (zero-copy VBuffer import still write-through; the bridges produce no trim warning, Core 64 unannotated ones), and section 8's cherry-pick table lists the seven fix / docs commits it missed (ffd56474, 376b6923, 4817862b, 7d68a494, 8438ee3e, 13c559dd, ae97b8c7)

(cherry picked from commit f7699e8331c995a8894acce9ac0a4639f58842f2)
…CanShuffle false, as LoadFromEnumerable's view) and that SDCA on class-ordered rows trains badly from it - 0.50 accuracy on 20,000 class-sorted rows against 0.72 for the same rows permuted first (L-BFGS 0.74 either way) - so ordered training rows are permuted before the view; the QA file records F56 and P17 (a view that shuffles)

(cherry picked from commit b903226dbd5b2777176999a62fe701e0fcda92c7)
…otEncoder on unicode categories) fed from NumSharp U and S arrays is byte-identical to Python onnxruntime; A22 closed by F49 / P15; A38 (serving a bridge-trained model with PredictionEngine) and A39 (memory at scale) added

(cherry picked from commit f6d6fc5cc3794d9c47e6cffd2f53bc3572de7df3)
…rough PredictionEngine in a process without NumSharp, bit-equal to the bridge's scores; F59 - at 2M x 32 the released training route (scalar columns + Concatenate, the only way one view carries a label) trains SDCA 2x slower than a POCO list (9.2-10.8 s vs 5.1-5.3 s) at a lower peak (1,110 vs 1,470 MB), and P1's dictionary view trains at POCO speed (5.4-5.7 s) with an 841 MB peak - P1's performance case

(cherry picked from commit 5a26538d7559d6bd710d39036cfd8b8aa1eaf2be)
…heCheckpoint between Concatenate and a multi-pass trainer - on 2,000,000 x 32 rows SDCA trained in 5.7-5.9 s instead of 9.2-10.8 s (a POCO list: 5.1-5.3 s) for one more copy of the columns (peak 1.44 GB instead of 1.11 GB); F59's mitigation on the released API

(cherry picked from commit 22fcf90d177931fed327b402dd637547e50b8b63)
…ero-copy (out-of-core SDCA training over a mapped table with private bytes growing only by ML.NET's state; a read-only mapping fed to ONNX Runtime through AsOrtValue, logits equal to Python's; the files delete after disposal); A8 dropped with its reason

(cherry picked from commit f4d5821c2df44a8d599993bc87c9119cc721a0ee)
…-consumer-gates 3d3e897a: nupkg-consumer gates for both bridges, PASS on Windows and Linux, a planted dependency-range defect fails it)

(cherry picked from commit 52d523aff6afe486e1fe3be653d480d357a30545)
…-shuffling-view 6463b1b0: the view shuffles, tests proven by mutation, the example unchanged) and the combined branch's new head 1cb2deff (P14 and P17 merged, the cursor conflict with P1 resolved; ML.NET 220 / 220)

(cherry picked from commit 3443c33626be6e096be0ac3302253b9f6bc72215)
…AGES=1: the suites on qa/interop-release and qa/proposals-all, and the views that end or start at their buffer's edge through the vector, scalar, shuffled and dictionary cursors and ORT's Auto copy (a control proves the mode was live)

(cherry picked from commit 5f9b5962c16144de9a31b986be5e3d3bdd03b9e7)
…oad (NumSharp has no non-native arrays, so no swapped bytes can cross) and misaligned buffers cross both bridges zero-copy correctly (SqueezeNet on a View-mode input at byte offsets 1-3 is byte-identical to Python)

(cherry picked from commit 1d8ae93881af2355f9c68a27e4c6f9f7419c5869)
…ess and the remedy - a Transform's output reads the view only when it is read itself, so a using scope that ends before ToNDArray / Evaluate lands far from its cause - and the ML.NET page's Lifetime section says so; F63

Release QA angle A43 (docs/plans/interop-onnx-mlnet-release-qa.md).

Proven (scratch consumers/ml_lazy_scope.cs, packed beta, Microsoft.ML 5.0.1): the natural C# shape
  using (var view = x.AsDataView("Features")) output = model.Transform(view);
  output.ToNDArray("Features");
throws ObjectDisposedException "the NDArray-backed IDataView has been disposed; its NumSharp buffer is no longer
pinned." at the read - accurate, but nothing in it points back at the scope, because Transform read nothing; a
ToDataView snapshot in the same shape fails the same way (the VIEW was disposed). The page did not say it.

The change (no behavior change: the same exception, at the same place):
- NDArrayDataView.ThrowIfDisposed: the text adds "ML.NET reads lazily: a Transform's output (and every view built on
  this one) reads this view only when it is read itself, so keep this view undisposed until the last read of
  anything built on it (ToNDArray, Evaluate, Fit, a cursor)."; the method is documented (summary, remarks, the
  exception).
- docs/website-src/docs/interop/mlnet.md, Lifetime: "ML.NET reads lazily, so dispose the view after the last read",
  with the scope shape and the remedy.
- Test: ErrorTests.LazyTransformOutput_ReadAfterTheViewsScope_NamesTheLaziness - an AsDataView and a ToDataView
  disposed before their transform's output is read: ObjectDisposedException naming the laziness and the remedy;
  live exports 0.

Results: ML.NET 5.0.1 net10.0 212 / 212; ML.NET 2.0.1 net8.0 212 / 212.
(cherry picked from commit 4aa6b2f402fc6cc56ba361ecc3d8cb2854769ad3)
…rry-picks with no conflict onto release-0.80.0-beta1 at ac6526e2 (simulated read-only with git merge-tree, the tool proven on a known conflict), and the result builds and passes ORT 1.31.1 312 / 312, ORT 1.16.0 292 + 20, ML.NET 5.0.1 and 2.0.1 212 / 212

(cherry picked from commit 424aa1d770529baac55caf31cc87ebf84f8a5c4b)
…sult: each applies cleanly alone, and together only the three known conflicts appear (P11 and P7 after P12 in the Run forms' XML docs, P17 after P1 in the cursor constructor), resolved by qa/proposals-all's merge commits fa99e1c4, ddb979f8 and 1cb2deff

(cherry picked from commit 1abe3272d214d2213a735bc92722b2a78d5bd221)
… what works (with the finding ids), what was fixed on this branch, what must be decided before the tag (P16's NU1107 first), the proposal branches ready to merge, and what is deferred

(cherry picked from commit fcbae9ac5fb04433a508a32a4d4da24ef92a5d4e)
…ies, NaN, +-inf, -0.0, axis 0, int32, float16 and largest=false; k = 0 is refused by the documented contract where torch returns empty arrays

(cherry picked from commit 2ec57126fe03a1005ff14decd83ce1921d0b2b3b)
…is not C-contiguous is refused with the OUTPUT remedy - a C-contiguous array, or Run and np.copyto(view, result) - instead of the export text's "materialize a copy, or ToOrtValue", which for an output would make ORT write a copy while the caller's view silently kept its old values; F65

Release QA angle A45 (docs/plans/interop-onnx-mlnet-release-qa.md).

Proven (scratch consumers/ort_offsets.cs, packed beta, ORT 1.31.1):
- offsets are right: x["1:"] (C-contiguous, offset 4) is shared zero-copy at its own address under OnnxCodecMode.View
  and Identity returns its rows; RunInto into out["1:3"] of a (5, 4) buffer writes rows 1-2 and leaves rows 0 and
  3-4 at their sentinel;
- a non-contiguous pre-allocated output (a column slice) was refused with NDArrayOnnxInterop.NotCContiguousMessage,
  the EXPORT text: "Materialize first - np.ascontiguousarray(nd) or nd.copy() - or call ToOrtValue(nd) for an
  ORT-owned copy." Followed for an output, ORT writes the copy and the caller's view keeps its old values - the code's
  own comment in PrepareOutput says "a copy would absorb the result silently" - and ToOrtValue is an input verb.

The change (the same InvalidOperationException, same place): InferenceSessionExtensions.PrepareOutput refuses with
"output 'Y': the pre-allocated array is not C-contiguous, and ONNX Runtime writes an output in place as dense
row-major memory with no strides. A C-contiguous copy (np.ascontiguousarray / nd.copy()) would receive the result
instead of this array, so pass a C-contiguous array (np.empty of the output's shape and dtype), or call Run and copy
its result into this view with np.copyto(view, result)." Inputs keep the export text.

Test: SessionRunEdgeTests.PreallocatedOutput_NonContiguous_IsRefused now checks the remedy (and that ToOrtValue is
not offered) and follows it: Run + np.copyto into the transposed view lands Y1 in the base buffer.

Results: ORT 1.31.1 net10.0 312 / 312; ORT 1.16.0 net8.0 292 passed + 20 Inconclusive.
(cherry picked from commit 226e8f61c3f568d43d773d59c4c4866e9974fc2c)
…sage: all exist, ONNX Runtime's at the 1.16.0 floor and 1.31.1

(cherry picked from commit b050aa462457cc97cf0e199e080489902eec1960)
… reproduces PyTorch's tokens with every step's logits byte-equal to Python onnxruntime, copied and zero-copy outputs fed back alike (an undisposed slice of a zero-copy output holds ORT's buffer: the easy leak); RunAsync needs P7 for the decode step, which P7 runs equal to Run

(cherry picked from commit 09f1eda12d8a9ae35dfc3b1df26a519fcbb693fa)
… it takes of zero-copy outputs (an undisposed slice keeps the whole output's ORT buffer until a collection: a 12-step GPT-2 decode loop left 7 alive), and that zero-copy outputs fed back as inputs (a KV cache) are read in place; F67

(cherry picked from commit 46f78deac85bb6fb32b7c71adb2dd3d2734fa8db)
…des and resizes the photos (it said ImageSharp, which the example replaced - F28: ImageSharp restores with advisories and needs a license key from 3.2)

(cherry picked from commit 1da48cb5be9268d629e00de8dc77e4f108f2dfa7)
Nucs added 27 commits November 7, 2026 11:49
… and 6,091 internal members with file:line and summary, what each closes to a consumer who extends or inherits, the 32 internals our own InternalsVisibleTo packages need, and a measured NDArray-subclass probe (11 of 11 operations drop a consumer's class; NumPy 2.4.2 keeps it in all 11)

Owner's request: "Map out all the internal classes and methods. NumSharp is generally supposed to be extensible and
inheritable". Measured on strings b10afc4 (the line 0.80.0 ships from; journey4's three commits past the merge base
change no API), in a branch of its own so no shared worktree is touched.

New tool: tools/internal-surface-map/SurfaceMap.cs (a .NET 10 file-based app on Mono.Cecil 0.11.6)
- Reads the BUILT NumSharp.dll (metadata + portable PDB + XML doc file) instead of grepping, so accessibility is the
  compiler's: effective reach (a public member of an internal type is internal; private protected counts as internal;
  a nested type takes the narrowest access of its chain), partial types, per-accessor access, and compiler-generated
  code (closures, iterators, backing fields, file-local types, records' synthesized members) excluded.
- Locations: the PDB's first sequence point moved to the signature line (a block body starts at its brace, an
  expression body at its expression); a constructor is placed at its base/this-constructor call (its first points are
  the type's field initializers); fields, constants and abstract members are found by a keyword-checked search of
  every file of the partial type (access + abstract/virtual/override/const/static on the line, then the name in
  declaration position; `new X {` never counts). Verified: all 6,091 member lines declare their member (34 implicit
  constructors link their type's declaration); all 889 links in the docs resolve.
- Friend scan: walks the IL of every InternalsVisibleTo friend built under src/ (OpenBLAS, pythonnet, OnnxRuntime,
  MLNet, System.Numerics.Tensors, ParquetNet): calls, fields, signatures, locals, base types, overrides.
- Source scan: GetType() equality tests, split into the 5 that can treat a subclass differently from `is` and the 53
  element-type dispatches.
- Builds with 0 warnings under the repo's Directory.Build.props; one run takes ~45 s.
- Regenerate: build src/NumSharp.Core and src/NumSharp.Interop.* for net10.0, then
  `dotnet run tools/internal-surface-map/SurfaceMap.cs -- --repo . --out docs/plans/internal-surface --label "<branch> <commit>"`.

New docs: docs/plans/internal-surface/
- README.md: the analysis - method, numbers, ten extension scenarios with verdicts and evidence, what the internals
  are by kind, and seven seams to decide on (nothing implemented).
- tables.md (generated): totals, areas, visible types with internal members, findings by category, virtuality, friend
  usage per package, exact type tests, every internal type by area with summary and source link.
- internal-types.tsv (493 rows), internal-members.tsv (6,091), extensibility-findings.tsv (470), virtuality.tsv,
  friend-internals-usage.tsv (47 package-by-member pairs, 410 IL sites), exact-type-checks.tsv (58).
- probe/subclass_probe.cs + subclass_probe.py: a consumer-side `MyArray : NDArray` (public API only,
  __array_priority__ = 20) through np.add, x + 1, -x, np.sqrt, two slices, .T, reshape, np.sum(axis), concatenate,
  copy, asanyarray. NumSharp: plain NDArray for all 11 operations; NumPy 2.4.2: MyArray for all 11; asanyarray returns
  the same object in both.

Findings (strings b10afc4):
- 1,299 declared types: 524 public, 493 internal, 282 private nested, no protected nested type. 6,091 internal members:
  1,908 declared internal / private protected on 170 visible types, 4,183 on internal types. Biggest areas:
  Backends/Strings 68 types / 811 members, Backends/Kernels 109 / 542 (+494 on public kernel classes),
  Backends/Default 73 / 515 (+173), Backends/Iterators 48 / 302 (+264).
- Dead code: Utilities/Maths/Operator.cs - one internal class, 726 members, zero callers in src/ or test/.
- OPEN: engines. TensorEngine 258/267 visible members overridable with a protected ctor, DefaultEngine 254/277,
  LinalgEngine / DefaultLinalgEngine / FftEngine / DefaultFftEngine 100%, TensorEngine.Default publicly settable, no
  internal abstract member anywhere on the engine side. Optional internal virtuals: Evaluate(NDExprProgram) (compiled
  expressions run only on DefaultEngine-derived engines), DotFunction1D / VdotFunction1D / VdotReadsFirstInPlace.
- NDArray subclass compiles but loses its class: __array_wrap__, both __array_finalize__, _collapse, NanMax / NanMin and
  ReprClassName are internal virtual (only __array_priority__ is public and nothing reads it for an unknown class);
  NDSubclass.KindOf exact-tests NDMatrix and NDCharArray only; SubclassWeaver generates arms for the subclasses found in
  NumSharp.dll (`internal const string __name__`). 7 of NDArray's 271 visible instance members are overridable.
- CLOSED: NDMatrix / NDCharArray / NDMaskedArray (internal ctors only); BitGenerator (private protected ctor, internal
  abstract NextUInt64 / NextUInt32 / Name, private protected abstract seeding/state, 9 internal virtual draws and fills;
  Generator and every concrete bit generator sealed - NumPy documents user BitGenerators via bitgen_t); NDPolyBase (23
  private protected members incl. its ctor; the six series classes and poly1d sealed); NDExpr nodes (7 internal
  abstract members; NDExpr.Call(Delegate/MethodInfo) is the public route).
- User dtypes: DTypeRegistry.Register, the protected DTypeMeta ctor and AddCastingImpl are public, but setitem / getitem
  (throw "no slot" in the base), SetFreshItems, copyswapn, fill, nonzero and six slot flags are internal virtual - a
  registered user dtype can neither store nor read an element; NPTypeCode is a closed enum and ufunc loops have no
  per-dtype registry (ArrayMethod serves casts only).
- Storage: UnmanagedStorage has 28 public/protected ctors and 0 of 159 visible members virtual; its state (OwnsData,
  ItemBytes, WriteProtected, ExternalBase, _baseStorage, ...) is internal.
- Bridges: our six packages name 32 distinct internals. Public routes exist (at a cost) for Shape.dimensions / strides
  (Dimensions / Strides allocate per read), offset / size / bufferSize, the 4-arg Shape ctor and Adopt (public
  Shape(dims, strides) copies, no offset), NDArray.GetTypeCode (typecode), np._FindCommonArrayType (promote_types),
  ForOverwrite (np.empty). No public route: SizeBucketedBufferPool.BookForeign / ReturnForeign (ORT, MLNet, Tensors,
  pythonnet), ArraySlice.WrapShared (ORT, 14 sites), the VString allocator (pythonnet, 53 sites).
- Exact type tests: 58; 5 subclass-relevant (NDSubclass.KindOf x2, Slice.Equals, Shape.Equals on a struct, NDExpr's
  delegate comparer).

Seams to decide (README "Seams to decide on"): 1 the subclass protocol (hooks overridable outside + an open-world
dispatch), 2 protected ctors on NDMatrix / NDCharArray / NDMaskedArray, 3 a public bridge surface (foreign-byte
booking, WrapShared, a Shape from dims/strides/offset/bufferSize, allocation-free dims/strides reads), 4 BitGenerator
primitives, 5 NDPolyBase ctor + unsealed series, 6 DTypeMeta slots (plus a loop registry to compute), 7
TensorEngine.Evaluate(NDExprProgram).
…nce 8e5324b: the analyzers' build-cost work (0c61a57), the weaver's (d612f99) and the owner's comment-only NPTypeCode.cs edit (cd31b79)

strings had journey4 up to 8e5324b (f87de21, 2026-10-31); journey4 then moved three commits past it, and the owner
asked for the build fix on strings (2026-11-07: "bring to strings"). The same three commits reached release-0.80.0
through e230280; strings has not changed any of their files since b10afc4, so the merge is clean here too
(git merge-tree found no conflict; ScopeWeaver.cs and tools/NumSharp.Build/README.md auto-merged).

- 0c61a57 perf(build-analyzer): the three ownership analyzers share one per-compilation fact set (CompilationFacts)
  and a syntax-only name index (NumSharpNameIndex) that skips every method body and type that cannot involve an
  NDArray, take Roslyn's generated-code test over (GeneratedCodeFilter, pinned to the driver by a differential test),
  and walk a body with OperationKind dispatch and a pooled per-thread scan - on journey4: a NumSharp-free project's
  holder analysis 13 s -> 0.003 s, NDW012 on NumSharp itself 3.5 s -> 1.5 s in a fresh compiler process; every
  diagnostic unchanged. strings' own analyzer, InvariantExceptionTextAnalyzer (NDW020), keeps its own setup.
- d612f99 perf(build): CecilSemanticsPrefill reads property and event accessors in linear time instead of Mono.Cecil's
  quadratic scan (a 50,000-property type 8.2 s -> 1.8 s, written bytes identical); the tool opts out of Windows'
  efficiency mode and runs without dynamic PGO; NumSharp.Core's self-weave runs it under a 4-heap server GC through
  the new NumSharpBuildToolEnvironment knob (1.65 s -> 1.05 s per framework on journey4).
- cd31b79 Update NPTypeCode.cs: comments only.

Gates on this tree (Windows, i9-13900K, other sessions' builds running):
- NumSharp.Tests.Build.Analyzer (Release): 499 passed on net10.0 and on net8.0 (strings' tests plus the merge's 23:
  PayForPlayIndexTests 18, CecilSemanticsPrefillTests 5).
- NumSharp.Core Release build, both frameworks: 0 errors; the self-weave wove 2,404 methods on each framework,
  re-signed, symbols updated.
- NumSharp.Tests (Release, net10.0), the weave, strong-name and DType gates (NDScopeWeave, StrongName, NDScopeTests,
  NpyTypeChar, DTypeStringGrammar, DTypeOracle, NDScalarType, MixedScalars, DTypeCasting, type_checks): 255 passed.

Measured on this tree's Core build (csc /reportanalyzer, both frameworks compiling at once on a loaded machine):
NumSharp.Build.Analyzer 4.4 / 6.3 s - NDW012 2.9 / 4.1 s, NDW020 0.66 / 0.62 s, the target gate 0.54 / 0.70 s, the
holder analyzer 0.34 / 0.87 s. strings' Core is a far larger compilation than journey4's (2,404 woven methods to 638),
so NDW012 there is still over the owner's one-second goal: the next step on strings.
… ships 0.80.0 safe to dispose under threads - every OrtValue it touches carries an ARC, and NDInferenceSession releases a session once after the bridge's runs on it (a7cff38); the other bridges' rules documented (c0c513b) - the owner's decisions of 2026-11-07

The owner: "Merge to strings and release-0.80.0". The same two commits strings received in 34e48e1, merged onto
release-0.80.0 bf6f404 (strings b10afc4 + the journey4 merge + the 0.80.0 version and notes commits), so the first
release of NumSharp.Interop.OnnxRuntime carries them:
- a7cff38 feat(onnxruntime): the value ARC (a call lease per verb, a view lease per view, the native release taken over
  through ORT's _memHandle hook so it happens once after the last lease) and the new public NDInferenceSession. Plan
  section 10.
- c0c513b docs(interop): OpenBLAS's library-swap rule, the TensorSpan rule and pythonnet's GIL rule for Dispose; the
  pythonnet shutdown fix deferred after its proof. Plan section 11.

Two files were edited on both sides and merged without conflict: .claude/rules/interop-packages.md (the release
reworded the release state and the new-package check) and src/NumSharp.Interop.OnnxRuntime/README.md (the release's
"first release is 0.80.0" sentence; this branch's lifetime and threads bullets).

Verified on the merged tree (Release, Windows): ONNX Runtime 1.31.1 384 / 384 (net10.0); 1.16.0 360 + 24 Inconclusive
on net8.0 and net10.0; ML.NET 241 / 241 (net10.0); System.Numerics.Tensors 114 / 114 (net10.0); the pythonnet and
OpenBLAS bridges build.

The 0.80.0 notes do not mention this yet: NDInferenceSession is a new public type of a package 0.80.0 releases, and the
Overview's tallies are kept by the notes' other commits.
…rrency merged into strings as 34e48e1 and into release-0.80.0 as bfb8244, with both trees' verification (ORT 384 / 384 and 360 + 24, ML.NET 241, Tensors 114)

Sections 0, 8, 10 and 11 said the branch was not merged; they now name both merges, and the log records how they
landed: each made in a detached worktree and fast-forwarded, no conflict, release-0.80.0 not pushed (PR #633's head
is 3 commits behind it) and its notes not yet naming NDInferenceSession.
…t.org Trusted Publishing (482d44d, merged into strings as efdf19f) and the ONNX Runtime plan records its threads work as it landed (e1619cc); journey4 and qa/ort-concurrency were already here

The owner, 2026-11-07: "check if journey4 has changes that strings missing so bring them to strings and then if
strings needs to be merged to release-0.80.0 and then if release-0.80.0-beta1 needs to be merged to release-0.80.0".
Of the three, only strings -> release-0.80.0 had anything to bring; the other two are recorded at the end.

What strings had that release-0.80.0 lacked (release-0.80.0..strings, 5 commits)
- 482d44d ci: nuget publish via Trusted Publishing (OIDC), drop long-lived API key, with its strings merge efdf19f
  (2026-11-07): publish-nuget trades the job's GitHub OIDC token for a one-hour nuget.org key through NuGet/login@v1
  (job permissions id-token: write, contents: read, actions: read; `user` from the NUGET_USER secret, the profile name
  of the policy's CREATOR), keeps strings' every-package push loop and pushes with the login step's key. The
  NUGETAPIKEY secret is no longer read. .claude/rules/ci-release.md and interop-packages.md describe the policy instead
  of the key, and RELEASE_0.80.0.md gains its Testing & Tooling line ("Packages publish to nuget.org through Trusted
  Publishing ... - 482d44d").
- e1619cc docs(plan): docs/plans/interop-onnx-mlnet-release-qa.md names qa/ort-concurrency's two merges (strings
  34e48e1, release-0.80.0 bfb8244).
- ff2bde6 (journey4 d612f99 into strings) and 34e48e1 (qa/ort-concurrency into strings) bring no content: the same
  commits reached this branch through e230280 and bfb8244, and both pairs resolved to identical trees (the merged
  tree differs from bfb8244 only in the five files below).

Conflict and its resolution (.claude/rules/interop-packages.md, one hunk)
- This branch's 1495928 renamed the release "0.80.0-beta1" -> "0.80.0" in "the two packages released for ...";
  strings' efdf19f replaced "the key behind `NUGETAPIKEY`" with "the Trusted Publishing policy" in the next line.
  Both edits are kept: the resolved file differs from strings' copy only by 1495928's renames and from bfb8244's
  copy only by efdf19f's sentence.
- Auto-merged: .github/workflows/build-and-release.yml (strings changed publish-nuget; this branch's edits to the file
  lie elsewhere) and docs/releases/RELEASE_0.80.0.md (strings' one line; bf6f404's Overview tallies untouched).

Checks on the merged tree (the five files are a workflow, two rules, a plan and the notes: no code, so no build)
- git diff --check clean; no conflict marker; no em or en dash in the 55 added lines.
- PyYAML parses all 6 jobs; publish-nuget = download-artifact@v8, setup-dotnet@v6, NuGet/login@v1 (id nuget-login,
  user secrets.NUGET_USER), then the push loop on steps.nuget-login.outputs.NUGET_API_KEY; no workflow reads
  NUGETAPIKEY.
- Directory.Build.props keeps VersionPrefix 0.80.0 (strings still says 0.70.0; the merge does not touch the file).

Checked and not merged
- journey4 -> strings: nothing to bring. strings..journey4 is empty since ff2bde6, and replaying journey4's three
  newest commits (8e5324b..d612f99) onto strings' tip gives strings' exact tree. The 15 paths journey4 has and
  strings lacks were each deleted on strings by a single-parent refactor (795560d the max/min family's one signature,
  abbafef the matrix boundary, 7dbd3ef / 595e112 / d8545c5 / 62b5af8 / b7b1b4d engine moves, ed1b647 the
  ML.NET Postprocess, d8e2327 the matrix-operand OpenBugs file), and journey4 last touched each of them before
  strings' 10-31 merge f87de21.
- release-0.80.0-beta1 -> release-0.80.0: nothing to bring. Its 8 commits are here in a later form: e343aba8's ORT /
  ML.NET release and ad0ef23a's version (1495928 redid the rename over strings' newer doc text); cad16ea7's Linux /
  macOS pins as 2402b23 (its RELEASE_0.71.0.md -> RELEASE_0.80.0-beta1.md rename left out on purpose); 335d67f4 /
  54cde1b4's notes as the input of RELEASE_0.80.0.md (608215d derived part 1 from 54cde1b4's bullets) and
  e8944e69's C# 12 line as RELEASE_0.80.0.md line 360; bf75552b's and ac6526e2's merged commits (cd31b79, strings
  860447b) are ancestors. A trial merge of the whole branch conflicts only where it would put back "0.80.0-beta1"
  release names, doc text from before the subclass redesign ("the matrix boundary's plain region") and the rename.

Not done here
- Not pushed: PR #633's head stays at bfb8244; this branch is now 6 commits past it.
- The notes' Overview tallies (bf6f404: 439 cited commits of the release's 1,586) are not recounted: 482d44d is a
  new citation and the merges add commits. The release session recounts at its final commit, as efdf19f and bfb8244
  also left it.
- The owner's, from 482d44d / efdf19f: before the v0.80.0 tag, check on nuget.org (Account > Trusted Publishing)
  that the policy is active, scoped "Push new packages and package versions", with a glob matching the two new ids
  (NumSharp.Interop.OnnxRuntime, NumSharp.Interop.MLNet); delete the NUGETAPIKEY secret after the first trusted
  publish.
…s, strides, copy = true) replace the internal Shape.Adopt factories - NumPy's copy keyword is now the one named opt-out of bug 2d's copy, for users and Core alike; the default still copies, Core's 374 sites call new Shape(arr, copy: false), allocations byte-identical

WHY
---
The owner asked why Shape.Adopt exists at all ("dont we have an existing method to do that?"), then "Why not go back to
constructors and make them public? also passing dims should be cloned or not". Findings, 2026-11-07:
- Adopt came with 5c9d792 (bug 2d of docs/plans/view-shape-open-bugs.md), when the public long[] constructors started
  copying. Before that commit the public Shape(params long[]) WAS the no-copy route (`this.dimensions = dims;`), which is
  exactly what let `long[] s = {3, 4}; var a = np.zeros(s); s[0] = 100;` turn `a` into (100, 4) over 12 elements.
- One constructor signature either copies or keeps, and C# cannot overload on accessibility (an internal Shape(long[])
  is CS0111 against the public Shape(params long[])), so the keeping route needs a signature of its own: a factory name
  (Adopt) or a parameter.
- The only older no-copy constructor, the internal view constructor Shape(dims, strides, offset, bufferSize), covers
  only the strides form and needs the caller's buffer size: bufferSize 0 is not a safe "unset" there, because
  UnmanagedStorage.OverBuffer (Count = layout.bufferSize) and DefaultEngine.CanonicalResultShape (bufferSize == size)
  read the raw field.
- What each answer to "cloned or not" costs, measured on d612f99 in two detached worktrees (Release net10.0, bytes per
  call): ALWAYS copying (Adopt made to copy) adds +184 B to np.sum(m, 0) and np.mean(m, 1) on 10x10 (1576 -> 1760,
  +12%), +224 B to np.sum(big, 1) on 10x10x10 (1704 -> 1928), +32-40 B to np.argmax(m, 0), np.concatenate and m.T.copy(),
  and nothing to a + b, np.sqrt, np.cumsum, np.matmul, reshape. NEVER copying reopens bug 2d: through a kept caller
  array, after s[0] = 100 z.shape is (100, 4) while z.size stays 12 over a 12-element buffer, and z[99, 3] addresses
  element 399.
On a four-option decision page (constructors + copy keyword / keep Shape.Adopt / always copy / never copy) the owner
chose public constructors with NumPy's copy keyword, copying by default.

WHAT
----
- View/Shape.cs:
  * Shape(long[] dims, char order) -> Shape(long[] dims, char order = 'C', bool copy = true). copy: true copies, as
    before; copy: false keeps dims (null -> the 0-d shape). ValidPhysicalOrder stays the first constructor-initializer
    argument, so a bad order still throws before any copy.
  * Shape(long[] dims, long[] strides) -> Shape(long[] dims, long[] strides, bool copy = true). The null and length
    checks run in both modes (the adopting core trusts its arrays), before anything is copied.
  * internal static Shape.Adopt(long[]), Adopt(long[], char) and Adopt(long[], long[]) are removed (no [Obsolete]): the
    two constructors reach the same private AdoptTag cores, so every field (dimensions, strides, size, buffer size,
    offset, flags, hash, IsScalar) is what Adopt built.
  * Unchanged, all copying: Shape(params long[]), Shape(params ReadOnlySpan<long>), Shape(long[], long[], Shape) and the
    long[] -> Shape / Shape? conversions. new Shape(dims) still binds params long[] (identity conversion, no default
    substitution), and a C# 12 caller's new Shape(3, 4), which has no span params, still binds it too.
  * Docs: both constructors document <param name="copy"> and, in remarks, the hazard: a later write into an array passed
    with copy: false changes the dimensions of every array built on the shape but not its size or buffer, so indexing
    reads and writes past the buffer; NumPy has no such mode. Shape(params long[])'s remarks point at copy: false
    instead of Adopt; AdoptTag's summary says the public forms copy unless told copy: false.
- 363 call sites in 146 files (Core and the OpenBLAS friend package), rewritten mechanically: Shape.Adopt(x) ->
  new Shape(x, copy: false); Shape.Adopt(x, order) -> new Shape(x, order, copy: false); Shape.Adopt(d, s) ->
  new Shape(d, s, copy: false); NDIter's NumSharp.Shape.Adopt(...) -> new NumSharp.Shape(..., copy: false); and the bare
  Adopt(outputDims, strides) of Shape.StridePerm.ReductionOutputShape. Three calls sit in interpolated-string holes of
  the polynomial package (NDPolyCalc.cs, NDPolySequence.cs); copy: false inside the call's parentheses is not a format
  specifier, and they compile. The rewrite matched parentheses while skipping strings, chars and comments, rewrote the
  innermost call first, and skipped crefs (fixed by hand). No call spans lines.
- Overload safety, proved on a mock with exactly Shape's overload set before any edit: new Shape(dims) -> params long[];
  new Shape(3, 4) -> params span; new Shape(dims, 'F') -> (dims, order, copy = True); new Shape(dims, copy: false) ->
  (dims, order = 'C', copy = False); new Shape(d, s, copy: false) -> (dims, strides, copy = False);
  new Shape(d, s, 0, 12) -> the internal view constructor; (Shape)dims -> params long[]; new Shape(dims, false) ->
  CS1503 (a positional false lands on order), so every keeping site has to name copy.

TESTS
-----
test/NumSharp.Tests/View/Shape.CallerArrays.Tests.cs (13 -> 16):
- Adopt_SharesTheArray_ThePublicConstructorDoesNot -> CopyFalse_KeepsTheArray_TheDefaultCopies: copy: false keeps dims
  and strides in all three forms; the default and an explicit copy: true copy.
- New CopyFalse_BuildsTheSameShapeAsTheCopy: every field equal between copy: false and the copying form, C and F over
  (2,3), (4), (2,0,3) and (2,3,4), null and empty dims (= Shape.Scalar), and strides with a broadcast (0) and a reversed
  (negative) axis.
- New CopyParameter_DefaultsToTrue (reflection): copy defaults to true on both constructors and order to 'C' (a false
  default would silently bring bug 2d back to every new Shape(dims, 'F') and new Shape(dims, strides) call); the
  two-parameter forms are gone.
- New CopyFalse_ArgumentChecks_MatchTheCopyingForm: null dims, null strides, mismatched lengths and a logical order throw
  the copying form's types and texts under copy: false.
- The allocation tests compare the inline forms with new Shape(new long[] {r, c}, copy: false): same bytes as before.

GATES
-----
Detached worktree at d612f99 + this change, the OpenBLAS binaries staged:
- Solution build (Debug, net8.0 + net10.0): every project builds except NumSharp.Benchmark.CSharp net8.0, whose 11
  CS0121 ambiguities (np.amin / amax / min / max under C# 12, MinMaxBenchmarks.cs and SimdVsScalarBenchmarks.cs) occur
  identically on d612f99 without this change.
- NumSharp.Tests, CI filter: net10.0 18,653 passed, 11 skipped, 0 failed; net8.0 18,652 passed, 11 skipped, 0 failed.
  ShapeCallerArrayTests 16 / 16.
- NumSharp.Tests.Interop net10.0 against live NumPy 2.4.2 (Python 3.12.12): 740 passed, 1 failed:
  CreationCtorEdgeCaseLiveParityTests.T1_47_CanCoerceAll_VariousStarts_NoThrow_Match_Numpy_ResultType, which fails the
  same way on d612f99 (np._can_coerce_all(arr, 3) is Double there too). 8804843 ported NumPy 1.26.4's search, whose
  start indexes the candidate list __test_types, not the input list; the test still expects the old Empty. Not touched.
- Non-vacuity, one exact edit of Shape.cs per mutant, rebuilt, ShapeCallerArrayTests run: M1 (the order form defaults
  to copy: false) fails CopyFalse_KeepsTheArray_TheDefaultCopies, CopyParameter_DefaultsToTrue and
  ShapeConstructorsAndConversions_CopyTheCallersArrays; M2 (the strides form defaults to copy: false) fails the same
  three; M3 (the order form ignores copy: false) fails CopyFalse_KeepsTheArray_TheDefaultCopies and both allocation
  tests; M4 (the strides form skips its checks under copy: false) fails
  CopyFalse_ArgumentChecks_MatchTheCopyingForm. 4 / 4 killed; the restored source is byte-identical and passes 16 / 16.

COST
----
The same byte script on d612f99 (Shape.Adopt) and on this change (new Shape(..., copy: false)), Release net10.0:
new Shape(long[2], copy: false) 40 B = Shape.Adopt(long[2]) 40 B; long[3] 48 = 48; and all 12 operation rows equal
(a + b 520, np.sqrt 520, np.sum(m, 0) 1576, np.sum(big, 1) 1704, np.mean(m, 1) 1576, np.argmax(m, 0) 584,
np.cumsum 552, m.reshape(20, 5) 496, m.T.copy() 1000, np.concatenate 1072, np.matmul 600, np.zeros(long[2]) 568).
The keeping constructor does one more comparison than Adopt(long[]) did (the order test of the shared ordered core).

Docs: .claude/CLAUDE.md (= AGENTS.md) and .claude/rules/memory-views.md state the rule with copy: false (named, and why
a positional false does not compile); docs/plans/view-shape-open-bugs.md gets a dated follow-up under bug 2d.

Not changed: the interop packages' eight new Shape(dims) calls over arrays they build (pythonnet 5, ML.NET 1, ONNX
Runtime 1, System.Numerics.Tensors 1) still copy; they are friends now (the 5c9d792 note "not friends" is out of
date) and could pass copy: false. The benchmark project's net8.0 build fails with 11 CS0121 ambiguities between the
np.amin / amax / min / max overloads (C# 12), identically on d612f99 without this change.

BREAKING: Shape(long[], char) and Shape(long[], long[]) became Shape(long[], char = 'C', bool = true) and
Shape(long[], long[], bool = true): source-compatible, binary-breaking (a compiled caller of the old signatures needs a
rebuild). New public opt-out: copy: false.
… hooks are protected internal, the NumPy subclasses' constructors are public, the bridge surface (foreign-byte booking, WrapShared, the StringDType allocator's NpyString API, Shape's raw layout) is public, BitGenerator is a public contract with Generator and the five bit generators unsealed, TensorEngine's compiled-evaluation and 1-D dot hooks are public, and UnmanagedStorage's state is publicly readable (the owner's decisions of 2026-11-07)

WHY
---
The owner asked to "Map out all the internal classes and methods. NumSharp is generally supposed to be extensible and
inheritable" (the map: docs/plans/internal-surface, the previous commit), then decided the seams group by group:
"1. Make all protected internal / 2. make subclasses all public and their constructors / 3. Public / 4. Public and the
core ones also virtual and classes have clean proper constructors / 5. Shape - all public, TensorEngine.Evaluate(...)
DotFunction1D / VdotFunction1D / VdotReadsFirstInPlace - all public, np.NDEnumerate / NDIndex / NDIterator
constructors - public, UnmanagedStorage's state - public". Follow-up questions the same hour: group 4 = "Unseal the
classes"; UnmanagedStorage's three mutable fields = "Public read, internal write"; NDMaskedConstant's constructor =
"Public".

WHAT (every changed member's XML docs completed for a consumer: when to use it, the footgun, params / returns /
exceptions)
1. Subclass protocol, protected internal: NDArray.__array_wrap__, both __array_finalize__ overloads, _collapse,
   NanMax, NanMin, ReprClassName (and NDMatrix's / NDCharArray's overrides, CS0507), the two `__name__` constants.
   NewSubtypeZeroDim (the third parameter of __array_finalize__) is public: a protected internal member cannot take an
   internal type (CS0051). Documented footgun: NDSubclass.KindOf and the build-time weave still route only NDMatrix
   and NDCharArray, so np.* functions still return plain arrays for a consumer's subclass (the map's probe stays 11 of
   11); NanMax / NanMin are the exception - np.nanmax reaches them by plain virtual dispatch.
2. Constructors, public: NDMatrix(UnmanagedStorage), NDCharArray(UnmanagedStorage), NDMaskedArray(data, mask,
   fill_value), NDMaskedConstant() (footgun documented: NumPy's MaskedConstant() returns the singleton, a C#
   constructor builds a new object; library code recognises np.ma.masked by 5 type tests and 9 reference-identity
   tests, so a second instance is masked to the first kind only), NDPolyFitResult(storage, residuals, rank, sv,
   rcond), NDArray<T>(Shape, bool fillZeros, TensorEngine), NDArray(DType, Shape, bool fillZeros, TensorEngine). The
   last binds TensorEngine.Default for a null engine (the private sibling's rule and the owner's "null resolves to the
   default engine"; it threw a bare NullReferenceException from the storage read before).
3. Bridge surface, public: SizeBucketedBufferPool.BookForeign / ReturnForeign, ArraySlice.WrapShared<T> and
   WrapShared(NPTypeCode, ...), VStringAllocator with exactly NumPy's documented NpyString C API -
   acquire_allocator, release_allocator, load, pack, pack_null - and its 10 NumPy layout constants
   (SIZEOF_NPY_PACKED_STATIC_STRING, NPY_STRING_*, ...; DTypeOracleTests.Members_ExistUnderNumPysNames requires a
   public holder to expose them publicly), VStringArraySlice.Allocator, UnmanagedStorage.VStringSlice / StringSlice.
4. BitGenerator, public: NextUInt64 / NextUInt32 / Name / CreateFromSeed / GetStateCore / SetStateCore (abstract),
   NextDouble / UnitToDouble / NextFloat / NextRaw and the five Fill* (virtual), SeedWords32 / SeedWords64,
   FillDoubleFrom64 / FillUInt32From64 / FillFloatFrom64; its constructor is protected (the proper form for an
   abstract class). Generator (15 partial files), MT19937 (2), PCG64, PCG64DXSM, Philox and SFC64 are unsealed, their
   overrides public - NumPy subclasses Generator and PCG64 alike (checked on 2.4.2).
5. Shape: the raw fields dimensions / strides / offset / size / bufferSize and the four-argument view constructor
   Shape(dims, strides, offset, bufferSize) are public (footguns documented: the arrays are shared and must never be
   written; the constructor adopts and checks nothing). TensorEngine.Evaluate(NDExprProgram, ...), DotFunction1D,
   VdotFunction1D, VdotReadsFirstInPlace public virtual (DefaultEngine's overrides public). np.NDEnumerate,
   np.NDEnumerate<T>, np.NDIndex, np.NDIterator constructors public. UnmanagedStorage.OwnsData, IsDataAligned,
   ItemBytes public; WriteProtected / ExternalBase become public { get; internal set; } properties (every write is a
   plain assignment inside NumSharp).

Judgment calls (reported to the owner):
- NDExprProgram and NDEvaluateOptions are public TYPES with internal members (an opaque handle and bundle): three of
  NDExprProgram's fields have internal plan types (DirectOp, TextPlan, FMaxPlan), and NDEvaluateOptions' constructor
  would let callers pass keyword bundles the engine never validated. An engine overrides Evaluate(program, ...) to
  observe or reroute and calls base; one not derived from DefaultEngine cannot run a program itself.
- UnmanagedStorage._baseStorage stays internal: BaseStorage is its public read form, and a public setter is the
  use-after-free its docs rule out (owner: "Public read, internal write").
- Shape.Adopt stays internal: the owner's journey4 commit 278e986 (12:24, after this request) replaces the three Adopt
  factories with public Shape(dims, order, copy) / Shape(dims, strides, copy) constructors, NumPy's copy keyword
  being the public no-copy route. Opening Adopt here would ship an API already decided away and conflict when that
  refactor reaches strings.
- Not done, not decided: DTypeMeta's element slots (the map's group 5); the subclass dispatch stays closed-world.

VERIFIED (worktree off strings 34e48e1, Debug)
- The whole solution builds on net8.0 and net10.0 with 0 errors (tests, oracle, interop packages and their tests,
  analyzers, examples); no override outside Core.
- XML docs: a Core build with NoWarn replaced shows one cref warning on changed lines, fixed
  (System.NullReferenceException in NDPolyFitResult).
- Unit suite, CI filter: net10.0 23,085 passed, net8.0 23,084 passed; the 4 failures (Trapezoid_NaNSpacing_*,
  SumProd_NaNPayloads_AreNumPys, NaNPriority_MatchesNumPyBits conv complex128, ShortRows_SeveralNaNs_*) fail
  identically on the untouched strings tip 34e48e1 in the same Debug build.
…internal types and 5,939 internal members (was 493 / 6,091), 405 extensibility findings (was 470), 16 internals our packages still name (was 47; the ONNX Runtime, ML.NET and Tensors bridges none), the owner's decisions recorded, the subclass probe re-measured (still 11 of 11: the dispatch stays closed-world)

The tables and TSVs are regenerated by tools/internal-surface-map/SurfaceMap.cs from the Debug / net10.0 build of
strings 34e48e1 + 687fb94; every one of the 5,939 member lines checked to declare its member, 752 table links and 33
README links resolve.

README.md, rewritten for the new state:
- "Decisions (2026-11-07)": the owner's words per seam (subclass protocol "Make all protected internal"; subclass
  constructors "make subclasses all public and their constructors", NDMaskedConstant "Public"; bridge surface
  "Public"; bit generators "Public and the core ones also virtual and classes have clean proper constructors" ->
  "Unseal the classes"; Shape "all public"; engine hooks "all public"; iterator constructors "public"; storage state
  "public" -> "Public read, internal write") and what each became; the calls made where an instruction met a technical
  limit (NDExprProgram / NDEvaluateOptions opaque, _baseStorage behind BaseStorage, Shape.Adopt left to journey4's
  278e986, DTypeMeta's slots undecided).
- Scenario verdicts: engines open (Evaluate(program) and the 1-D dot hooks public; the program opaque), NDArray subclass
  "hooks open, class still lost" (probe re-run: 11 of 11; KindOf exact-tests NDMatrix / NDCharArray, the weave knows
  NumSharp.dll's subclasses only; np.nanmax / np.nanmin reach a consumer's override), NDMatrix / NDCharArray /
  NDMaskedArray constructible (a subclass of the first two is plain to np.* functions; NDMaskedConstant's second-instance
  behaviour: 5 type tests vs 9 identity tests), storage readable, bridges open (remaining 16 internals with their public
  routes), bit generators open (unsealed, public contract), user dtypes / polynomial bases / expression nodes still
  closed.
- "Still closed": the subclass dispatch, DTypeMeta's slots, NDPolyBase (needs a design), NDExpr nodes (by design),
  Shape.Adopt (replaced on journey4).
…ap (tools/internal-surface-map, docs/plans/internal-surface) and the extension seams it found, opened as the owner decided on 2026-11-07 (687fb94) - the NDArray subclass hooks protected internal, the NumPy subclasses' constructors public, the bridge surface public, BitGenerator a public contract with Generator and the five bit generators unsealed, TensorEngine's compiled-evaluation and 1-D dot hooks public, Shape's raw layout public, UnmanagedStorage's state publicly readable

Branch commits: 6257076 (the map before the changes), 687fb94 (the access changes, with every changed member's
docs), cdc422e (the map regenerated after them, the decisions recorded). strings had moved to e1619cc meanwhile
(the journey4 merge ff2bde6 - weaver and analyzer build-cost work - and a docs commit); no file overlaps, and the
merge result was rebuilt and its unit suite run before strings was fast-forwarded to it.
…PI and leak gates - exempt by name from the random-API oracle (NumPy keeps bitgen_t in C), measured directly by the leak catalogue on all six owners; random_surface.json 493 -> 571 members, the regenerated corpus byte-identical

Why
---
687fb94 (merged into strings as 23bd32a) made BitGenerator's engine contract public, with a protected
constructor, and unsealed Generator and the five engines, so a user's own bit generator can plug in.
That change was verified with the unit suite only. The Oracle project's surface gates then failed in 7
tests, all caused by the 78 newly public members (BitGenerator's 20, the engines' 58 overrides):
  - RandomApiSurfaceTests.CommittedInventory_EqualsTheReflectedSurface: random_surface.json stale (78 added).
  - RandomApiCoverageTests.G2: 78 members with no case and no exemption.
  - G3 parameter names: 72 methods with no NumPy counterpart and no allow-list entry.
  - G3 parameters, G4, G5: KeyNotFoundException on 'BitGenerator.CreateFromSeed(ISeedSequence)'.
  - LeakSurfaceCoverageTests.EveryInventoryMember_IsLeakAudited: 114 members unmeasured (19 methods x 6 owners;
    the leak surface flattens BitGenerator's members onto every engine).

None of these members has a Python counterpart. NumPy keeps the same contract in C: bitgen_t's next_uint64 /
next_uint32 / next_double / next_raw, which Python reaches only as the function pointers of
BitGenerator.ctypes / .cffi, plus the C-side helpers around them (next_float, uint64_to_double,
generate_state as the constructors read it, type(self)(seed=child) in spawn). So no oracle case can record
NumPy running them. Every Generator and RandomState stream case already replays NumPy's draws through them
on every engine.

What
----
- test/NumSharp.Tests.Oracle/Fuzz/RandomApi/RandomApiCoverageTests.cs:
  - EngineContractMembers: the 20 contract member names (19 methods + Name), each with its reason.
    It exempts every member of that name that BitGenerator or one of the five engines declares; an engine's
    override IS the contract member, so it is not listed per engine. A member of an unrelated type with the
    same name is never excused, and a NEW public bit-generator member still fails G2 until it has a case.
  - IsEngineContract(m) and ExemptionOf(m): the single lookup G2, G3 (parameters), G4 and G5 now share,
    so OverloadExemptions and the contract table cannot disagree across gates. The parameter-name gate
    accepts a contract member as NumSharp-only.
  - Self-retirement:
    - G2 fails on a contract name BitGenerator no longer declares.
    - The parameter-name gate fails on a contract name NumPy's signature table turns out to have (on
      BitGenerator or any engine).
    - An exempt member that gains a case fails G2, as before.
- test/oracle/gen_random_oracle.py: the generator's mirror, ENGINE_CONTRACT plus make_engine_contract_family.
  It registers a family per (BitGenerator or engine, contract name) that exempt(...)s every overload the
  type declares, and refuses (SystemExit) a contract name BitGenerator no longer declares.
- test/oracle/random_surface.json: regenerated with NUMSHARP_WRITE_RANDOM_SURFACE=1 (493 -> 571 members:
  BitGenerator +20, MT19937 +14, PCG64 / PCG64DXSM / Philox / SFC64 +11 each).
- test/NumSharp.Tests.Oracle/Fuzz/LeakCatalogue.cs: AddBitGeneratorContract, called from AddObjects.
  It adds 114 entries: the 19 contract methods on each of the six owners (BitGenerator through a base-typed
  PCG64, then each engine on itself).
  - Each entry builds its own engine and a managed destination, pinned for the pointer fills, so a
    re-execution sees the same state.
  - Fill counts are odd.
  - FillDoubleFrom64 (301 draws) and FillFloatFrom64 (601 draws) cross a chunk boundary.
  - FillUInt32From64 serves a pending half before whole words and an odd tail.
  - Measured: zero pool traffic.
- docs/plans/random-oracle-coverage.md:
  - §7: an engine-contract exemption row.
  - The capsule / cffi / ctypes row now says NumSharp exposes the contract itself as C# members.
  - §9: a dated state-log entry.

Verification (scratch worktree at strings 23bd32a plus this change, net10.0 Debug)
--------------------------------------------------------------------------------
- python test/oracle/gen_random_oracle.py (NumPy 2.4.2, LP64 sub-run through WSL ~/np242): accepts the
  571-member inventory. Every tier is byte-identical to the committed corpus (random_api 9,820,
  random_api_host 15,795, random_api_mvn 730, random_api_lp64 234), and random_numpy_signatures.json is
  unchanged (130 members).
- RandomApi tests: 12 passed, 2 skipped (RandomApiMultivariateNormal and RandomApiSoak skip by design).
- Leak gates: EveryInventoryMember_IsLeakAudited,
  Catalogue_EveryEntry_LeavesNoUndisposedIntermediates and Catalogue_EveryEntry_LeavesTheFixturesIntact
  all passed.
- Full Oracle project, unfiltered (6 m 26 s): 434 tests, 386 passed, 39 skipped, 9 failed.
  - The 9 failures are the [OpenBugs] known-gap corpora, which fail by design while their gaps are open and
    which CI's TestCategory!=OpenBugs filter excludes: MaSurface 6,904 cases / 19 gaps, Matrix 64 / 2,
    MatrixOperand 330 / 5, MatrixOperandHost 10 / 1, Relayout 3 / 1, Surface 192 / 14, ViewFlags 225 / 13,
    ShapeArgs 24 / 1, ArrayArgs 17 / 3.
  - The same counts come out of the untouched release-0.80.0 tip efc91c6 (strings e1619cc's content).
- coverage/generate_coverage.py (what the docs workflow runs): headline 533 / 560, oracle-verified 537 / 560,
  0 available headline APIs without contracts; its unit tests pass 91 / 91.
…p in 0.80.0 - the internal surface map (6257076, cdc422e), the access changes the owner decided on 2026-11-07 (687fb94, merged into strings as 23bd32a) and the oracle gates' follow-up for the now-public BitGenerator contract (130a0f0)

The owner asked for the extension seams commits ("commit them to release-0.80.0"). Before this merge,
strings had 5 commits the release lacked, all from this work:
- 6257076: docs(plans), the internal surface map (tools/internal-surface-map/SurfaceMap.cs,
  docs/plans/internal-surface/).
- 687fb94: feat(api), the extension seams opened as the owner decided:
  - the NDArray subclass hooks are protected internal;
  - the NumPy subclasses' constructors are public;
  - the bridge surface is public: foreign-byte booking, WrapShared, the StringDType allocator's NpyString
    API, and Shape's raw layout;
  - BitGenerator is a public contract, with Generator and the five bit generators unsealed;
  - TensorEngine's compiled-evaluation and 1-D dot hooks are public;
  - UnmanagedStorage's state is publicly readable.
- cdc422e: docs(plans), the map after the changes.
- 23bd32a: the merge of the three into strings.
- 130a0f0: test(oracle). The random-API oracle exempts the engine contract by name (NumPy keeps bitgen_t
  in C), and the leak catalogue measures it on all six owners. random_surface.json goes from 493 to 571
  members; the regenerated corpus is byte-identical.

The only file both sides changed is src/NumSharp.Core/Backends/TensorEngine.LinearAlgebra.cs, and git merged it
by hunks with no conflict:
- the release (1495928) changed a "0.71.0" mention to "0.80.0" in the class remarks;
- strings (687fb94) made VdotFunction1D, VdotReadsFirstInPlace and DotFunction1D public virtual and
  documented them.
None of the merged lines names 0.71.0 or 0.80.0-beta1, so the release's version sweep has nothing new to cover.

docs/releases/RELEASE_0.80.0.md does not describe these API changes yet. That is left to the notes'
owner (other sessions edit the notes).

Verification on this merge's tree (scratch worktree)
-----------------------------------------------------
- Tree: the merge differs from strings 130a0f0 exactly in the 30 files strings...release-0.80.0 changes
  (version, notes, platform skips, workflow).
- Hygiene: no conflict markers. git diff --check is clean except data:
  - docs/plans/internal-surface/*.tsv rows end in empty tab-separated fields, as
    benchmark/openblas/openblas_results.tsv already does;
  - the generated tables.md ends with one blank line.
- Release build of the solution: 0 errors. Debug: 0 errors.
- NumSharp.Tests with CI's filter (TestCategory!=OpenBugs&TestCategory!=HighMemory), Release:
  - net10.0: 23,089 passed, 0 failed, 280 skipped;
  - net8.0: 23,088 passed, 0 failed, 280 skipped.
  - In Debug the same suite fails only the 4 NaN-bit tests that fail identically in Debug on the untouched
    strings tip: Trapezoid_NaNSpacing_IsTheProductsFirstOperand, SumProd_NaNPayloads_AreNumPys,
    NaNPriority_MatchesNumPyBits (conv complex128) and ShortRows_SeveralNaNs_KeepTheRunningSumsNaN.
- NumSharp.Tests.Oracle, unfiltered, Release, net10.0 (7 m 11 s): 434 tests, 388 passed, 37 skipped,
  9 failed.
  - The 9 failures are the [OpenBugs] known-gap corpora, which CI's filter excludes. Their counts are the
    same as on the untouched release tip efc91c6: MaSurface 6,904 cases / 19 gaps, Matrix 64 / 2,
    MatrixOperand 330 / 5, MatrixOperandHost 10 / 1, Relayout 3 / 1, Surface 192 / 14, ViewFlags 225 / 13,
    ShapeArgs 24 / 1, ArrayArgs 17 / 3.
  - The surface gates that 687fb94 had turned red pass here: random-API G1 to G5 and the leak
    completeness and catalogue gates.
…s, strides, copy = true) replace the internal Shape.Adopt factories on release-0.80.0 - NumPy's copy keyword is the one named opt-out of bug 2d's copy; the default still copies, the release's 514 Core / OpenBLAS sites call new Shape(arr, copy: false) (journey4 278e986, re-derived on this tree)

WHY
---
The owner, 2026-11-07: "Shape.Adopt, why do we even use that.. dont we have an existing method to do that?", then "Why not
go back to constructors and make them public? also passing dims should be cloned or not", then, after journey4's
278e986, "Commit to release-0.80.0". The reasoning and the decision page are in 278e986's message; in short:
- Adopt came with 5c9d792 (bug 2d), when the public long[] constructors started copying: before it the public
  Shape(params long[]) kept the caller's array, which let `s[0] = 100` turn np.zeros(s) into (100, 4) over 12 elements.
- One signature either copies or keeps, and C# cannot overload on accessibility, so the keeping route needs its own
  signature. The owner chose NumPy's copy keyword on public constructors, copying by default.
- Measured on d612f99: always copying adds +184 B per np.sum(m, 0) / np.mean(m, 1) call (+12%) and +224 B to a 3-D axis
  sum; never copying reopens bug 2d (z (100, 4) over 12 elements, z[99, 3] at element 399).

A cherry-pick of 278e986 conflicts in 97 files (strings rewrote most of the same lines) and would leave the ~156 sites
only strings has calling a removed method, so the change is re-derived here: the hand-written parts of 278e986
(Shape.cs, the test file, CLAUDE.md, memory-views.md, the bug 2d plan) applied with git apply --3way, cleanly, and the
same rewrite over every call site of this tree.

WHAT
----
- View/Shape.cs (as in 278e986): Shape(long[] dims, char order) -> Shape(long[] dims, char order = 'C', bool copy = true);
  Shape(long[] dims, long[] strides) -> Shape(long[] dims, long[] strides, bool copy = true); copy: false keeps the
  array (null -> 0-d), the checks run in both modes before any copy; internal Shape.Adopt(long[]),
  Adopt(long[], char), Adopt(long[], long[]) removed (no [Obsolete]); both constructors document the copy: false
  hazard. Unchanged and copying: Shape(params long[]), Shape(params ReadOnlySpan<long>), Shape(long[], long[], Shape),
  the long[] -> Shape conversions.
- 514 call sites in src (501 in Core, 13 in the OpenBLAS friend package), 17 in 6 test files and 3 in 2 benchmark
  probe scripts: Shape.Adopt(x) -> new Shape(x, copy: false), Shape.Adopt(x, order) -> new Shape(x, order,
  copy: false), Shape.Adopt(d, s) -> new Shape(d, s, copy: false), NumSharp.Shape.Adopt -> new NumSharp.Shape, the bare
  Adopt(outputDims, strides) of Shape.StridePerm.ReductionOutputShape by hand, and the cref of
  NDArray.FunctionBase.OpenMeshShape's <returns> (strings only) re-pointed at Shape(long[], char, bool).
- Tests: Shape.CallerArrays.Tests.cs as in 278e986 (13 -> 16: CopyFalse_KeepsTheArray_TheDefaultCopies,
  CopyFalse_BuildsTheSameShapeAsTheCopy, CopyParameter_DefaultsToTrue, CopyFalse_ArgumentChecks_MatchTheCopyingForm;
  four planted defects each killed on journey4).
- Docs: .claude/CLAUDE.md (= AGENTS.md) and .claude/rules/memory-views.md state the rule with copy: false;
  docs/plans/view-shape-open-bugs.md has the dated follow-up under bug 2d; docs/plans/internal-surface/README.md (the
  surface map that 503dca4 brought, which already expected this refactor) says Shape.Adopt is gone, its OpenBLAS row
  names the public route the 13 sites now call, and its seam row is done (strings still has Shape.Adopt). The map's
  generated TSVs stay the snapshot they are.

GATES
-----
Built first on efc91c6; release-0.80.0 then moved to 503dca4 (the extension-seams merge, 4 of these files touched),
so the change was re-derived on 503dca4 the same way. Detached worktrees, the OpenBLAS binaries staged:
- Solution rebuild (-t:Rebuild, Debug, net8.0 + net10.0): 0 errors.
- NumSharp.Tests, CI filter, this change: net10.0 23,360 passed, 8 skipped, 4 failed; net8.0 23,359 passed, 8 skipped,
  4 failed. Clean 503dca4 without it: net10.0 23,357 passed, net8.0 23,356 passed, the same 4 failed. The difference
  is exactly the 3 new tests; the 4 are the release tip's (on efc91c6 too): Trapezoid_NaNSpacing_IsTheProductsFirstOperand,
  SumProd_NaNPayloads_AreNumPys, NaNPriority_MatchesNumPyBits (conv, complex128) and
  ShortRows_SeveralNaNs_KeepTheRunningSumsNaN. Not investigated here.

BREAKING: Shape(long[], char) and Shape(long[], long[]) became Shape(long[], char = 'C', bool = true) and
Shape(long[], long[], bool = true): source-compatible, binary-breaking. New public opt-out: copy: false.
…hape - 150 copies removed at 128 call sites (96 clones of a shape's own array at 88, 17 clones of locals nothing writes after, 23 copying-constructor calls, 14 of them after a clone); the audit of every no-copy site found nothing kept by mistake - np.nanvar(m, 0) -272 B a call, np.lexsort -128 B, np.argsort -72 B, np.cumsum / np.reciprocal / np.left_shift -40 B, np.sort / np.partition -32 B

WHY
---
The owner, after f9aa136: "Then do a second pass ensuring we didnt copy or not copy by mistake". f9aa136 rewrote every
Shape.Adopt call mechanically, so it carried each site's earlier ownership decision over unchanged; this pass checks
those decisions in both directions over the whole release tree.

THE AUDIT (a Roslyn syntax tool, run on this tree; not committed)
-------------------------------------------------------------------
1. The invariant sharing rests on: nothing writes into a shape's dimensions / strides after the shape is built.
   Every element write, Array.Copy / Fill / Reverse / Sort destination, ref / out hand-off, fixed pointer and local
   alias of a Shape's arrays was listed, and each hand-off followed into the callee bodies (recursively, by name and
   arity). The only writes are construction-time fills of a fresh array (ComputeContiguousStrides, StridesForPerm,
   Shape(int[]) filling its own array) and ResolveReturnShape's local Shape.Empty, filled before Clean() rebuilds it.
   The 47 pinned pointers and the arrays handed to kernels are only read in C# (IL kernels and emitted builders are
   read-only by design; TakeAlongAxis's emitted kernel was checked by hand: its stores go to its own coordinate
   buffer, the output and outBadIdx). The members that hand a shape's array out return NDArray.strides (a new
   byte-strides array) or keep it internal and read-only.
2. Not copying by mistake: all 514 copy: false sites and the 92 internal view-constructor sites. Each kept array was
   classified by origin (fresh, a shape's own, a helper's fresh result, a local, a parameter, a member), every later
   write or hand-off of a kept local checked against the construction (writes after it, reuse across loop iterations,
   lambda capture), and every kept parameter traced through its callers to its origins. Result: no array is kept from
   a public parameter (the one trace that reaches one, NDIterRef.AdvancedNew's iterShape, takes the
   (long[])iterShape.Clone() branch: only np.nditer passes shapeValidated: true, with a fresh array) and no kept array
   is written afterwards (take_along_axis's kept dims are read by its IL kernel; RandomBroadcast.OutputDims returns a
   fresh array its three callers discard). The .npy header's shape, np.vectorize's core shapes, the reduce
   iterator's ResultDims / ResultStrides (fresh per build) and NDPolyCalc.Moved's arrays are all fresh.
3. Copying by mistake: an [Obsolete] marker on every copying member (the four long[] constructors, IEnumerable<long>,
   int[], and the four conversions) for one Core build listed 111 copying sites besides copy: false. Fixed here:
   - A: 88 sites passed (long[])X.dimensions.Clone() / (long[])X.strides.Clone() of a Shape X into a constructor that
     keeps it (96 arguments; the nine strides receivers are Shapes, they also read .offset / .bufferSize): the shape
     now shares X's array, as every view and elementwise result already does.
   - C: 17 sites cloned a local that nothing writes after the construction, every one of fresh or shape-owned origin
     (BroadcastAll, ShapeOf and BroadcastWhereOperands return fresh arrays): the shape takes the local.
   - B: 22 lines (23 calls) used the copying constructor on an array Core owns: 16 on a shape's own array, 14 of them
     after a clone (two copies each: NDStringCopy, NDTextNumericReduction, NDVStringMove, NDVStringReduction x2,
     UnmanagedStorage.Mapping x2, NDArray.Indexing.Strings x7) and 2 directly (NDStringMethods and
     NDVStringMethods.Case on a.Shape.dimensions); new Shape(new[] { count }) (NDVStringMove); dims arrays Core had just filled
     (UnmanagedStorage.Assign.Strings, UnmanagedStorage.Strings, NDArray.Multiarray's empty object grid,
     NDMaskedArray.DiffEdge's copy of d.shape) and the two error-text shapes of NDPolyEval.FromRoots: they say
     copy: false.
   Kept on purpose: the 18 clones of a parameter (the caller owns that array and may write it later),
   UnmanagedStorage.ChooseCore's clone (its local goes to an IL kernel the tool cannot read), Coercion.Text.Build's
   copy (its internal FromValues callers are not traced), every public entry point that copies a caller's array (the
   shape setter, the long[] overloads of np.zeros / empty / ones / full, reshape, resize and the random sizes, the
   incrementors' public constructors, NDListArray.resize), and the 29 int[] conversions, which allocate anyway.

COST
----
Bytes per call, Release net10.0, f9aa136 against this change (same script, both built from detached worktrees):
np.nanvar(m, 0) 9504 -> 9232, np.lexsort((k1, k2)) 7000 -> 6872, np.argsort(m, 1) 1728 -> 1656, np.cumsum(m, 1)
792 -> 752, np.reciprocal(int 10x10) 608 -> 568, np.left_shift(int, 1) 1232 -> 1192, np.sort(m, 1) 1328 -> 1296,
np.partition(m, 3, 1) 1216 -> 1184; np.searchsorted, np.digitize and np.argwhere unchanged (their edited lines are
off these inputs' paths).

GATES
-----
On f9aa136 + this change, the OpenBLAS binaries staged:
- Solution build (Debug, net8.0 + net10.0): 0 errors.
- NumSharp.Tests, CI filter: net10.0 23,360 passed, 8 skipped, 4 failed; net8.0 23,359 passed, 8 skipped, 4 failed:
  the same counts as f9aa136, and the same 4 NaN tests the release tip already fails.
- The audit re-run on this tree: 537 copy: false sites (514 + the 23 calls of B), 0 clones of a shape's array left in
  front of a kept array, the same two reviewed flags, no new public parameter reaching a kept array.
…keyword - Shape(long[], char = 'C', bool copy = true) and Shape(long[], long[], bool copy = true) replace the two-parameter constructors and copy: false keeps an array nothing will write into again (f9aa136)

The Breaking Changes bullet on Shape immutability and copied caller arrays (bd26afb, b88bcb8, 95fd514, 5c9d792)
now also says what f9aa136 changed for users: the two constructors gained NumPy's copy keyword with a copying
default, the two-parameter forms are gone (a compiled caller recompiles; source-compatible), and copy: false is
the named opt-out. ffeb592 (the copies NumSharp no longer makes of its own arrays) changes no public behaviour and
is not cited. The Overview tallies (cited commits) are not recounted here: they were already behind the notes' lines
before this commit and are recounted before the tag.
… = true) and Shape(dims, strides, copy = true) replace the internal Shape.Adopt factories on strings too, resolved as release-0.80.0's re-derivation f9aa136: every Shape.Adopt call of strings becomes new Shape(..., copy: false)

The owner, 2026-11-07: "check if journey4 has changes that strings missing so bring them to strings", then "Check
again and push". journey4 moved one commit past what strings had merged (ff2bde6, d612f99):
- 278e986 refactor(shape)!: NumPy's copy keyword on the two long[] Shape constructors (copying by default) replaces
  the internal Shape.Adopt(long[]) / Adopt(long[], char) / Adopt(long[], long[]) factories, and Core's call sites say
  new Shape(arr, copy: false). The reasoning and the owner's decision page are in its message.

Resolution
- A plain merge conflicts in 96 files: strings rewrote most of the lines journey4's mechanical rewrite touched, and
  strings has call sites journey4 never had. release-0.80.0 met the same conflict and re-derived the change on its own
  tree as f9aa136 (278e986's hand-written parts applied with git apply --3way, then the same rewrite over every call
  site of that tree). That tree is strings 130a0f0 plus release-only commits, so this merge's tree is strings 130a0f0
  with f9aa136's diff applied (git merge-tree --merge-base=503dca41 strings f9aa136): no conflict, the same 183
  files f9aa136 changes.
- Byte-identical to f9aa136: src/NumSharp.Core/View/Shape.cs (the two constructors, the removed factories, the
  copy: false hazard in their docs), .claude/CLAUDE.md, .claude/rules/memory-views.md. Byte-identical to 278e986:
  test/NumSharp.Tests/View/Shape.CallerArrays.Tests.cs (16 tests) and docs/plans/view-shape-open-bugs.md.
- 560 Adopt mentions in strings' src / test / tools / benchmark / examples become 24, the same 24 release-0.80.0 has,
  none of them Shape.Adopt: the ONNX Runtime bridge's OrtValueBooking.Adopt and analyzer test fixtures with a method
  named Adopt.

Checks
- Replaying 278e986 onto this tree (git merge-tree --merge-base=d612f996) changes nothing outside its 96 conflicted
  files, so every non-conflicting part of journey4's commit is here. Its 136 conflict blocks were read by a script:
  no block's strings side still calls Shape.Adopt, and each journey4 side is journey4's older code in a region strings
  rewrote since (NDMatrixBoundary scopes, the old reduction and nonzero paths, np.where_internal, DropInsertedAxis),
  the regions strings' earlier journey4 merges (f87de21, ff2bde6) also resolved to strings.
- The tree differs from release-0.80.0 (497c429) in 99 files, every one of them changed by a release-only commit
  (130a0f0..503dca4: the 0.80.0 renames, the Linux / macOS pins, the notes) or by ffeb592 / 497c429.
- Release build and the CI suites: see the release-0.80.0 merge of this commit, whose tree adds only release-0.80.0's
  own commits to this one.

Not brought here: release-0.80.0's ffeb592 (perf(shape): 150 copies of arrays Core owns removed at 128 sites, the
no-copy audit) is release-only work, not journey4's.
278e986 reaches strings and the release by history; no content change - the release has had it since f9aa136 (+ ffeb592)

The owner, 2026-11-07: "Check again and push". The re-check at 18:32 found strings..journey4 = 1 (278e986, the
Shape(dims, ..., copy) constructors replacing Shape.Adopt) and release-0.80.0..strings = 0. strings took 278e986 in
f10aae1, resolved as this branch's own re-derivation f9aa136, so the merged strings tree is strings 130a0f0 with
f9aa136's diff and no Shape.Adopt call left.

This merge brings that strings commit here. Its tree is this branch's tree unchanged (e397b9e):
- 55 files conflict, each where ffeb592 (the no-copy audit, release-only) rewrote a line f9aa136 had already
  rewritten, e.g. new Shape(result.Shape.dimensions, 'F', copy: false) here against strings' new Shape((long[])
  result.Shape.dimensions.Clone(), 'F', copy: false). Every conflict keeps this branch's line.
- git merge-tree -X ours (this side only inside conflicting hunks, everything else merged) gives exactly e397b9e, so
  strings holds nothing this branch lacks.

After it: release-0.80.0..strings = 0, strings..journey4 = 0, release-0.80.0..journey4 = 0.
…er, so GitHub can build its merge ref and run CI; master's 52 commits outside this branch bring no content - all 33 conflicts keep this branch's side

PR #633 (release-0.80.0 -> master) showed mergeable = CONFLICTING (33 files), and GitHub runs no pull_request
workflow while a PR conflicts, so the release never had a CI run. The owner, 2026-11-07: "Check again and push.
Confirm cicd is stable and PR ready for merge".

What master has outside this branch (4c24db3, merge base 90311b6, 52 commits)
- 50 non-merge commits, every one with a copy on this branch (same author, author date and subject): journey4's
  DocFX work as it stood before the 2026-10-02 rebase (the Fundamentals / Advanced / quickstart / absolute-basics
  pages, the TOC state script, the em-dash normalization), the 2026-09-25..27 np.random work, the interop hide /
  release commits, the 10-14 docs fixes and PR #632's 4ba7ed7 (campersau: the ownership analyzer walks each type
  once), which journey4 carries with the review's fixes (c544595..7af67dc).
- The two merges that brought them: 3c1f376 (journey4-docfx, 2026-10-15) and 4c24db3 (PR #632, 2026-10-28).

Resolution
- 13 add/add conflicts (docs pages both sides added) and 20 content conflicts (19 docs / docfx files and
  tools/NumSharp.Build.Analyzer/OwnershipModel.cs): every one keeps this branch's side, which holds the later text.
- git merge-tree -X ours (this side only inside conflicting hunks, master's other changes merged) gives this branch's
  tree byte for byte (e397b9e): every change master makes outside a conflict is already here, so the merge changes
  no file.
…kes the CPU's sign (HostCpu), libm bits are pinned to the win-amd64 CRT (HostLibm), NaN-priority and region-rule pins run on x86 / X86_V3, D23's zdotu NaN is x86's everywhere, macOS's 8-byte long double and AVX-512's plan verdicts are stated; the workflow runs every suite after a red one and keeps OpenBugs out of the pythonnet runs

WHY
The owner, 2026-11-07: "Check again and push. Confirm cicd is stable and PR ready for merge". release-0.80.0 had never
run CI: PR #633 conflicted with master until 55ba806. Its first run (Build and Release 42392670928, 55ba806) was red
on all three OSes, though the Release build passes everything locally on Windows (unit 23,091 / 23,092 on net8.0 /
net10.0, oracle 386 + 35 skipped on both, analyzer 488 on both, i9-13900K, AVX2). The release carries a month of strings
tests that only ever ran on win-amd64, and their expectations hold win-amd64 facts no other host shares:
- ubuntu, Test (net8.0), 7 failures: UCRT libm bits under glibc - float32 atan2f (FindingValues_AreNumPys,
  ScalarAndEvaluateRoutes_AreNumPys, Float32_Arctan2_HashesToNumPys), acosh(2) ...166 vs glibc ...168
  (Real_Arccosh_Float64_DomainAndEdges, red on master since image 20261021), and the digests of the log1p / expm1 /
  logaddexp models, which run over the HOST's log and exp (Log1p_Grids_DigestEqualNumPy,
  Expm1_Float64_ExactOutsideTheBand_WithinTwoUlpInside, LogAddExp_CancellationAndGrid_AreNumPysBits).
- macOS (Apple silicon), Test (net8.0), 44 failures in 37 tests: the x86 default NaN (negative) pinned where arm64
  generates the positive one; win-amd64 NaN-payload priorities and AVX2 region rules; Apple libm's pow / atan2f; and
  macOS's 8-byte C long double. cad16ea7 / 2402b23 had left macOS unreplayed.
- windows (an AVX-512 runner), Test (net8.0), 1 failure: VectorPlan_SameWidth_DecidesLaneAndRefusals.
- interop-test, all three OSes: pythonnet parity's two [TestCategory("OpenBugs")] tests (the OpenBLAS backend's
  view-copying vdot and its own loops' NaN order, pinned 2026-11-05), because those two steps' filters were the only
  ones without TestCategory!=OpenBugs. Everything else in interop-test passed (ORT floor / current / newest, ML.NET,
  Tensors, ParquetNet, the consumer scripts).
- Deploy Docs (the inventories) and Example: NumSharp with Python passed on every OS.
Every `test` leg stopped at Test (net8.0), so the net10.0 suite, the oracle and the analyzer tests never ran.

WHAT (tests: nothing in src changes; the rules are the suite's own)
- Generated NaNs take the CPU's sign. HostCpu (the 2026-10-02 helper for exactly this) gains IsX86V3 (NumPy's
  X86_V3 target, the oracle's NaNOperandOrderHost), the float32 default NaN (DefaultNaN32Bits / Hex), the big-endian
  word forms the reduction tests print (DefaultNaN64Word / 32Word / 16Word) and WithHostDefaultNaN (a slot-wise splice
  of a win-amd64 tobytes() dump, documented with its footgun: only for dumps whose inputs hold no NaN with x86's exact
  default bits). Every expected NaN born of an invalid operation (inf*0, inf-inf, sqrt(-inf), 0*inf) uses them, and
  every propagated NaN keeps its literal bits: np.average over the empty tuple, np.quantile's float16 index past the
  end, np.polyval's first product, the complex mean / nanmean / average divisions (np.mean, np.nanmean, np.sum.Identity),
  np.prod's float16 cast loop, the float16 dot's inf*0, the float16 / float64 reversed sums (np.sum.ReversedBroadcast),
  np.power's sqrt(-inf) cells, CPython's (inf+0j)**2, the normed Hermite kernel's special points.
- PolynomialWeightTests' NumPy dumps: no input of the table holds a NaN, so every NaN slot is generated - and some are
  negated on the way (herm's -x**2 turns x86's negative NaN into the positive one its dump shows, so arm64 writes the
  negative one). Off x86 a NaN slot is therefore a token (NaN where NumPy has NaN), every other slot keeps its exact or
  2-ULP check. The fused-tree and matrix-scaling route-agreement checks (FusedTree_AgreesWithTheStatements...,
  NDMatrix Scaling_EveryRoute...) take the same rule: on emulated arm64 their two routes generate their NaNs at
  different operations and disagree on the sign only (herme(complex128): 8 slots, f8ff vs f87f; every value equal).
- D23's model is x86 on every host (NDDot.NaN.cs: "The models write x86's default NaN explicitly"), so the zdotu
  positions of ComplexDot_IsZdotuOrThePlainLoop / ComplexNonFiniteProduct_WithoutBackend_MatchesZdotu expect
  000000000000f8ff everywhere; their plain-loop positions (a one-term factor, stride -16) are plain arithmetic and keep
  the host's NaN. Those two tests asserted the host's NaN for both, which only x86 satisfied.
- NaN priority is a win-amd64 contract (kernels.md: "a compiler artifact, pinned to win-amd64"): asserted on x86 only
  where the product still leaves the order to RyuJIT - SumProd_NaNPayloads_AreNumPys, ShortRows_SeveralNaNs...,
  Trapezoid_NaNSpacing... (three of the four tests that already fail in an x86 DEBUG build) and NaNPriority_MatchesNumPyBits'
  complex128 rows. The region rule (blocks of two 32-byte vectors) is asserted on X86_V3 hosts, like the oracle's
  nan_order tier: NaNOperandOrderTests' three region tests, Sum_BroadcastColumn...
- libm bits are the win-amd64 CRT's, through the existing HostLibm (IsPinnedHost / Matches / Describe), which gains
  RequirePinnedHost for tests whose every assertion is a digest: the float32 atan2f values and digests and the float16
  arctan2 digest (the float16 values and the log1p / expm1 digests held on glibc and Apple's libm), the log1p grids, the
  expm1 digest and exp-based edges, the logaddexp grid digests (the pinned pairs held everywhere), arccosh through the
  class's own ShouldMatchCrt, and the pow pins: single values through HostLibm.Matches (exact here, within 4 ULP
  elsewhere: np.linalg.norm's scalar pow, CPython's float pow), scalarmath's float64 square and np.power's three
  pow-cell tests through IsPinnedHost / RequireWindowsCrtPow (Apple's pow rounds 757.7617475757002 ** 2 to ...c1).
- DTypeOracleTests: macOS replays the lp64 (Linux) records with an 8-byte long double, so a sub-array of Linux's
  16-byte width ('2f16', '2c32', VoidDType there) is NumPy's "data type 'f16' not understood" on that host, as in
  NumSharp: the LongDoubleClasses branch's rule, applied to the AbsentClasses branch (NamesLinuxLongDoubleWidth).
- NDEvaluateVectorTests.VectorPlan_SameWidth_DecidesLaneAndRefusals: only a tree with bool I/O consults
  FusedBoolLanesAvailable (false on a 512-bit host); the four bool-free trees vectorize on every SIMD host, as the
  uniform-plan test next to it already states. The windows-latest runner planned "where(int64 cmp, float64, 0)" vector.

WORKFLOW (.github/workflows/build-and-release.yml)
- The `test` job's build steps get ids (build_tests, build_oracle, build_analyzer) and every test step runs on
  `!cancelled() && <its build> == 'success'`, so a red unit run no longer hides the oracle and analyzer results
  (the merged-job rule the 2026-09-23 layout already follows in interop-test).
- The four pythonnet steps exclude OpenBugs like every other test step; the comment that said the suite carries none
  now names the two it does.

VERIFIED (this tree, Release)
- Windows x64 (i9-13900K, AVX2): NumSharp.Tests 23,091 / 23,092 passed, 0 failed (net8.0 / net10.0), 280 skipped as
  before - no gate fires on the pinned host.
- Linux x64 (WSL Ubuntu 24.04, glibc; the Windows-built DLLs, CI's non-Windows filter): NumSharp.Tests 22,964 / 22,965
  passed, 0 failed (net8.0 / net10.0).
- Analyzer tests under WSL: 488 / 488 passed (net8.0 / net10.0).
- linux/arm64 under QEMU (neoverse-n1, .NET 10.0.14, the CPU's NaN rules exactly; not Apple's libm): the 37 tests
  macOS failed: 25 passed, 12 Inconclusive by these gates, 0 failed. The whole suite: 22,839 passed, 2 failed -
  T2_NonZero_Is_Much_Slower_Than_NumPy (a wall-clock limit, an emulation artifact as recorded before) and
  LimitQuirks_AreCPythons (OverflowException where an int.MaxValue-wide pad should raise OutOfMemoryException: under
  QEMU only; it passes on x64 and on macOS net8.0). Before the last three edits the run also failed
  FusedTree_AgreesWithTheStatements..., Scaling_EveryRoute... and NaNPriority's conv complex128 row.
- Not reproducible here: Apple's libm (macOS) and an AVX-512 host (the windows-latest runner): the next CI run.
- The workflow parses (PyYAML): 6 jobs; the six test steps carry their conditions; the pythonnet filters are
  TestCategory!=PythonEcosystem&TestCategory!=OpenBugs / TestCategory=PythonEcosystem&TestCategory!=OpenBugs.

FOUND, NOT FIXED HERE (the owner's)
- D23 gap off x86: complex128 dot routes still leave part of their NaN order to the JIT - .NET 8's arm64 JIT on the
  np.dot-against-stride-0 rows, .NET 10's on a one-term convolution - where NDDot.NaN.cs promises x86's bits on every host.
- NaN priority by RyuJIT order: np.sum / np.prod's reduce loops, the short-row sum and trapezoid's product keep the JIT's
  NaN (they fail in an x86 Debug build too), against kernels.md's "encode NaN priority explicitly".
- Route NaN signs: np.evaluate's fused herme(complex128) and the statement route, and NDMatrix's complex scaling routes,
  generate NaNs at different operations, so they agree on x86 only.
…closes lifetime gap LT31 (every {p}val), a %-format pad past .NET's longest string is MemoryError on arm64 too, the string oracle reads a NaN as a token off x86, arctan2 within 2 ULP off the reference host is the host's libm (H2), and the pythonnet cost test tolerates a shared runner

WHY
The owner, 2026-11-07: "Check again and push. Confirm cicd is stable and PR ready for merge". The second CI run of
release-0.80.0 (Build and Release 42399200721, b282895) was green on the Windows leg (an AVX-512 runner, the oracle
included) and on the Windows and Ubuntu interop legs. What stayed red:
- ubuntu, Oracle (net10.0): LifetimeOracleTests.Lifetime - lifetime/chebval/all/float64/0, rep 7: the result's bytes
  differed from the reference from byte 1,669,632 of 2,097,152.
- macOS (Apple silicon, macos-26-arm64), Oracle (net10.0): the same gate - polyval rep 7 (from byte 1,485,504) and
  chebval reps 4 and 2 (from byte 701,120). net8.0 passed the gate on both hosts, Windows on both frameworks.
- macOS, Oracle (net8.0 and net10.0), 10 tests: TextSurface, CharListArrayMembers, QuantilePrecisionAndOut,
  StringDTypeSurface (the string oracle: 466 cases whose only difference is a NaN's sign bit), Specials, Logic,
  WeakScalar, OutWhere, MatrixOperand, Relayout (float32 arctan2: Apple's libm puts +-pi and +-pi/2 one ULP from
  ucrtbase - 11 + 11 + 14 + 76 + 3 + 3 cases).
- macOS, Test (net10.0): PercentFormatTests.LimitQuirks_AreCPythons - '%9223372036854775807d' % 1 threw
  OverflowException where CPython raises MemoryError (OutOfMemoryException). b282895 had recorded it under QEMU only
  and taken it for an emulation artifact; it is .NET 10 on arm64.
- macOS, interop-test, pythonnet (net8.0): Costs_ViewVerbsAreFlat_CopyVerbsScale - ToNDArrayView 0.0175 -> 0.4766 ms
  across the 10^4x size step (27x against a 20x bound), on a shared runner, while the copy verbs scaled ~580x
  (ToNumpyCopy 0.0080 -> 4.6377 ms); net10.0 and the earlier run passed.

WHAT
- Lifetime gap LT31, a real lost GC anchor (src): NDPolyEval.Evaluate runs the recurrence kernel over the raw addresses
  of x and c, and every {p}val (polyval, chebval, legval, hermval, hermeval, lagval, and the series classes'
  __call__, integ's lbnd evaluation, the Gauss quadratures) reaches it through NDPolyEval.Val(PolyBasis, object,
  NDArray, bool), which opened no scope. Optimized code could drop a caller's temporary at its pointer read, and a
  collection mid-kernel released its buffer under the kernel: the tail of a 262,144-element result came out wrong.
  Fixed the house way (README "Lifetime"): [NDAnchored] on that overload, so the weaver roots x (object reaches an
  NDArray) and c until it returns; no hand-placed GC.KeepAlive.
  - The ledger, as for LT29 / LT30: the generator's FIXED["LT31"] and a _site_of rule (polyval / chebval lose operands
    0 and 1), the corpus regenerated with NumPy 2.4.2 - 357 -> 361 gate cases (a pinned call keeps its arg0 / arg1
    variants, so a regression names the operand), 123 pinned to 31 fixed gaps; every other line byte-identical, the
    four new cases carry their call's SHA-256, lifetime_gaps.jsonl unchanged. LifetimeOracleTests.FixedGaps covers
    LT01-LT31; OperandRootsTests gains the LT31 row (NDPolyEval.Val, PolyBasis,Object,NDArray,Boolean); the README's
    table, counts and "next id" (LT32), and .claude/rules/oracle.md, say LT01-LT31.
- PrintfFormatter (src): RequireStringFits checks a padded field's final length, as a long, against .NET's longest
  string (0x3FFFFFDF) before PadCodePoints, PadInteger, Pad and the integer precision's zero run allocate, and raises
  OutOfMemoryException (CPython's MemoryError) itself. new string(' ', n) past the limit throws OutOfMemoryException on
  x64 but OverflowException on arm64 under .NET 10, which no Python-facing caller maps to MemoryError.
- StringOracleTests: NanSignIsX86Reference (the process is x86 / x64). The corpus was recorded on win-amd64, whose
  generated NaN is negative; arm64 generates the positive one. Off x86 a slot compares as a NaN token
  (EqualWithNaNTokens: equal bytes, or both NaN at the dtype's width - 2, 4 or 8 bytes): the result arrays, the mutated
  base buffers (np.quantile's out=) and a masked array's data bytes. x86 hosts keep byte equality.
- MisalignedRegistry (H2) IsHostLibmAtan2: arctan2 is the platform C library's on both sides at every element
  (npy_atan2f / npy_atan2 are the CRT's atan2f / atan2; NumSharp calls MathF.Atan2 / Math.Atan2, the same functions
  since parity QA-24), so off the libm reference host a float32 / float64 value divergence whose every differing element
  is within 2 ULP is that host's rounding. Checked first in Classify, ahead of the zero-excuse tiers (weak_scalar is
  one), because it excuses the platform, not NumSharp; never on the reference host; the op itself, out_binary's
  params.ufunc or weak_scalar's params.fn must be arctan2. MisalignedRegistryTightnessTests: H2_Atan2WithinTwoUlp_
  ExcusedOnlyOffTheReferenceHost and H2_GrossMiss_OtherOp_OtherDtype_OtherKind_NotExcused (a 3-ULP miss, another op,
  float16, a dtype divergence: all still fail).
- DocExamples.PythonnetNumpyPage.Costs_ViewVerbsAreFlat_CopyVerbsScale: the fastest of 10 timed calls (was 4) after a
  warm-up, and a view verb may grow 100x (was 20x) across the 10^4x size step. A copy verb grows ~600x there, so the
  bound still separates O(1) from O(n) with a wide margin; the copy bounds (> 50x) are unchanged.

VERIFIED (this tree, Release)
- Windows x64 (i9-13900K, AVX2): NumSharp.Tests 23,092 / 23,093 passed, 0 failed (before the ledger update; the ledger
  touches only the Oracle project and the generator); the round-2 oracle tests and MisalignedRegistryTightnessTests
  57 / 57; LifetimeOracleTests (gate + contract, the regenerated corpus) 4 / 4 on net10.0 and net8.0; the interop
  test project builds.
- Linux x64 (WSL Ubuntu 24.04, glibc; the Windows-built DLLs): the oracle tests ubuntu failed plus the arctan2 tiers,
  64 / 64 on net10.0 and net8.0; LifetimeOracleTests (gate + contract) with the regenerated corpus 4 / 4 on net10.0
  and net8.0.
- linux/arm64 under QEMU (neoverse-n1, .NET 10): LimitQuirks / OperandRoots / PercentFormat 62 passed, 1 skipped;
  the whole StringOracleTests class 71 passed, 1 skipped, 0 failed (it failed QuantilePrecisionAndOut's base buffer
  before the token rule reached it).
- Not reproducible here: Apple's libm and a real macOS runner - the next CI run.
…ack frame - NDCharArray.NanMax, once at tier 1, inlined np.amax and tail-jumped to np.<amax>subclass, so the chararray caller audit reported np.nanmax as a library caller; NoInlining | NoOptimization on the four overrides, and a guard test that reads the flags from metadata

WHY
PR #633's third CI run (Build and Release 42404538019, c3a2f75) was green on five legs - macOS test (unit, the whole
oracle: lifetime gate, string-oracle NaN tokens, the H2 arctan2 excuse), windows test, and interop on all three OSes,
so every fix of c3a2f75 held - and red on one: ubuntu, Test Oracle (net10.0),
StringOracleTests.CharArrayOperand_LibraryNeverReachesACharArrayRule:
    library methods handed a chararray object to a public function (its subclass path ran):
        24 x NumSharp.np.nanmax -> amax
        24 x NumSharp.np.nanmin -> amin
The same test passed on that host's net8.0 leg, on ubuntu and macOS in run 2, and locally (Windows and WSL glibc,
tiered compilation on and off): a race, not a library change.

ROOT CAUSE (measured)
The audit (NDSubclass.Audit, a test hook) walks the stack up from a woven subclass path and stops at an override of a
[SubclassAware] member - NumPy's own class-reading code, which hands the subclass object on by design (np.nanmax of a
chararray is np.amax of the chararray, NDCharArray.NanMax). The walk can only see the frames optimized code keeps. The
JIT listing of NDCharArray.NanMax at full optimization on linux-x64 (DOTNET_TieredCompilation=0, DOTNET_JitDisasm):
    call     [NumSharp.NDArray:FmaxFminReduce(...)]                 (base.NanMax, inlined)
    tail.jmp [NumSharp.np:<amax>subclass(...)]                         (np.amax inlined, then a fast tail call)
So once the override is optimized its frame is REPLACED by np.<amax>subclass's, the walk skips that woven frame and finds
np.nanmax, which the audit records. Whether the override is optimized when the replay reaches it depends on when tiered
compilation lands its tier-1 code (after 30 calls and the call-counting delay, on a background thread), hence one red
leg in five.

WHAT
- NDCharArray.NanMax / NanMin and NDMatrix.NanMax / NanMin - the four overrides of [SubclassAware] virtuals (NDArray.NanMax
  / NanMin) - carry [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]: NoOptimization makes
  the call a call (MinOpts inlines nothing and makes no implicit tail call), NoInlining keeps any caller from absorbing
  the frame (guarded devirtualization under dynamic PGO included). The listing after the fix, same host and settings:
    NumSharp.NDCharArray:NanMax(...) (MinOpts)
    call     [NumSharp.np:amax(...)]
  The cost is nil: a chararray's text case always ends in np.amax's TypeError, and the matrix overrides only branch to
  NanExtreme. The matrix pair is not known to have failed (NanExtreme never hands the matrix to a public function), but
  NDMatrix.NanMax tail-calls NanExtreme the same way, so the rule is stated for every override.
- SubclassAuditFrameTests.SubclassAwareOverrides_KeepTheirFrame (NumSharp.Tests, new): every override of a
  [SubclassAware] member in NumSharp.Core has both flags, and at least the four known overrides are found (no vacuous
  pass). It reads metadata, so it is deterministic where the audit needed a tier-1 compile to land mid-replay. Checked
  against the build before the fix by the same rule: it names all four overrides (flags "IL").
- NDSubclass.Audit's remarks state the frame requirement and this failure; .claude/rules/char-chararray.md (the audit
  trap) and matrix.md (the matrix-operand audit) say the same.

NOT CHANGED (reported)
The audit stays a stack walk, so a library method that hands a subclass object to a public function in TAIL position
can still escape it once optimized (its frame jumps away; the walk then reaches a non-library frame and records
nothing). The [SubclassAware] statics (NDArray.block, NDArray.asanyarray, NDArray.AxisConcatenate, NDMatrix.Create)
delegate to their helpers in tail position as well; none of those helpers hands a subclass object to a public function
today (the audits are green with every method optimized from the start, TC=0, on Windows and WSL).

VERIFIED (this tree, Release)
- Windows x64: NumSharp.Tests 23,094 / 23,093 passed, 0 failed (net10.0 / net8.0); CharArrayOperand_ / MatrixOperand_
  LibraryNeverReaches* with CharArrayFunctions, CharArrayOperatorsAndMembers and CharStringMethods 5 / 5, tiered
  compilation on and off (net10.0).
- Linux x64 (WSL Ubuntu 24.04, .NET 10.0.3, the Windows-built DLLs): the JIT listings above; both caller audits 2 / 2
  on net10.0 and net8.0 with tiered compilation on.
- Not reproducible on demand anywhere: the failure needs tier 1 to land mid-replay; the guard test is the
  deterministic check, and the next CI run the live one.
…after the Overview and move the packages' elaboration into the details block as its first section, as 0.70.0's notes had it; CHANGELOG_STYLE records the layout

WHY
The owner, 2026-11-08, on PR #633's post, which is also the release's notes: "Move the nuget releases to the detailed
area and where they are now leave a one-liner per nuget". It reverses the layout of 2026-11-06 ("New NuGet Packages
should be outside detailed breakdown"): the visible part now holds the Overview and a short package list, and the
26-line elaboration of the three packages (Tensors, ONNX Runtime, ML.NET) no longer pushes everything below it down.

WHAT
docs/releases/RELEASE_0.80.0.md
- After the Overview, `### 📦 New NuGet Packages` is one line per package:
  - NumSharp.Interop.System.Numerics.Tensors - zero-copy interop between NDArray and the BCL's Tensor<T> /
    TensorSpan<T>, all 15 dtypes;
  - NumSharp.Interop.OnnxRuntime - run ONNX Runtime models on NDArrays, fed from the arrays' own memory, with the
    outputs read back as NDArrays;
  - NumSharp.Interop.MLNet - an NDArray as an ML.NET IDataView, and a transformer's output column back as an NDArray.
  Each line is its package's lead sentence, shortened; the links are the ones those sentences carry.
- The whole elaboration (the heading, the "Three optional companion packages" paragraph, each package's Export /
  Import / Modes / Running a model / Text / Post-processing / Dtypes / Limits / Depends sub-lists) moves unchanged
  to the top of `## Detailed Breakdown` / `<details>`, before Dashboards & Docs. git shows it as the four lines that
  open the details block moving above it (9 insertions, 4 deletions); no package line changed.
- 91,720 -> 92,344 characters (the release body limit is 125,000 with the install header). No hash, count or figure
  changed.

.claude/skills/changelog/references/CHANGELOG_STYLE.md
- §2: the layout is the Overview and a one-line-per-package New NuGet Packages list (visible), then the details block
  with every §3 section, the packages' §7A elaboration first.
- §3 note: sections 1-8 sit inside the details block; the visible list above it names each package in one line.
- §14: a 2026-11-08 row that supersedes the 2026-11-06 layout row.

PR #633
The post is set to this file minus its H1 and the intro placeholder comment, the form it took on 2026-11-07, so the
post and the release body hold the same text. That also brings in the two lines the post had missed since it was
copied at bf6f404: the Trusted Publishing line (Testing & Tooling, 482d44d) and the Shape copy-keyword wording of
the Shape breaking line (497c429).

Verified: the move script asserted every boundary before writing (one packages heading and one details opener, three
package entries, the details block opening with the elaboration, the file growing by exactly the one-liner list);
no em or en dash in either file; the post differs from the file only by the four header lines.
…ine - "NumPy 2.x API coverage around 95.2%, everything except datetime, complex64 and other exotic dtypes is fully supported, with byte-to-byte equality and compatibility to NumPy."

WHY
The owner, 2026-11-08, item 2 of the release-notes pass on PR #633 (whose post is the release notes):
"- NumPy 2.x API coverage **87.1% -> 95.2%** (488 -> 533 of the 560 headline APIs, 4 more partial), **537 of the 560
oracle-verified** against committed NumPy 2.4.2 contracts, and **2,015 of the 4,844 APIs** of NumPy's whole public
surface - every ......" -> "- NumPy 2.x API coverage around 95.2%, everything except datetime, complex64 and other
exotic dtypes is fully, with byte-to-byte equality and compatability to NumPy."

WHAT (docs/releases/RELEASE_0.80.0.md, the Overview's first bullet; its five sub-bullets stay)
- The owner's sentence replaces the figures line. Two typo fixes only, both named to the owner: "is fully, with" ->
  "is fully supported, with" (the missing word), "compatability" -> "compatibility".
- Gone from the Overview with it: the 87.1% -> 95.2% / 488 -> 533 before-after, the 537 oracle-verified, the 2,015 of
  4,844 expanded scope and the Overview's link to the Supported Features Dashboard. The Dashboards & Docs bullet in the
  details block keeps the dashboard's figures and link.

PR #633: the post is regenerated from this file (notes minus the H1 and the intro placeholder) and set with
`gh pr edit 633 --body-file`.

Verified: one line changed (1 insertion, 1 deletion); no em or en dash; the post equals the file minus its four
header lines.
…most used on GitHub - np.hypot, np.histogram, np.logspace, np.gradient, np.vectorize, np.hanning, np.geomspace, np.apply_along_axis, np.trapezoid and np.histogram2d, measured over all 48 by GitHub code search

WHY
The owner, 2026-11-08, item 3 of the release-notes pass on PR #633 (whose post is the release notes): "- `np.*` -
**+48 functions**. - mention top 10 used on github here".

WHAT (docs/releases/RELEASE_0.80.0.md, the Overview's np.* sub-bullet)
"`np.*` - **+48 functions**." -> "`np.*` - **+48 functions**, the ten most used on GitHub: `np.hypot`, `np.histogram`,
`np.logspace`, `np.gradient`, `np.vectorize`, `np.hanning`, `np.geomspace`, `np.apply_along_axis`, `np.trapezoid` and
`np.histogram2d`." Ordered by use. The 48 are the New APIs & Modules line's list.

HOW IT WAS MEASURED (2026-11-08; the repo's method, memory github-code-search-usage-counts)
- One GitHub legacy code-search request per function, keyring gh login, 6.5 s apart:
  `"np.<name>" extension:py size:<40000 NOT site-packages in:file,path` -> total_count (files). .py only, files under
  40 KB and nothing under site-packages, which drops most vendored NumPy copies.
- Sampled before ranking, because the engine prefix-matches the last word and folds case: page 1 (hits 1-100) of the 14
  contenders, and page 10 (hits 901-1000) of the six that decide the order, each hit kept only when a text-match
  fragment holds `np.<name>` followed by a non-word character and its path is no NumPy / site-packages copy.
  Estimate = count x the page-10 share where taken, else the page-1 share.
- Files (estimate; page-1 / page-10 literal share):
   1. np.hypot             190,976 -> ~189,000 (100 % / 99 %)
   2. np.histogram         193,536 -> ~180,000 (100 % / 93 %: 12 of page 10's hits were np.histogram2d /
                                                 np.histogram_bin_edges / np.histogramdd)
   3. np.logspace          157,952 -> ~156,400 (100 % / 99 %)
   4. np.gradient           81,024 (100 %)
   5. np.vectorize          67,328 (100 %)
   6. np.hanning            51,520 (100 %)
   7. np.geomspace          42,048 (100 %)
   8. np.apply_along_axis   36,480 (100 %)
   9. np.trapezoid          31,296 (100 % / 100 %); its NumPy 1.x name np.trapz adds 48,320, which NumSharp does
                            not ship, so it is not counted
  10. np.histogram2d        30,848 -> ~28,070 (100 % / 91 %)
  11. np.unwrap             28,032 (100 % / 100 %) - tied with np.histogram2d inside the estimate's noise (GitHub
                            extrapolates large totals, about +-10 %); histogram2d keeps 10th by 40 files
  then np.shares_memory ~20,800, np.packbits 20,384, np.binary_repr 19,328, np.unpackbits 18,176, np.fabs 18,016,
  np.spacing 15,104, np.sinc 13,600, np.may_share_memory 12,432, np.signbit 10,768, np.divmod 10,176, np.hamming
  9,984, np.putmask 8,720, np.remainder 8,112, np.histogramdd 7,360, np.fix 7,184, np.broadcast_shapes 6,272,
  np.frexp 6,016, np.fmod 5,832, np.real_if_close 5,824, np.gcd 5,792, np.put_along_axis 5,136, np.ldexp 4,960,
  np.histogram_bin_edges 4,880, np.heaviside 4,456, np.frompyfunc 3,880, np.piecewise 3,588, np.blackman 3,256,
  np.kaiser 2,892, np.lcm 2,756, np.array_equiv 2,388, np.apply_over_axes 2,132, np.bitwise_count 2,104,
  np.float_power 2,068, np.base_repr 2,020, np.typename 1,204, np.i0 866, np.bartlett 818 (raw counts, unsampled).
- Counts the `np.` spelling only (numpy.<name> and from-imports are not counted), indexed public default-branch
  files only.

PR #633: the post is regenerated from this file (notes minus the H1 and the intro placeholder) and set with
`gh pr edit 633 --body-file`.

Verified: one line changed; no em or en dash; the post equals the file minus its four header lines.
…ns (441 distinct hashes) leave 236 lines, the Overview's tally line states the release's 1,667 commits instead of "traced to one of 439 cited commits", and CHANGELOG_STYLE records that published notes strip their citations

WHY
The owner, 2026-11-08, on PR #633 (whose post is the release notes): "Remove all commit hashes from our new release
notes".

WHAT
docs/releases/RELEASE_0.80.0.md (92,344 -> 84,692 characters)
- Every hash was a trailing citation: ` - `h1`, `h2` (+ `h3`, `h4`)` before a line's final period, with none inside
  a sentence and no bare hex token anywhere (checked before the edit). Each is cut from its line and the period
  kept: 236 lines, 610 occurrences, 441 distinct hashes. No other word changed.
- The Overview's tally line: "**43 breaking changes** - every line traced to one of 439 cited commits out of the
  release's 1,586." -> "**43 breaking changes**, across the release's 1,667 commits." The claim about
  citations no longer holds once they are gone; the count is `git rev-list --count v0.70.0..HEAD` at this commit.
- The cited version of the file remains in history: c904213 is its last revision with every citation, for a
  future sweep's bucket 1 (Cited).

.claude/skills/changelog/references/CHANGELOG_STYLE.md
- §6: the published notes carry no hashes (the owner, 2026-11-08, for 0.80.0). Cite while drafting and sweeping
  (the 5-bucket triage reads the cited set from the file), then strip every citation before the notes become the
  release body and the PR post; the last cited revision stays in git history as the ledger; the Overview gives the
  release's commit count, not a cited count.
- §14: the matching 2026-11-08 row.

PR #633: the post is regenerated from this file (notes minus the H1 and the intro placeholder) and set with
`gh pr edit 633 --body-file`.

Verified: the strip script asserted no hash left, the line count unchanged and no changed line ending in a
dangling separator or a doubled period; every changed line ends with a period; 237 lines changed (236 citations +
the tally line); no em or en dash; the post equals the file minus its four header lines.
… vendored NumSharp.Collections.Concurrent.ConcurrentDictionary are internal - both names are the BCL's (CS0104 for every .NET 9+ consumer file that imports NumSharp.Collections beside the implicit System.Collections.Generic), both would have shipped first in 0.80.0, and ConcurrentOrderedDictionary / ConcurrentOrderedCompactDictionary stay public

WHY
The owner, 2026-11-08, on the 0.80.0 completion plan's decision page ("Two public types in NumSharp.Collections share
their names with BCL types ... What should ship?"): "Hide both".
- NumSharp.Collections.OrderedDictionary<TKey, TValue> (the lock-free, value-once ordered map) shares its simple name
  with System.Collections.Generic.OrderedDictionary<TKey, TValue> (.NET 9 and later). ImplicitUsings imports
  System.Collections.Generic into every SDK project, so `using NumSharp.Collections;` made the first unqualified
  `OrderedDictionary<,>` a CS0104 ambiguity (verified on net10 when the type was renamed, 2026-09-23).
- NumSharp.Collections.Concurrent.ConcurrentDictionary<TKey, TValue> (a vendored copy of the BCL type, the key path of
  ConcurrentOrderedDictionary) clashed with System.Collections.Concurrent.ConcurrentDictionary<TKey, TValue> in any file
  importing both namespaces, on every target, while adding only GetValueRefOrNullRef over the BCL type.
- Neither type is in v0.70.0 (no file under a Collections path there), no public NumSharp member takes or returns
  either, and no other package uses them: internal now breaks nobody, and making either public later (under a name of
  its own) breaks nobody either, where hiding a shipped type would.

WHAT
- src/NumSharp.Core/Collections/Concurrent/OrderedDictionary.cs: `internal sealed class`; the remarks gain an
  "Internal, not public" paragraph (the clash, the decision, why internal is the reversible direction).
- src/NumSharp.Core/Collections/Concurrent/ConcurrentDictionary.cs: `internal partial class`; the vendored file's
  deviation list and the class remarks say why, the deviation comment beside the declaration names both deviations.
- src/NumSharp.Core/Collections/Concurrent/ConcurrentDictionary.RefAccessors.cs: `internal partial class` (partial
  parts must agree on accessibility), with a comment pointing at the reason.
- src/NumSharp.Core/Assembly/Properties.cs: InternalsVisibleTo("NumSharp.Benchmark.CSharp", keyed like every friend).
  benchmark/NumSharp.Benchmark.CSharp/Benchmarks/Collections/ConcurrentOrderedDictionaryBenchmarks.cs measures the
  vendored clone (`using CloneCd = NumSharp.Collections.Concurrent.ConcurrentDictionary<int, int>`) against the BCL's,
  and the existing "NumSharp.Benchmark" grant is test/NumSharp.Benchmark's, not this project's. docs.yml's
  "Execute every official C# benchmark body once" step builds this project (#:project), so without the grant the
  docs build would stop compiling on master. No shipped package references the benchmark assembly.
- docs/website-src/api/toc.yml (the committed, customized API toc): the two types' entries go - OrderedDictionary`2
  with its Enumerator, and the NumSharp.Collections.Concurrent namespace block, whose only entry was
  ConcurrentDictionary`2.
- Not touched: docs/plans/internal-surface/* (the 2026-11-07 surface map snapshot, a record of that day) and
  docs/ORDERED_COLLECTIONS_FIX_HANDOVER.md (history); the release notes' NumSharp.Collections line changes with the
  notes commit.

VERIFIED (Release, this worktree)
- Builds: tools/NumSharp.Build.Analyzer, test/NumSharp.Tests, test/NumSharp.Tests.Oracle and
  benchmark/NumSharp.Benchmark.CSharp (net8.0 and net10.0, both recompiled against the new NumSharp.dll at 18:12) with
  0 errors - the benchmark's CloneCd uses compile through the new grant.
- test/NumSharp.Tests, every Collections and StrongName test (CI filter): 192 / 192 on net8.0 and net10.0 - the
  StrongName class checks that no friend declaration is keyless.
- test/NumSharp.Tests.Oracle, every Surface test (LeakSurfaceCoverage, OracleSurfaceCoverage, ...): 12 / 12 on both.
…release - NDW002, 003, 005, 006, 009, 010, 011, 012, 013, 015, 016 and 017 under "Release 0.70.0", NDW020 under "Release 0.80.0", and AnalyzerReleases.Unshipped.md starts empty for the next release

WHY
Roslyn's release tracking (RS2000-RS2008, Microsoft.CodeAnalysis.Analyzers reading the two files as AdditionalFiles)
expects a rule to move from Unshipped.md to Shipped.md under the release that first ships it. The tracking files were
added on master after v0.70.0 was tagged (1998f788), so every rule still sat in Unshipped.md, NDW020 included.

Which release shipped which rule, checked rather than assumed:
- The NumSharp 0.70.0 package on nuget.org carries analyzers/dotnet/cs/NumSharp.Build.Analyzer.dll (its 16 entries
  listed from the downloaded .nupkg; NumSharp.Build 0.70.0 carries only the weaver), and v0.70.0's analyzer sources
  define NDW002, 003, 005, 006, 009, 010, 011, 015 (Error), NDW012, 016, 017 and 013 (Warning) - the same ids,
  category (NumSharp.Build) and severities as today, so none is a "Changed Rule".
- NDW020 (culture-independent exception texts, off by default, parity QA-41) first ships in 0.80.0.
- NDW000 / NDW018 / NDW021 / NDW022-031 are MSBuild or weaver diagnostics, not analyzer descriptors, so they are not
  tracked here (as before).

WHAT
- tools/NumSharp.Build.Analyzer/AnalyzerReleases.Shipped.md: "## Release 0.70.0" with the twelve rules and
  "## Release 0.80.0" with NDW020, each table row as it stood in Unshipped.md.
- tools/NumSharp.Build.Analyzer/AnalyzerReleases.Unshipped.md: the two header lines only.

VERIFIED: a --no-incremental Release build of tools/NumSharp.Build.Analyzer compiles with both files as AdditionalFiles
(CoreCompile ran) and reports no RS1xxx / RS2xxx diagnostic.
…afted, accepted as shown), the bridges' follow-ups and thread safety (NDInferenceSession), the extension points, the NDScope foreign-thread breaking change, NDW021, decimal / char dtype.char, the pythonnet memory booking, NumSharp.Collections without its two internal types, and every figure re-read from inventories of the final code (86 fixes, 40 speedups, 44 breaking changes, 1,670 commits)

WHY
The owner, 2026-11-08: the 0.80.0 completion plan's decisions ("Hide both" for the Collections clash, "Keep,
documented" for the C# 12 null seed, "Draft it for me" for the intro), then "lets skip 1 [the keeporder merge], and at
6, dont wait - merge and see that release goes through". This is the last notes commit before the merge: everything
the bridge QA's section 8 listed "for whoever keeps the notes" (docs/plans/interop-onnx-mlnet-release-qa.md) and the
release-notes ledger's owed list is now in, swept from the 86 commits since 11-05 not yet represented (29 of them
code; full bodies read, each bucketed per the changelog skill).

WHAT (docs/releases/RELEASE_0.80.0.md; the PR #633 post is regenerated from it)
- The intro replaces the placeholder comment, three sentences in the 0.70.0 intro's voice, every claim one the notes
  already make (the coverage line, the Overview's modules, the three package one-liners). The PR post now opens with it.
- Overview tally: 85 -> 86 NumPy-parity, memory-safety and tooling fixes (+ the pythonnet line), 40 speedups, 43 -> 44
  breaking changes (+ the NDScope line), and the release's 1,670 commits (git rev-list --count
  v0.70.0..HEAD + 1, computed in the commit's own command). Counting rules as before: Performance = top-level lines,
  Parity = single lines + umbrella sub-bullets, Breaking = top-level lines.
- New NuGet Packages (the details block's elaboration):
  - ONNX Runtime: the import of a legacy Tensor<T> (F71); Running a model gains the up-front feed checks (P12 + P2: a
    missing required input with onnxruntime's own message, an unknown output name), RunAsync's dynamic dimensions bound
    from the fed inputs (P7) and outputs that leave an NDScope (P21); new "Threads and disposal" (section 10's ARC on
    every OrtValue the bridge touches, P11's ObjectDisposedException, NDInferenceSession) and "Memory" (P8: ToOrtValue's
    copy and the leases booked, 5,000 forgotten 602 KB copies +68 MB instead of +2,898 MB).
  - ML.NET: a repeated column name refused (P18), the dictionary "Tables" view (P1), the shuffling view (P17: SDCA on
    class-ordered rows passes 0.65 accuracy, ML.NET's pool shuffle left it at 0.50 - the gate's numbers), and the
    VBuffer<T> view's booking (P8 Q3).
  - System.Numerics.Tensors: the AsNDArray view's booking (P8 Q3).
- New APIs & Modules: NumSharp.Collections names only ConcurrentOrderedDictionary and ConcurrentOrderedCompactDictionary
  (82d952c made the other two internal); a grouped "Extension points" entry (687fb94, d475a42): __array_priority__
  and the protected internal hooks __array_wrap__ / __array_finalize__ / _collapse with the closed-world limit stated
  (a user subclass gets plain arrays back from np.*, np.nanmax / np.nanmin excepted), the public constructors, the
  BitGenerator contract and the unsealed generators, Shape's raw layout, TensorEngine.Evaluate / DotFunction1D /
  VdotFunction1D and the bridge surface. Each name was checked public in the source before it was written.
- Parity & Fixes: NumSharp.Interop.pythonnet's view leases book with the pool (P8; a released package, so a fix line).
- Testing & Tooling: NDW021, the PublishAot build warning (e41c75f, renumbered by d51ab34).
- Breaking Changes: NDScope.Dispose on a foreign thread throws in every build (9e75695, P20; NDScope and its
  Debug-only assert are in v0.70.0); the NPY_TYPECHAR line also states decimal's and char's dtype.char 'A' / 'u'
  (33d6ae4; v0.70.0's ToTYPECHAR gave 'q' and 'c', read here from v0.70.0's NPTypeCode.cs).
- Figures, from generators run on the final code (scratchpad copies of docs.yml's api-coverage steps:
  coverage/generate_coverage.py, test/inventory/generate_test_inventory.py, benchmark/scripts/audit_coverage.py):
  test declarations 20,478 -> 20,506 (Overview and the Tests & Oracle bullet), the expanded scope 2,015 -> 2,017 of 4,844;
  unchanged and re-confirmed: 95.2% (533/560, 4 partial), 537/560 oracle-verified, 244 NumPy-identical calls, 1,615
  expanded oracle-verified, 1,119,305 oracle cases over 1,448 operations, 836,054 corpus cases, 281,297 string cases,
  452 of 518 benchmarkable APIs.

NOT LINES (bucketed, with the reason): the subclass redesign's machinery (bcd3d9f, 910be4c, 9a0ff36, abbafef,
d8e2327, a15cfe1) - NDMatrix / NDCharArray are new in the release and their np.* answers are already the notes' lines
(np.matrix's "np.* functions return NumPy's class and shape", chararray's "np.* rules"); the PA2a / PA2b / PA2c kernel
work (d1754bc, da886b5, ddfe2d3), ffeb592 and d6f9991 - measured old/new or in-range features, no NPY/NS line owed
(CHANGELOG_STYLE §8); 6b8c2ae - np.where over text, an in-range feature (folded); a54d88d - batch 7's QA-02 text,
inside the parity-QA line; e48d7bc / 5b642f6 - np.mintypecode (new) and a ratchet; 582a43b, c3a2f75, 1495928 -
coverage, CI and version plumbing; 278e986 / f9aa136 - the Shape copy keyword, already in the bug 2d breaking line.
ParquetNet (eaac31e's first half) stays out of the notes: unreleased.

Verified: no commit hash, no "0.71.0", no ParquetNet, no non-ASCII dash in the notes; 89,2xx characters (the release
body's budget is 122,500 with the install header); the post equals the notes minus the H1.
@Nucs
Nucs marked this pull request as ready for review November 8, 2026 16:22
@Nucs
Nucs merged commit d523eb4 into master Nov 8, 2026
1 of 9 checks passed
@Nucs
Nucs deleted the release-0.80.0 branch November 8, 2026 18:10
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