Repository navigation
[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
Conversation
…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)
… 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
bytes_(S) andstr_(U) dtypes and NEP 55's variable-widthStringDType, with every computation NumPy runs on text, all 46numpy.stringsfunctions andnumpy.charwithchararray.np.polynomial.*- the wholenumpy.polynomialpackage, 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.trapezoidandnp.histogram2d.np.random.*- every NumPy bit generator, the wholeGeneratordistribution surface, and a legacyRandomStatethat 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.matrixas the newNDMatrixtype.DTypeis NumPy's two-level dtype model -NDArray.dtypereturns anumpy.dtype-shaped descriptor, withnp.dtypes, byte order anddatetime64/timedelta64descriptors, and NumPy's scalar classes (np.integer,np.floating,np.number, ...) withnp.typing.max,min,ptp,nanmax,nanmin,argmax, the flatsum/meanand the fusednp.evaluatereductions match NumPy bit for bit, down to the sign of a ±0 tie and a NaN's payload;mean,var,stdand their NaN-aware forms are NumPy's own statements; and most of them run faster.np.*function is aTensorEnginemember named after its NumPy source file,numpy.linalgandnumpy.fftare replaceable sub-engines, and the default engine is settable.np.dot/np.vdot/np.vecdot.📦 New NuGet Packages
NDArrayand the BCL'sTensor<T>/TensorSpan<T>(System.Numerics.Tensors), all 15 dtypes.NDArrays, fed from the arrays' own memory, with the outputs read back asNDArrays.NDArrayas an ML.NETIDataView, and a transformer's output column back as anNDArray.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.
NDArrayand System.Numerics.Tensors (Tensor<T>,TensorSpan<T>,ReadOnlyTensorSpan<T>); all 15 NumSharp dtypes cross as their own CLR type,Half,Complex,decimalandcharincluded.AsTensorSpan<T>()views anNDArrayof any non-negative-stride layout (contiguous, sliced, transposed, strided, broadcast) as aTensorSpan<T>over the same unmanaged buffer, through a disposable handle that keeps the buffer pinned;ToTensor<T>()copies into a denseTensor<T>.AsNDArray()views aTensor<T>as anNDArraywithout 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 aTensor<T>,TensorSpan<T>orReadOnlyTensorSpan<T>.S/U/StringDTypearrays cross as aTensor<string>copy in both directions (ToStringTensor()/ToNDArray(Tensor<string>)); the view verbs refuse text with one message, because aTensor<string>holds managed strings.TensorPrimitivesdirectly, with no fill loop.NDArrayto an ONNX RuntimeInferenceSessionfrom the array's own memory and read the outputs back asNDArrays; no Python.Auto(a view when ONNX Runtime can read the buffer in place, else a copy the handle owns),View(refuses to copy) orCopy.AsOrtValue/ToOrtValue, andAsDenseTensor/ToDenseTensorfor ONNX Runtime's legacy tensor tier; import -ToNDArraycopies (a legacyTensor<T>too),AsNDArrayviews and can own the value.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;RunIntofills pre-allocated outputs;RunAsync/RunIntoAsyncrun ONNX Runtime's asynchronous inference (1.30.0 or later) and size a dynamic dimension (batch_size,N) from the fed inputs; the outputs ofRunandRunAsyncleave anNDScope(scope.ReturnsTask(session.RunAsync(...))).OrtValuethe bridge touches is released once, after its last use: aDisposefrom 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 withObjectDisposedException;NDInferenceSession, anInferenceSessionwith ONNX Runtime's constructors, releases a session once, after the bridge's runs on it.ToOrtValue's copy and every view the bridge hands out count toward NumSharp's memory accounting, so forgotten ones get collected: 5,000 forgottenToOrtValuecopies of a 602 KB array peak at +68 MB instead of +2,898 MB.U/Sarrays cross as string tensors (a copy;ReadStringTensorreads one asstring[]), and aStringDTypearray is refused with its sized-Uremedy;ToNDArrays,ToMap,ToMapsandGetMapKeysread sequence and map outputs.Softmax,LogSoftmax,Sigmoid,Argmax,TopK.charexports as uint16 anddecimalas float64 through a copy the handle owns; complex and bfloat16 are refused, each refusal naming the route that works.NDArrayto an ML.NET pipeline as anIDataViewand read a transformer's output column back as anNDArray, with no POCO class and no row-by-row fill.AsDataViewis a lazyIDataViewover the buffer's strides (one vector column, or one scalar column per feature, a repeated name refused);ToDataViewtakes a snapshot;ToVBuffercopies into aVBuffer<T>.AsDataViewof 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 nohstackand no conversion step.CanShuffleand 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).ToNDArrayreads a scalar column as(R,)and a fixed-size vector column as(R, C);AsNDArrayviews a denseVBuffer<T>'s array without copying, and the pinned array counts toward NumSharp's memory accounting until the last view is disposed.transformer.Transform(nd.AsDataView("Features")).ToNDArray("Score"): theIDataViewis the whole bridge.Postprocess(Softmax,LogSoftmax,Sigmoid,Argmax,TopK), which this package references, so an app that uses both bridges has one.charone way as uint16, float16 as float32 anddecimalas 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[]>).Postprocess(bringing ONNX Runtime's C# API, whichPostprocessnever loads); theMicrosoft.MLyou reference brings the trainers and transforms. Verified on ML.NET 2.0.1 and 5.0.1.📊 Dashboards & Docs
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.benchmark/strings: 442 text cells at geomean x6.61 NumPy's speed.np.remainder,np.random.ranfandnp.random.sample, which 0.70.0 credited throughnp.modandnp.random.random_sample, are methods of their own now, with no benchmark of their own.✨ New APIs & Modules
bytes_(S) andstr_(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-copynp.frombuffer(bytes, "S3")- creation.item(),tolist(),NDArray.AsStringArray, stores cut to the width; promotion,can_castand thenp.dtypegrammar; views, joins, gathers and indexing in every layout;astypebetween widths and classes; printing and.npy- the dtype end to end.str(), a text read as a number is CPython'sint()/float()/complex(), through every route that converts.StringDType-np.dtypes.StringDType(na_object, coerce)andnp.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, andnp.ndarray(shape, StringDType, buffer:)views of another array's cells; a maskednp.copytointo it converts only the elements the mask selects.S/U/StringDTypetext computes with NumPy's results, NA rules and refusals in NumPy's order -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 everykind),np.argmax/np.argmin,np.searchsorted/np.digitize,np.uniqueand 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.traceofStringDType, reductions into a textout=ordtype=,np.padover text, and numpy.ma over text.np.strings- all 46 ofnumpy.strings' functions overS/U/StringDTypewith 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 forencode/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, without=/where=/dtype=.upper,lower,swapcase,capitalize,title,translate- case maps and translation.add,multiply,mod,encode,decode, andequal...greater_equalunder theirnumpy.stringsnames.np.@char- NumPy'snumpy.charmodule (charis a C# keyword) withchararrayasNDCharArray.compare_chararrays, and everynp.stringsfunction re-exported.chararray- construction, element access that strips as Python'srstrip()does, the+/*/%operators, its string members, and thenp.*rules that keep, wrap or drop the class as NumPy's do.split,rsplit,splitlines,join- the object path, whose results areNDListArray, NumPy's object array of lists with 65 of its 70 ndarray members.np.loadtxt/np.savetxt/ndarray.tofileoverStringDTypewith Python's codec names (encoding:),np.save/np.loadofStringDTypeas NumPy's own pickle (allow_pickle), andnp.fromstring(byte[]).np.evaluateover text -S/U/StringDTypearrays and a C#stringareNDExprleaves: the comparisons, truth readers,IsNaN, casts to and from text,NDExpr.StrLen/Find/StartsWith/ the predicates, concatenation,Wherewith text branches and the root reductions compute as NumPy's statements do.np.polynomial- thenumpy.polynomialpackage for all six bases (polynomial,chebyshev,legendre,laguerre,hermite,hermite_e) pluspolyutils: 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 ispoly/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 everym/k/lbnd/scl/axisform.{p}vander,{p}vander2d,{p}vander3d- Vandermonde matrices.{p}fit(everydeg/rcond/full/wform) andchebinterpolate- fitting and interpolation.{p}companion,{p}roots- companion matrices and roots (the roots compute through the OpenBLAS backend'seigvals).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 fiveX2poly/poly2Xpairs - helpers, module constants and basis conversions.as_series,trimseq,trimcoef,getdomain,mapparms,mapdomain,format_float,set_default_printstyle-polyutilsand the printing helpers.Polynomial,Chebyshev,Legendre,Laguerre,Hermite,HermiteE- the series classes (NDPolynomial...NDHermiteEoverNDPolyBase) under NumPy's names: construction, Python's operator protocol, evaluation and composition,fit/fromroots/roots/convert/integ/derivand the rest, the printers and pickling.np.randombit-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;MT19937is now a NumPyBitGeneratortoo.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, aGenerator, aRandomState,nullfor 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.Generatordistributions - NumPy's wholenumpy.random.Generatorsurface on NumPy's modern algorithms, 29 samplers byte-identical todefault_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/powerstay within 2 ULP /ceil(2/a)+3ULP of NumPy, whose C runtimeexpm1has no portable twin;multivariate_normalis byte-identical with the OpenBLAS backend and may pick the other eigenvector sign without it.RandomStateandGeneratortakeNDArrayparameters, broadcast againstsizeas NumPy does, byte-identical streams.integers,randint- array bounds andBigIntegerbounds, sog.integers(0, BigInteger.Pow(2, 64), dtype: np.uint64)ports verbatim.RandomStategains NumPy's form over any bit generator (np.random.RandomState(new PCG64(42))), the dictget_state(legacy: false)/set_state,tomaxint,ranfandsample.np.ma- NumPy's masked-array module on a newNDMaskedArraytype, bit-exact vs NumPy 2.4.2 and gated by 109,961 differential cases in 13 tiers plus a 27,240-case surface tier.mean/var/std/anomas 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.str/repr; only the structured/record-dtype functions are missing.np.emath- automatic-domainsqrt,log,log2,log10,logn,power,arccos,arcsin,arctanh(an out-of-domain input gives the complex result instead of NaN).np.matrix- NumPy'snumpy.matrixas the typeNDMatrix(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, andnp.stackraises as in NumPy;np.max/np.mintake the method's keyword andout=rules (np.max(m, initial=1)raises itsTypeError),np.nanmax/np.nanminNumPy's subclass route.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(withTooHardError) - array predicates.logspace,geomspace- log-scale ranges, bit-exact.binary_repr,base_repr,typename- string helpers.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[]/Shapedimensions (np.indices(arr.shape)), andmgridpast 2^31 elements.max/minfamily's keywords -np.max/np.min/np.amax/np.amin/np.nanmax/np.nanminandndarray.max/ndarray.mintake NumPy'sout=,initial=andwhere=in NumPy's order, through a port of NumPy's reduction iterator: the loop ispromote_types(out, a),initialis packed into it as NumPy packs a Python scalar, and a mask withoutinitialraises NumPy's no-identity error; bit-exact over a 35,847-caseminmax_kwargsoracle tier with zero excuses. Plainnp.max/np.minof 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).np.sum/np.prodtakeinitial=andwhere=,np.meanwhere=, andnp.var/np.stdwhere=,mean=andcorrection=, bit for bit with NumPy.weights=fornp.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 onNDExpr- logic and selection.Any,All,CountNonzero,NanSum,NanProd,Ptp,NanMin,NanMax,ArgMax,ArgMin,NanMean,Var,Std(ddof), the weightedAverage, axis forms andkeepdims- reductions.dtype=,casting=,order=,where=- keywords;expr.Compile()returns a reusableCompiledExpression.If,Switch,Mux,Saturate,NanTo,Coalesce,Relu,LeakyRelu,Elu,Sigmoid,Swish,Softplus,Gelu,Step,Nand,Implies,IsClose,Between,Bucketize,Lerpand 18 more - 38 decision and activation combinators that fuse like any node.DTypesystem - NumPy's NEP 40-50 two-level dtype model (dtype classes and dtype instances), the descriptor every dtype parameter andNDArray.dtypenow use.np.dtypes(Float64DTypeand 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 fullnumpy.dtypesurface.isalignedstruct,metadata,len(dtype)/dtype[key](__len__/__getitem__) andnp.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(),castingimplswith each class pair's strided loop (get_strided_loop),DTypeFlags.NPY_DT_*,DType.NPY_NATIVEand the other byte-order macros).np.dtype(">i4")) anddatetime64/timedelta64descriptors that parse, print, promote and cast-check (no datetime storage yet).promote_types,result_type,can_cast,issubdtype,isdtype-DTypeoverloads with NEP 50 weak scalars.np.integer,np.floating,np.numberand the rest of NumPy's scalar-type hierarchy - the abstract classes as the newNDScalarType, the class behind every dtype (a.dtype.type) andnp.typing, held to NumPy 2.4.2 by a 6,123-case oracle section (the most-used NumPy names NumSharp lacked:np.integeralone 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.float64derives fromfloat).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]isNDArray<double>).TensorEngine.Linalg,TensorEngine.Fft,TensorEngine.Default-numpy.linalgandnumpy.fftas sub-engines whoseprotected virtualkernels a native package overrides, and a settable default engine that every new array binds.np.flat<T>(a)/a.flatiter.AsTyped<T>()- typed by-refC-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 theNDArray);nd.AsEnumerable<T>()and the typed iterators'ToArray()/CopyTo(Span<T>)- safe copies.Py.RegisterNumSharpCodec()andPy.RegisterNumSharpArrayAdapter(adapter)(NumSharp.Interop.pythonnet) - the one-call codec and array-adapter registrations, spelled on pythonnet's ownPy(C# 14 extension members).S/Uarrays share memory with numpy in both directions,StringDTypecrosses as an exact copy through numpy'sNpyStringC API, and an object array ofstr(a pandas text column) copies asStringDType.NumSharp.Collections-ConcurrentOrderedDictionary<TKey,TValue>andConcurrentOrderedCompactDictionary<TKey,TValue>(about a third of the memory): insertion-ordered, index-addressable concurrent dictionaries.__array_priority__and theprotected internalsubclass hooks__array_wrap__,__array_finalize__and_collapse, under NumPy's names, whichNDMatrixandNDCharArrayoverride; thenp.*functions route only those two classes through them, so a subclass of your own gets plain arrays back from them (np.nanmax/np.nanminexcepted: they dispatch virtually).NDMatrix,NDCharArray,NDMaskedArray,NDMaskedConstant,NDPolyFitResult,NDArray<T>(Shape, bool, TensorEngine)andNDArray(DType, Shape, bool, TensorEngine).BitGeneratoris a public abstract base (NextUInt64,NextUInt32, the state hooks), andGenerator,MT19937,PCG64,PCG64DXSM,PhiloxandSFC64can 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.WrapSharedandVStringAllocatorwith NumPy'sNpyStringC API.🧩 ndarray surface
ToArray<T>()- aTother than the dtype converts likeastype(it threwArrayTypeMismatchException), 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, aTother than the dtype (converted likeastype), 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 aTother 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->xHIGHspans the worst→best measured cell across sizes and dtypes.x0.70->x3.1-np.max/np.min/np.ptpon 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.argminalong 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.sumalong 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.convolvelong products without the OpenBLAS backend (1000x1000 float64 was ~x0.9, complex128 x0.25).x1.15->x5.9- complex128divide,multiplyand integer-exponentpower, now on NumPy's own formulas.x1.26->x4.0-np.modfplain,out=and promoted calls (out=at 10M was x0.40).x1.0->x16-np.array_equal/np.array_equivfused early-exit compare (x15-x16 at N=1000, parity at 100K, instant on an early mismatch).x1.05->x6.4- everynp.randombit generator fills through a bulk layer: all 105 engine x op x size cells beat NumPy.x1.08->x5.58- legacyRandomStatesamplers (hypergeometric was x0.96, zipf x1.12, multinomial x0.97; the samplers bound by the C runtime'spow/log/expstay at x1.17-x1.49).x0.65->x17.6-np.evaluatecomparison, logical,whereand bool trees vectorize instead of running the whole tree scalar (a > 0.5was x0.16).x1.0->x22.2-np.evaluatemixed-dtype trees vectorize through lane groups (i4*2+f8x14.2 andf4*f8+f4x22.2 at 100K; 1K cells sit at the per-call floor).x0.86->x53-np.evaluatereductions stream block by block instead of materializing their child, andAny/Allstop 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 smallnp.evaluatecall skips the iterator like NumPy's trivial loop:a*b+cat 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 fillingsum(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.75on 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.meanon 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 -> float64dtype=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- float16np.max/np.min/np.nanmax/np.nanmin/np.ptpon 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.clipwith array bounds,out=andwhere=: 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- maskednp.copytois 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- float16add/subtract/multiply/dividewith a row, column, strided, reversed or outer-product operand, and intoout=, 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 float16anomx0.86 -> x5.97).x0.71->x80- movingS/UandStringDTypetext: copies, gathers,np.where,np.repeat,np.arrayand the casts (StringDTypegeomean x5.35).x1.14->x221- comparisons and truth values ofS/Utext (geomean x12.3), concatenation and repetition (x1.14-x44), text <-> number (x2.0-x7.9).x0.66->x45- sorting, searching, set routines and statistics overS/Utext (family geomeans x3.05-x4.86; the low cells wait on memory on both sides).x1.54->x47- computing on and orderingStringDTypetext (geomeans x3.31 and x7.80, every cell over x1.5).x1.65->x540-np.strings: searches, strips, pads andreplacex1.65-x15.5, predicates x3.99-x33, case maps x4.50-x237,translatex10.8-x540,modx10.7-x39.7,encode/decodex30.7-x317.x1.03->x459-np.@charandchararray(the stripping comparisons x2.6-x10.5, the member families' geomeans x2.70-x57.1, the object path x4.6-x48) andNDListArray's members (x1.92-x459).x1.4->x30.8- the text readers and writers over every dtype (219 cells, geomean x3.81), andStringDTypethroughnp.save/np.load(x2.1-x9.5) andnp.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.isinover numbers, its table method in one pass (26 cells, geomean x4.21).x0.91->x15.6-np.argsortandnp.uniqueof 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_notin one pass (72 cells, geomean x3.04).x1.84->x10.6-np.evaluatetrees whose dtypes share one element size vectorize (Where(int64 >= 0, float64, 0)x0.74 -> x4.73).x0.88->x9.91-np.indicesof a dtype other than int64 (x7.01 at (1000, 1000)) andnp.mgrid/np.ogridof an imaginary step (x9.91).x0.34->x2.40- the sorts take their per-call scratch from the buffer pool (np.argsortint32 x1.73 -> x2.40).x1.33->x4.34-np.correlate/np.convolveof short and view operands (np.correlateof 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- pythonnetStringDTypecopies; every small pythonnet import runs 2.45x faster than before.x1.07->x1.32-np.dotof 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.nanminare NumPy'sfmax.reduce/fmin.reducebit 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.divide(1 ULP off on ~33% of operands),multiply(NumPy's fusedsimd_cmul),powerwith an integer exponent (up to ~1e14 ULP off) andabs(NumPy's fusedsimd_cabsolute; 35.5% of values were off).percentile/quantile/medianare NumPy's two-branch_lerpin each dtype's arithmetic (medianthe 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,qfollows NumPy 2.4.2 (an intqpicks the element, a 2-Dq, arithmetic inq's dtype), the nan forms return NumPy's axis order and positive NaN, complexmedianof 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.aroundwithdecimals != 0portPyArray_Round- negative decimals threw, complex values stayed unrounded, float32 rounded in double, and the result dtypes follow NumPy.sum/prodof 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 inareturnedTrue; NumPy returnsFalseunlessequal_nan.mod/floor_divide/fmodby zero give ±inf like NumPy (they gave NaN), and float16add/subtract/multiply/divideon theout=/where=/ strided paths compute in float32 like NumPy's HALF loops (they double-rounded through double).maximum/minimumof float16 with float32/float64 keep the NaN's bits.np.linalg.eig/eigvalsof a float32 matrix with complex eigenvalues return NumPy's complex64 values (they kept double precision).np.real/np.imag/.real/.imag,view(dtype)andgetfielddid not keep their owner's buffer alive, so after the owner was disposed (as insideeigvals) the next allocation overwrote them;resize(refcheck: true)now sees such views.np.convolve/np.correlatewithout the backend are byte-exact on float32 positions under 32 terms and complex128 positions under 8 (NumPy's scalarsdot/zdoturegimes), and complex products with inf/NaN follow NumPy (convolve([1+0j], [inf+0j])isnan+nanj).np.arctan2(complex)corrupted the heap and crashed the process; it raises NumPy'sTypeError.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'srepr(data type "a'b" not understood).np.min_scalar_typeof an array returnednull; it answers the array's dtype, and a 0-d array by its value.np.common_type()with no array is float16,np.mintypecoderaises NumPy'sValueErrorfor a code outside its size table,np.result_typeof oneulongpast int64 is uint64, andNonearguments tocan_cast/promote_typesraise NumPy'sTypeErrors.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'snp.dtype(None)(it threwArgumentNullException), and a nullnp.result_typeoperand is NumPy'sNone, a float64 operand (np.result_type((DType)null, np.int8)is float64).np.find_common_typeis NumPy 1.26.4's, the last NumPy that has it: each list's type is the first of?bBhHiIlLqQefdgFDGOthat 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 throwsOverflowExceptionfor a uint16 array with an int16 0-d array.np.asanyarrayof mixed scalars promotes in element order like NumPy ([int8, uint8, float16]is float32; it was float16), and a float16 among other types converts (it threwInvalidCastException).cbrt,floor,ceil,trunc,deg2rad,rad2deg,floor_divideandmodraises NumPy'sTypeError(it raisedNotSupportedException, and a zero-size complex input returned an empty array);IncorrectTypeExceptionnow derives fromTypeError.np.modfaccepts int, float16 and bool input like NumPy (it rejected them), float16 byte-exact.np.traceof uint8/uint16/char accumulates in uint64 like NumPy;ndarray.flatof a 0-d array has shape(1,);np.take,ndarray.item()andlen()of a 0-d array raise NumPy's verbatim texts.np.argsortof an empty array with sort axis >= 2 overran the heap.flags.owndata), like NumPy.!(logical not) on a non-contiguous array returned wrong values.np.choose,tofile,tobytes,ogrid,poly1d,polyfit,multivariate_normal,apply_along_axis/apply_over_axes,flatiterslice/fancy access, cross-dtypeSetDataand the float16sum/meantemporaries;NDArray.ReplaceDatacould free another array's buffer.Generatorparity (it shipped in 0.70.0).integerswith ahighpast the dtype returned constant data (integers(0, 257, dtype: np.uint8)was all zeros); it raises NumPy's error.lock, as in NumPy.choicewithout asizereturns one element (it returned shape(1,));permutedfollows memory order and casts intoout=safely; a-0.0scale, shape or range is rejected; a()size keeps float32.RandomStateparity.RandomState()took its seed fromEnvironment.TickCount, so instances made in the same millisecond were identical; it uses OS entropy.choiceignoredreplace=Falseand searched the probability CDF from the wrong side;randint(dtype: bool)crashed.legacy-distributions.cline for line:gamma(shape<1),binomial,negative_binomial,poisson,f,pareto,power,standard_cauchy,multinomial,hypergeometric,zipf,geometricandvonmisesused other algorithms.SeedSequence- a typednullseed array seeded the empty sequence (the same stream on every run; now fresh entropy, NumPy'sNone), andSeedSequence(new[] { -1 })silently seeded0xFFFFFFFFinstead of raising.result_typeof four or more operands dropped every other pair (result_type(int8, int8, float64, int8)was int8; it is float64).np.evaluate- flat and axisSum/Mean/Prod/Min/Maxfollow NumPy's own schedules bit for bit (they matched the value only), a flatMeanof 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.nd["1:3"]) re-parsed its pattern on every call because the staticRegexcache thrashed - 9-18 µs -> 0.6-2.8 µs per parse.NDArray.Scalarstalled 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.clipis NumPy's_clipand its ufunc call: an integer bound outside the dtype no longer wraps (np.clip(uint8, 0, 256)returned all 0 andnp.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 raisesOverflowError),min=/max=/where=exist with fromnumeric's keyword rule, the bounds,outandwherebroadcast together (a bound larger than the array, a largerout), anoutthat 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 givenoutis returned itself; the 4,170-caseclip_pyintand 2,302-caseclip_ufuncoracle tiers.where=and anoutthat overlaps an input no longer writes heap bytes into the masked-off positions (np.add(x[:-1], 1, out=x[1:], where=m)): the maskedoutis read-write as in NumPy, on everyout=/where=route and innp.evaluate.Shapeis laid out fresh, as NumPy'snp.empty(shape)is:new NDArray(dtype, shape),np.empty/np.zeros/np.ones/np.fulland everynp.randomsize:kept a stepped, offset or broadcast view's strides over a buffer ofsizeelements (default_rng(42).integers(0, 100, arange(20)["::4"].Shape)returned heap values andnp.polyval(p, a["::4"])crashed the CLR), and a transposed view'sShapeput a sampler's stream in column order.new NDArray(Array | IArraySlice, Shape, order)is NumPy'snp.ndarray(shape, buffer=, order=)and reads only the shape's dimensions: a view'sShaperead 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'sValueError.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 untypedreshapedoes: theShapeof 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-valuenp.insertoverloads no longer dispose the caller'sNDArray(its buffer went back to the pool, and a second call returned wrong values), andnp.choosereleases its cast choices and itsout=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-casenan_ordertier compares NaN payloads raw.np.max/np.min/np.nanmax/np.nanmin/np.ptpreturn 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-casehalf_minmaxtier.np.sortwrites 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.uniquereads 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.isinfollows NumPy's three paths;np.searchsortedchecks its sorter and reads offset views.np.sum/np.meanstart from the add identity, as NumPy's add.reduce does: a sum of only-0.0is+0.0on every route (one element, a length-1 axis;np.mean,np.average,np.nansum,np.nanmean,np.trapezoid, einsum's sums andnp.evaluatetoo). The flat float sums and means are NumPy's add.reduce bit for bit (NpyIter's call schedule withnp.getbufsize()fills and a pairwise sum per call; 106 of 268 benchmark results had other bits than NumPy's), adtype=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-casesum_identitytier.np.var/np.std(_var/_std) -ddof=anddtype=work at every size: 8,284 of 22,110 probed cases were grossly wrong, and 936 emptydtype=calls wrote past the result buffer; the 16,108-casevarstdtier, geomean x3.07.np.nanvar/np.nanstd- a 1-D axis gives a 0-d result (it was(1,)),keepdimsholds for every input, addofat 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 isx * conj(x), anddtype=exists; the 13,382-casenanvarstdtier, geomean x3.71.np.mean(_mean) - everydtype=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-casemean_loopstier, geomean x2.62 (a bool mean along axis 0 at 10M x0.08 -> x28).np.nanmean-keepdimsholds for integer inputs, float16 / float32 sum in their own dtype (float16 can overflow toinf), a complex sum divides as NumPy's complex loop does, anddtype=exists; the 5,017-casenanmeantier, geomean x5.82.np.all/np.anywithout=orwhere=run NumPy's reduce prologue before any work: a non-boolwhere=mask raises NumPy's safe-castTypeError(it was read by truth value), a read-onlyoutis reported first, then the mask, the axis andout's rank, and the reduction iterator's shape checks use NumPy's texts (a mask may not stretch the input or add axes); thereduce_kwargstier starts here with 4,752 cases.np.copytois NumPy'sarray_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-casecopyto_fulltier.np.power's float loops take NumPy's scalar-exponent fast path (1/x,1,sqrt(x),x,x*xfor -1, 0, 0.5, 1, 2) exactly where NumPy's calls see a zero exponent step: theout=/where=routes calledpowfor a 0-d exponent,(1,) ** (1,)took the fast path where NumPy callspow, a column exponent never took it, and float16 power ispowfwith no fast path (7,161 of 1,114,112 values differed); the 7,902-casepower_fasttier, fast-path cells geomean x6.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 newpp_fbcast_*cases in the binary oracle tiers.np.randomreturns NumPy's kind of array:Generator.choicewithout replacement,RandomState.choicewithout replacement but withp, andmultivariate_normalown 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.NpzFileis safe to share between threads: concurrent first loads of its members decoded each other's bytes; itsZipArchivereads run under one gate, with lock-free cache hits.NDW012, the dropped-NDArraywarning 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].NDW012costs a project that never namesNDArrayone memoized lookup per operation.NDArray(the holder analysis of a 2,500-file project that never namesNDArray13 s -> under 0.01 s,NDW012over 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).0.1in float32, an out-of-range integer literal raisesOverflowErrorin a binary ufunc and in every store (it wrapped), and only a literal's 0-d array is weak (an element read ornp.array(x)promotes as a NumPy scalar).Convertsspeaks culture-invariant NumPy text (under de-DE"1.5"read as 15), and every internal promotion is NumPy's order-independentresult_type.prepare_indexandarray_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),Nonestores NumPy's value, and the key array is never written.np.insertfollowPyArray_AssignArrayand 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).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.takearePyArray_PutTo/PyArray_TakeFrom(a values view read slots the put had written),np.argwhere/np.flatnonzero/np.unravel_indexreturn NumPy's views, andnp.placeconverts 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.dtype=andout=are NumPy's loops:np.cumsum/np.cumprod/np.prodcast the input first (a float16 or complex128dtype=crashed or read past the buffer), anout=reduces inpromote_types(out, a),np.cumsum/np.cumprodstore into everyout=NumPy's accumulate accepts (more trailing axes, an extent-1 input against a longer out, an empty input or out returned untouched),dtype=charruns uint16's loop anddtype=boolthe 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.nanprodarenp.sum/np.prodover_replace_nan(float16 was several ULP off) and no longer read freed memory or divide by zero over an empty axis; complexnp.prod/np.cumprodand float16np.prodmultiply in NumPy's forms.np.dot/np.vdot/np.vecdotcompute 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 annp.splitchild from its parent's start; the matmul family checks calls, casts andout=in NumPy's order and returns NumPy's output object; float16 products are NumPy's HALF loops.np.correlate/np.convolvecompute complex128 positions with NumPy'szdotuand readmodewith NumPy's converter.np.linalgruns NumPy's statements innorm/vector_norm/matrix_norm,cond,tensorsolve,tensorinv,cross, thehermitian=Trueforms,qr's modes,matrix_power,solve,multi_dotand the scalars ofdet/slogdet/lstsq, and answers empty operands without the OpenBLAS backend.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, andcharreads as uint16.np.fmax/np.fminat NumPy's SIMD and C-library positions, float16arctan2/log1p/expm1, float64hypot, models of the UCRT'slog1p/expm1/logaddexp, complexmaximum/fmaxties and the two-bound complexnp.clip.np.arangeisPyArray_ArangeObj(a float ramp in the dtype's own arithmetic, exactlongbounds),np.linspacetakes 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), andnp.frombufferisPyArray_FromBuffer.np.pad,np.delete,np.select,np.where,np.choose,np.compress,np.fill_diagonaland the broadcast family check and convert as NumPy does, with its texts (np.whereread an offset view from its buffer's start;np.broadcast_arraysmade a read-only operand writeable).np.averageis NumPy's statements (2,998 of 3,000 random cases byte-identical, 1,482 before; an empty axis crashed the process), andnp.cov/np.corrcoef,np.einsumand the legacynp.poly*functions answer text, integerdtype=,out=and N-D operands as NumPy does.np.fromstring/np.fromfileare NumPy's C scanners (630 of 2,000 random cases differed),np.loadtxtreads Python'sfloat()and NumPy'sDataSource,np.savetxt/ndarray.tofileformat with CPython's%, files open with Python's share mode, and a binarynp.fromfilepast 2 GB reads.np.format_float_positional/np.format_float_scientificare NumPy'sdragon4.c, a float64 prints NumPy's digits on .NET 10, and a text repr escapes as CPython 3.12 does.tolist()/AsStringArrayof a temporary,np.uniquepastArray.MaxLengthelements and a float16 element at the end of mapped memory on .NET 8 no longer read freed or unmapped memory.GC.AddMemoryPressureper view: 5,000 views disposed as they go ran 8-15 full collections, now none.(bool)of a one-element array is NumPy's truth test, a leading boolean mask over trailing extents of 1 selects rows,np.isclosetakes an infinite tolerance,view/getfieldrefuse a non-native descriptor,np.interp's complex NaN isnan+0j,np.nan_to_numrefuses a text fill,ndarray.astypechecks its order and casting first,np.mintypecodereads arrays, and exception texts and thenp.finfo/np.iinforeprs 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,finfoprintedmin=-3.402823E+038and uint64'siinfomaxlong.MaxValue).np.einsum/np.nested_iterscheck their arguments in NumPy's order:np.nested_itersconvertsflags/order/castingbefore it readsaxesor the operands, andnp.einsumrefuses anout=of the wrong extent before any cast check and plans an explicit path first (an incomplete one raises NumPy'sRuntimeError: Invalid einsum_path is specified; it was contracted anyway).np.polyvaltakes its first product inx's dtype, as NumPy'szeros_like(x)start does:np.polyval([1+1j], [inf])isnan+1j(wasnan+nanj) andnp.polyval([-0.0], int [-3])is+0.0(was-0.0).🧰 Testing & Tooling
numpy.polynomialunit (179,118 cases),np.ma(109,961),np.evaluate(29,893),np.emath,ndarrayinstance members, and complex128 / Decimal coverage, with a dtype-spread gate (at least 4 dtypes per op) and an applicability gate.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.np.randomoverload 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.numpy.ufuncmethod (reduce,accumulate,reduceat,outer,at,resolve_dtypes, read from the type) for every ufunc, including those ofnumpy.stringsandnumpy.char, and the methods numpy.ma's masked ufunc wrappers implement: 323 more API contracts.numpy.ma.MaskedArrayandnumpy.vectorizemembers to their NumSharp ownersNDMaskedArrayandVectorized(79 rows had read missing).numpy.dtypes,numpy.exceptions,numpy.typing,numpy.versionandnumpy.ma.mrecordshad 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.numpy.testingandnumpy.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 asToString()forstr()), the 923 members of NumPy's scalar types and the members of the remaining classes, withnumpy.ctypeslib,numpy.__array_namespace_info__andnumpy.lib.NumpyVersionnow 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.numpy.dtype/ dtype-class oracle replays 9,566 / 8,526 NumPy 2.4.2 records (Windows / Linux NumPy, one file per Clongmodel): 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 throughnp.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_typereplays 11,291 NumPy 1.26.4 cases, andnp.asanyarrayof mixed scalars 2,917 NumPy 2.4.2 orderings.[DoNotParallelize], and VSTest's result batch is 1000 (NumSharp.Tests.runsettings).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 ofrun_benchmark.py: 442 cells over the text dtypes at 1K / 100K / 1M, geomean x6.61, min x1.44.tools/facade-move/snapshot.csrecords 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.NDW020keeps exception texts culture-independent: a number,objector type parameter formatted without a format provider inside an exception message (an interpolation hole, a+operand,ToString(),string.Format/Join, aStringBuilderappend, anArgumentOutOfRangeExceptionactual 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 = errorin an.editorconfigturns it on).NDW021- a project that references NumSharp withPublishAot=true(the default of adotnet run app.csfile-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 throwingPlatformNotSupportedExceptionat the first array arithmetic;NumSharpDisableAotWarning=truesilences it.NuGet/login), so no long-lived API key is stored.💥 Breaking Changes
DTypereplacesSystem.Typeas the dtype currency.NDArray.dtypereturnsDType(wasType), andnp.float64,np.int32and the rest areDTypedescriptors (wereType);DType->Typeis explicit (a.dtype.type), anda.dtype.Nameisa.dtype.name.DType: theType/NPTypeCodetwins are gone (aType,NPTypeCodeor dtype string still converts implicitly), andlinspace/nancumsum'stypeCode:isdtype:.result_type,promote_types,find_common_type,common_type, ...) returnsDType, andisdtyperejectsissubdtype's vocabulary ("floating"), as NumPy does.finfo.dtype/iinfo.dtypereturnDType(wereNPTypeCode), and the pythonnet dtype maps takeDType.np.issubdtype's string argument is a dtype string, as in NumPy: the category names are gone (np.issubdtype(x, "integer")raisesdata type 'integer' not understood, andnp.issubdtype(np.float32, "float")is False because"float"is float64); pass the class,np.issubdtype(x, np.integer)."Safe"raisesValueError).np.dtype("<float64"),np.dtype("|bool")and NumSharp's own"<Int32"raisedata type '<float64' not understood(they were accepted). Before a code the prefix still works ("<f8",">i4").np.int32/np.int64report the local NumPy's LLP64 type numbers and chars (7/'l'and9/'q'; they were hard-coded to Linux's5/'i'and7/'l').NPY_TYPECHARis NumPy 2.4.2's:NPY_STRINGLTR2isNPY_DEPRECATED_STRINGLTR2,NPY_INTPLTR/NPY_UINTPLTRare gone (cast the character,(NPY_TYPECHAR)'n'), andNPY_VSTRINGLTR('T') is new.NPTypeCode.ToTypeCode(NPY_TYPECHAR)reads a character asnp.dtypedoes:'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).ToTYPECHARgives 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 theirdtype.charis'A'/'u', characters of their own thatnp.dtyperefuses as NumPy refuses a user dtype's (they were int64's'q'and S1's'c', sonp.dtype(d.char)built another dtype).ValueError(wasNumSharpException); it derives fromArgumentException, socatch (NumSharpException)no longer catches it.np.maximum/np.minimum/np.fmax/np.fmintakeoutas the third positional argument (NumPy's order), notdtype; passdtype:by name.np.logical_and/logical_or/logical_xor/logical_notreturnNDArray(wasNDArray<bool>), andnp.modf's tuple fieldIntergralisIntegral.ndarray.shape = ...raises NumPy'sAttributeErrorwhen the layout cannot be reshaped in place (it silently copied); usereshape.np.nanmax/np.nanminof 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'snp.random.MT19937(42)(throughSeedSequence), not likeRandomState(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, andPcg64StateDataisPCG64.State.RandomStatestreams ofgamma(shape<1),binomial,negative_binomial,poisson,f,pareto,power,standard_cauchy,multinomial,hypergeometric,zipf,geometric,vonmises(andgumbel/logistic/rayleighat scale 0) change to NumPy's values;binomial(int n)isbinomial(long n); inputs NumPy accepts (dirichlet([]), a NaN alpha,negative_binomial(p: 0)) no longer throw.long:randintdefaults to int64 (was int32),random_integers/permutation/choice/multinomialreturn int64,NumPyRandom.Seedis auint(wasint), andSeedSequence.generate_statereturns anNDArray(wasArray).Generator(bit_generator)(wasbitGenerator),PCG64(seed)and the other bit generators (wasseedSeq, now anyISeedSequence), anddefault_rng(seed)(wasbitGenerator/generator) - named-argument calls must be renamed.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.bmatreturnNDMatrix(was a plain 2-DNDArray):*is the matrix product, an index keeps two axes, andnp.*functions return NumPy's matrix results for it (np.sum(m, 0)has shape(1, N)); take.Afor the plain array.ValueError"shape mismatch: value array of shape (1,4) could not be broadcast to indexing result of shape (2,)" before anything is written (wasIncorrectShapeException), and a(1, N)value now fits an(N,)selection.TypeError, and a 1-d value whose length is neither 1 nor the number of true elements raises NumPy'sValueError.Shapeis immutable and the shape accessors return copies, as NumPy's tuples are: theShapeindexer has no setter (s[i] = vno longer compiles; build a newShape),a.shape,Shape.DimensionsandShape.Stridesreturn a copy on every read, andnew Shape(long[]),(Shape)long[]and every public method taking along[]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 useshape[i]/NDim), and newparams ReadOnlySpan<long>overloads ofreshape/reshape_unsafe/resizekeep inline calls at one allocation.Shape(long[] dims, char order = 'C', bool copy = true)andShape(long[] dims, long[] strides, bool copy = true)replace the two-parameter constructors (recompile: the old signatures are gone), andcopy: 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'sndarray.__new__: float64 and C order by default (they were float32 and'F'),orderis honored and validated without a buffer, the buffer is any bytes-likeobjectwhose bytes are read asdtype(its element type was the dtype),offsetandstridescount bytes, and the fourth positional argument isoffset(acharthere is a compile error: nameorder:). The wrap constructors read a larger buffer's prefix and raiseTypeErrorfor a smaller one (both raisedArgumentException), andflags.alignedis false for misaligned memory (np.frombuffer/getfieldat odd offsets too), sosetflags(align: true)can raise andnp.require(..., 'A')can copy; 167 of 185 probe cases match NumPy (21 did), gated by a 5,919-casendarray_ctortier.np.var/np.std/np.nanvar/np.nanstd/np.mean/np.nanmeanreturn NumPy's dtypes, shapes and values for every loop: a 1-D-axisnanvar/nanstdis 0-d (it was(1,)), float16 / float32 inputs are summed in their own dtype (a float16nanmeancan overflow toinf), an integer or boolmeanloop wraps, and a flat decimal mean is decimal (it was float64).np.medianinto an integer or boolout=stores NumPy's value, cast unsafely as NumPy'smean(out=out)does ([1, 5, 9]forarange(12.).reshape(3, 4)along axis 1; a nonzero median is True), andnp.ptpof a float input into an integer out raises subtract'sTypeError: 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'sInvalidCastException); integernp.ptpwraps in its dtype and widens exactly (np.ptp(int8 [127, -128], out=int64)is 255; it was -1).np.float_power(x, 2.0)ispow, no longerx*x; float16np.powerispowf(-0.0 ** 0.5is+0.0and-inf ** 0.5is+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).net8.0project,np.random.seed(null)andnew PCG64(null)no longer compile (CS0121; both compiled against 0.70.0): the overload that takes anullseed is chosen by[OverloadResolutionPriority], which only C# 13 and later honor. Callnp.random.seed()/new PCG64(), or set<LangVersion>13</LangVersion>;net9.0and later projects are not affected.stringis NumPy'sstr: the implicitstring->NDArrayconversion andnp.array(string)give a 0-dUarray (the text was parsed as numbers; parse withastype(np.float64)), a.npy<U1loads asU1(it loaded asChar), andnp.@charis thenumpy.charmodule (it still converts to the char dtype).doubleliteral compares in its own precision, an out-of-range integer literal raisesOverflowExceptionin a binary ufunc and in every store (a[0] = 1000on int8,np.full(3, -1, dtype: np.uint8),np.copyto(int8, 1000); they wrapped), a NaN literal into an integer array raises, aComplexone into a real arrayTypeError,'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).AxisErrorfor an out-of-range axis,IndexErrorfromnp.put/np.take/np.choose, the indexers andnp.einsum_path(an ellipsis without its operand, an explicit path past the operands),TypeErrorfor 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 anullstored into an integer array,ValueErrorfromnp.nditer's construction errors andnp.nested_iterswithout an operand, a zero slice step andnp.nonzeroof a 0-d array (they wereArgumentException,IndexOutOfRangeException,ArgumentOutOfRangeException,NotSupportedException,InvalidOperationExceptionorIncorrectShapeException);AxisError.ActualValueis the axis as invariant text ("-3";AxisError.Axiskeeps the number).np.sum/np.prod/np.mean/np.var/np.stdand themax/minfamily (and theirndarraymethods) have one overload each in NumPy's order, so a positional third argument isdtype/out(namekeepdims:); the genericnp.amax<T>/a.max<T>()andNDArray.amax/aminare gone;np.evaluate(expr, x)passesxas an operand (name@out:).__getitem__/__setitem__:NDArray's other indexers bindthis[params object[]]with the same results, anint[]key to__getitem__is an index array, andNDArray<T>'s indexer returnsNDArray<T>(its typed scalar indexers are gone:(double)a[0], ora.GetValue<double>(0)in a hot loop).TensorEnginesubclasses implement the new abstract members that carry each computing function;np.sum/np.meancallReduceAdd/ReduceMean; the LAPACK members (Cholesky...Svd) areDefaultLinalgEngine's kernels underTensorEngine.Linalg;BackendFactory.GetEngine()returnsTensorEngine.Default, results and views bind the engine that made them, and a null array operand raisesArgumentNullExceptionwhen the call starts.np.fftkeeps the input's memory order.np.setbufsizeare context-local under Python 3.12'sContextVarrules: a new thread starts at the defaults,Task.Runandawaitcontinuations see the caller's.NDScope.Disposeon a thread other than the one that opened the scope throwsInvalidOperationExceptionin every build (only a Debug assert guarded it): a hand-written scope held across anawaitwas reused on the continuation's thread, where its close freed the opening thread's later arrays. Close the scope before theawaitand open another after it, or mark the async method[NDScopedAsync]; a refused scope's arrays are left to the finalizer.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.tofileraise CPython's errors where they were lenient, andnp.fromstring/np.fromfileread 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).np.arangeraises NumPy's errors (a zero stepDivideByZeroException, an out-of-range boundOverflowException; it wrapped),np.asanyarrayof a C# object list reads integers as int64 and floats as float64,np.where/np.selectread C# literals as Python scalars (np.where(c, 7, 7)is int64), andnp.frombuffer's range checks raiseValueError.np.padreads its widths before its mode (a null mode isValueError; oneint[]entry per axis is refused, write anint[,]), a value that does not fit a view assignment ornp.insertraisesValueError(wasIncorrectShapeException), andnp.average/np.isin(assume_unique: true)answer as NumPy does.np.complex64/np.csingleare scalar classes (np.zeros(3, np.complex64)no longer compiles), andnp.longdouble/np.clongdouble/np.intc/np.longlongand their unsigned twins are classes of their own (equal to their twin, not the same object).np.sort/np.argsortvalidatekindand takeorder/stable,np.argsort's first parameter isa(wasnd), andnp.format_float_positional/np.format_float_scientifictakeobject xand astring trim.<U1imports as aU1view (was aCharcopy),np.bytes_as a 0-dS<n>, andFromNumpyDtypeStr/FromBufferFormatreturnDType.np.polyderis NumPy's recursion: a 2-Dpis differentiated along its first axis (np.polyder(np.ones((3, 2)))is[[2, 1], [2, 1]]; it raised), and a 0-dpraisesTypeError: len() of unsized objectat every order (m = 0returned it).np.isscalaranswers as NumPy does: aTypeis not a scalar (np.isscalar(typeof(int))wasTrue), andHalf,BigInteger,Char8andDateTime64are (they wereFalse).