feat(manipulation): flip/fliplr/flipud, rot90, permute_dims/matrix_transpose/mT, trim_zeros (+ fuzz-oracle & benchmark coverage)#619
Merged
Merged
Conversation
Implements the flip family as O(ndim) view constructors, matching NumPy
2.4.2 (numpy/lib/_function_base_impl.py flip, _twodim_base_impl.py
fliplr/flipud) probed behavior exactly.
API (Manipulation/np.flip.cs):
- np.flip(m, int? axis = null) — axis=null flips ALL axes (m[::-1,...,::-1])
- np.flip(m, int[] axis) — NumPy tuple-of-ints form; empty array
flips nothing (full unreversed view); null = flip all
- np.fliplr(m) — m[:, ::-1], requires ndim >= 2
- np.flipud(m) — m[::-1, ...], requires ndim >= 1
Implementation:
- Hot path builds the flipped view directly: negate the stride of each
flipped axis and advance the base offset by stride*(dim-1), then
Shape(dims, strides, offset, bufferSize) + Storage.Alias — the exact
Transpose pattern. No slice resolution, no data movement, no kernel
(NumPy's flip is likewise pure basic indexing; there is no loop).
- Cold path (0-d and size==0 arrays) routes through the general
Slice[] machinery: flip(0-d) yields the scalar back (NumPy's m[()]).
- Differentially verified BIT-IDENTICAL (values, dims, strides, offset,
C/F-contiguity, writeable, broadcasted, sliced flags) against the
m[..., ::-1, ...] slice path across 13 layouts x axis sets (50 checks):
C-contig 1/2/3/5-D, F-contig, transposed, stepped slice, offset view,
negative-stride input, full+partial broadcast, dim-1 axis, 1-element.
Downstream kernels consume the direct-built views (sum/add/copy/print).
NumPy semantics pinned (61-check parity battery, all probed on 2.4.2):
- normalize_axis_tuple order: the WHOLE axis tuple normalizes first
(AxisError reports the ORIGINAL negative axis, e.g. "axis -4 is out
of bounds for array of dimension 3"), duplicates checked after —
flip(a, (0,0,5)) raises AxisError, not ValueError("repeated axis").
- flip(0-d) / flip(0-d, ()) return the 0-d scalar; flip(0-d, 0) raises
AxisError "axis 0 is out of bounds for array of dimension 0".
- fliplr/flipud guards raise ValueError "Input must be >= 2-d." /
">= 1-d." verbatim (checked before any axis work).
- Views all the way down: write-through to the root (incl. flip of a
sliced view), double flip restores contiguity flags, flip of a
broadcast stays read-only (Alias writeability inheritance).
- All 15 dtypes preserved (Boolean..Complex incl. Char/Half/Decimal).
Perf (Release, best-of-rounds, 1M iters; NPY/NS ratio, higher = faster):
- axis-specified forms beat NumPy 1.1-2.6x (flip(a,ax) ~1.08x,
flip(a,(0,1)) ~2.5x, flip3d(ax) ~2.6x) — NumPy pays Python-level
normalize_axis_tuple there.
- bare flip/fliplr/flipud ~0.55-0.9x vs NumPy's C basic-indexing fast
path (~350-450ns): bounded by the house view-materialization floor
(bare Shape ctor + Alias + NDArray measured ~640ns), which every
view op shares. Same-process A/B: the direct path is ~2.8x faster
than the Slice[] machinery, making np.flip the fastest view op in
the codebase (beats transpose 1136ns and expand_dims 1253ns).
Tests (test/NumSharp.UnitTest/Manipulation/np.flip.Test.cs, 46 methods):
axis variants incl. tuple/negative/empty, view write-through, broadcast
read-only, transposed/stepped inputs, 0-d/empty/1-element, verbatim
error messages + precedence, all-15-dtype preservation + round-trip,
fliplr/flipud values, equivalences flip(m,0)==flipud(m) and
flip(m,1)==fliplr(m). Full suite: 11,452 passed / 0 failed (net8.0);
flip tests green on net8.0 + net10.0.
Docs: added flip/fliplr/flipud to CLAUDE.md Shape Manipulation section
(+ semantics note) and website-src/api/index.md Shape Manipulation table.
….mT (NumPy 2.4.2 transpose aliases)
Implements the NumPy 2.x / Array-API transpose aliases, both pure O(1) views
(metadata-only stride/shape permutation, no data copy) exactly as NumPy does.
## np.permute_dims(a, axes=null)
- Straight alias of np.transpose (in NumPy 2.4.2, 'np.permute_dims is np.transpose'
returns True). Delegates to a.TensorEngine.Transpose(a, axes) so it inherits the
full transpose behavior: axis reversal by default, explicit permutation, negative
axis indices, empty arrays, read-only/broadcast preservation, and the verbatim
'repeated axis in transpose' / "axes don't match array" errors.
- Parameter named 'axes' to match NumPy's documented public signature.
## np.matrix_transpose(x)
- Swaps the two innermost axes: (..., M, N) -> (..., N, M), i.e. swapaxes(x, -1, -2)
(matches NumPy's fromnumeric.py which composes swapaxes). Returns a view.
- Requires ndim >= 2, else ArgumentException with the NumPy-verbatim message
'Input array must be at least 2-dimensional, but it is {ndim}'.
- SwapAxes normalizes the negative axes via check_and_adjust_axis, so the last two
dims are swapped for any rank; empty and non-contiguous inputs handled by the
existing Transpose view path.
## ndarray.mT property
- Companion to the existing ndarray.T; same operation as np.matrix_transpose but
raises NumPy's DISTINCT message 'matrix transpose with ndim < 2 is undefined'
(the two entry points genuinely differ in NumPy 2.4.2 - both probed).
Not added: ndarray.permute_dims()/matrix_transpose() methods (NumPy exposes neither -
only the mT property), and np.linalg.matrix_transpose (NumSharp has no np.linalg
namespace; it is redundant with np.matrix_transpose).
## Validation
All outputs, shapes, view/write-through semantics, read-only preservation on
broadcast views, and both error messages verified bit-identical against NumPy 2.4.2
across the variation matrix: C/F-contiguous, negative-stride, sliced, transposed/
non-contiguous, 0-d/1-d, empty (0x3, 2x0x3), higher-rank (5-D), and broadcast views.
## Tests
- test/.../np.permute_dims.Test.cs (8): reverse 2-D/3-D, explicit & negative axes,
1-D unchanged, alias-equivalence to transpose, repeated-axis & wrong-length errors.
- test/.../np.matrix_transpose.Test.cs (11): 2-D/3-D/higher-rank, swapaxes equivalence,
view write-through, empty arrays, broadcast read-only, ndim<2 errors (function + mT),
and the mT property.
All 19 pass on net8.0 and net10.0; 23 existing transpose/swapaxes tests still green.
Implement np.rot90(m, k=1, axes=(0,1)) — rotate an array by 90°·k in the
plane of two axes, counterclockwise from the first axis toward the second.
Port of NumPy's numpy.rot90 (numpy/lib/_function_base_impl.py). Like NumPy
it is a PURE VIEW COMPOSITION of axis-flips + a transpose — no data moves,
so the result always shares memory with the input (and stays read-only when
the source is, e.g. a broadcast view). This makes rot90 an O(1) metadata op
regardless of array size.
Implementation (src/NumSharp.Core/Manipulation/np.rot90.cs):
* k is reduced Python-style (((k % 4) + 4) % 4) so negative/large k wrap.
* k==0 -> full view (Storage.Alias of the same Shape), matching m[:].
* k==2 -> flip both axes.
* k==1 -> transpose(flip(m, axis1), axes_list).
* k==3 -> flip(transpose(m, axes_list), axis1).
* A private FlipAxisView(m, axis) builds NumPy's flip(m, axis) primitive as
a negative-stride view: negate the axis stride and advance the offset to
that axis' last element (no copy). Empty arrays return a fresh same-shape
array (consistent with how DefaultEngine.Transpose handles size==0), and
a stride-0 (broadcast) axis flips to a no-op exactly as in NumPy.
* Single dtype-agnostic path — works for all 15 NumSharp dtypes with no
per-dtype specialization (a view op needs none).
Validation mirrors NumPy verbatim (ArgumentException as the ValueError
analogue), including the quirky check ORDER:
* len(axes)!=2 -> "len(axes) must be 2."
* axes[0]==axes[1] || |axes[0]-axes[1]|==ndim -> "Axes must be different."
(fires BEFORE the range check, so axes=(0,2) on 2-D and axes=(0,3) on 3-D
report "different", not out-of-range; 1-D default axes and same-axis via
mixed signs like (0,-2) are caught here too)
* otherwise out-of-range -> "Axes=(a, b) out of range for
array of ndim=N."
Validated against NumPy 2.4.2:
* 76-case differential oracle (data + shape + writeable flag + verbatim
error text) across 2-D/3-D/4-D, negative & large k, negative/reversed
axes, strided/transposed/broadcast/empty inputs — all match.
* All 15 dtypes preserve dtype and rotate identically; view write-through
reaches the parent for every k; broadcast input yields a read-only result.
* Benchmarks (Release): NumSharp 1.3-1.6 us/call vs NumPy 3.1-4.2 us/call =
2.2x-2.6x faster (NPY/NS), well past the 1.5x bar; constant in array size.
Tests:
* test/NumSharp.UnitTest/Manipulation/np.rot90.Test.cs — 25 tests (values,
shapes, k-modulo, axes, view semantics, read-only broadcast, all-dtype
preservation, empties, and error parity), green on net8.0 and net10.0.
* Woven into the differential-fuzz manip tier: gen_oracle.py adds rot90
k=1/2/3 (its three distinct code paths) to gen_manip, OpRegistry.cs
replays it, and the regenerated corpus/manip.jsonl carries 798 new
rot90 cases (incl. Char-woven). The full FuzzMatrix gate is bit-exact.
CLAUDE.md: add rot90 to the Shape Manipulation inventory with a behavior note.
…ty axes + error types)
Second-pass edge-case hardening for the transpose aliases. All behavior probed
side-by-side against NumPy 2.4.2; results are now byte-identical except for the
pre-existing codebase-wide AxisOutOfRangeException number formatting (parens).
## Bug fixed: empty axes was wrongly treated as 'reverse'
DefaultEngine.Transpose treated an explicit length-0 'premute' (new int[0]) the
same as null and REVERSED the axes. NumPy distinguishes the two: axes=None reverses,
but an explicit length-0 permutation axes=()/[] matches ONLY a 0-d array and raises
'axes don't match array' for any ndim >= 1.
Before: np.permute_dims(np.ones((2,3)), new int[0]) -> (3,2) [WRONG: reversed]
After : np.permute_dims(np.ones((2,3)), new int[0]) -> ArgumentException
np.permute_dims(np.array(5), new int[0]) -> scalar () [matches NumPy]
Fix: gate the reverse branch on 'premute == null' only; an empty array now falls
through to the length check. Verified safe for all internal callers (SwapAxes,
MoveAxis, RollAxis all pass proper-length arrays; the 0-d edge still yields a scalar)
and against the committed fuzz corpus (0 empty-axes transpose cases; OpRegistry only
calls transpose with no axes).
## Error TYPE parity: Exception -> ArgumentException
'axes don't match array' and 'repeated axis in transpose' were raised as bare
System.Exception; NumPy raises ValueError, whose established NumSharp analog is
ArgumentException (matches np.expand_dims' 'repeated axis' throw). Messages unchanged.
Out-of-range axes already raise AxisOutOfRangeException (NumPy's AxisError analog).
Shared by transpose/permute_dims/swapaxes/moveaxis/rollaxis - all remain green.
## Final NumSharp-vs-NumPy parity (probed 2.4.2), now identical:
axes=null -> reverse
axes=[] on 0-d -> scalar ()
axes=[] on ndim>=1 -> ValueError/ArgumentException 'axes don't match array'
wrong-length axes -> 'axes don't match array'
repeated / neg-dup -> 'repeated axis in transpose'
out-of-range axis -> AxisError/AxisOutOfRangeException 'out of bounds'
matrix_transpose <2d, mT <2d, empty stacks, all 15 dtypes -> exact
## Tests (net8.0 + net10.0)
- np.permute_dims.Test.cs: tightened repeated/wrong-length to ArgumentException;
added NegativeAxisDuplicate, OutOfRangeAxis, and EmptyAxes (0-d ok / ndim>=1 throws).
- np.matrix_transpose.Test.cs: added mT.mT round-trip and all-15-dtype view coverage.
- 39 transpose-family tests pass; FuzzMatrix differential gate green (0 failures);
the 6 failing AuditV2 tests are pre-existing (confirmed on baseline, unrelated).
…non-default axes
Second-pass hardening of np.rot90 driven by a 4,800-case randomized
differential fuzz against NumPy 2.4.2 (ndim 2-5, 9 dtypes, C/F/transposed/
strided/reversed/offset views and their combinations, non-default & negative
axes, and extreme k incl. int.MinValue/int.MaxValue).
Fix (src/NumSharp.Core/Manipulation/np.rot90.cs):
* The k==0 path returned m[:] via Storage.Alias(m.Shape) unconditionally.
For an EMPTY view with a non-zero offset that yields a zero-length offset
shape which downstream buffer ops (e.g. flatten) cannot materialize —
while the k=1/2/3 paths already returned a fresh same-shape array for
size==0 (via FlipAxisView / DefaultEngine.Transpose). k==0 now does the
same for empty inputs, making rot90's empty handling uniform across all k
and every result well-formed/flattenable. Non-empty k==0 is unchanged
(still a memory-sharing view, matching NumPy's m[:]).
* The fuzz surfaced this as 95/4000 "failures" that were in fact a PRE-
EXISTING, rot90-independent bug: flatten() on any empty view with a
non-zero offset throws ArgumentOutOfRangeException (reproducible via
arr["1:,1:,1:1"].flatten() with no rot90 involved). rot90 itself always
produced the correct shape; this change only stops its own output from
tripping that separate defect.
Tests (test/NumSharp.UnitTest/Manipulation/np.rot90.Test.cs, 25 -> 33):
* Rot90_NonDefaultAxes_OnTransposedInput, _NewAxisInput, _6D_ShapeParity,
_ExtremeK_WrapsLikePython (int.MinValue/MaxValue), _Chained_ComposesTo-
LargerRotation, _EmptySlicedOffsetView_IsFlattenable (regression for the
fix), _DoesNotMutateAxesArgument, _ResultUsableDownstream (arith + copy).
Fuzz corpus — widen the committed differential gate to NON-DEFAULT AXES
(previously only the default (0,1) plane was gated):
* gen_oracle.py gen_manip now emits rot90 with explicit axes: default plane
(0,1) k=1/2/3, the reversed plane (1,0) k=1 (axes[0] > axes[1]), and for
ndim>=3 a non-adjacent plane (0, ndim-1) and a negative-axis plane
(-1,-2). Woven into the Char tier as before.
* OpRegistry.cs replays the axes param (falls back to default when absent).
* corpus/manip.jsonl regenerated (deterministic; sha256-stable): 1,204
rot90 cases across 5 distinct axis planes x all layouts x all dtypes,
all bit-exact. FuzzMatrix gate green.
…3.4-361x faster)
Implements np.trim_zeros(filt, trim='fb', axis=None) with full NumPy 2.2.0+ parity:
the N-dimensional bounding-box form, not just the legacy 1-D trim. All behavior probed
against NumPy 2.4.2 and differential-fuzzed 400/400 bit-exact.
## API (mirrors NumPy)
- np.trim_zeros(NDArray filt, string trim = "fb", int[] axis = null) -- primary
- np.trim_zeros(NDArray filt, string trim, int axis) -- single-axis convenience
- trim: case-insensitive, 'f'=front / 'b'=back / "fb"|"bf"=both; validated with the
verbatim ValueError text ("unexpected character(s) in `trim`: '...'").
- axis: null => bounding box over ALL axes; an explicit axis/list trims only those (negative
wrap, out-of-range -> AxisOutOfRangeException ~ NumPy AxisError, repeat -> ArgumentException
"repeated axis in `axis` argument"); an EMPTY axis list or a 0-d input returns the input
unmodified (NumPy 'if not axis_tuple: return filt').
- Returns a VIEW (dtype & ndim preserved). All-zero / empty inputs collapse the trimmed axes
to length 0 with the trim spec ignored, matching NumPy (start == stop == 0).
## Semantics verified against NumPy 2.4.2
1-D fb/f/b/bf/FB; all-zero -> empty; single-nonzero; 2-D/3-D/5-D bounding box; axis int /
sequence / negative / empty / out-of-range / duplicate; 0-d scalar (unmodified) and 0-d
explicit-axis (AxisError); float (-0.0 is zero, NaN/inf are non-zero); complex (0+0j zero);
bool; strided / reversed / F-contiguous / broadcast inputs; view write-through.
## Implementation -- faster algorithm than NumPy's
NumPy computes argwhere(filt).min/max(axis=0), which materializes EVERY non-zero coordinate
(K x ndim) just to take per-dim min/max. The bounding box is SEPARABLE, so instead this takes
the first/last non-zero index of a 1-D projection, touching only the trimmed axes:
- 1-D : np.nonzero(filt)[0] (skip the projection)
- N-D : np.nonzero(np.any(filt, other_axes))[0] (reduce 'any non-zero' over the other axes)
then slices [start, stop) per trimmed axis (Slice[] -> a view). All loops run inside the
existing IL-generated nonzero/any/reduction kernels; no hand-written element loop, no per-dtype
switch. pos.size == 0 uniformly handles all-zero and empty.
## Performance (best-of, warm, -c Release; NPY/NS, >1 = NumSharp faster)
1d f64 N=1000 3.4x 1d i32 N=1000 6.3x 1d f64 N=10M 3.6x
1d sparse 1M 8.3x 2d 1kx1k fb 31x 2d 1kx1k axis1 67x
3d 100^3 fb 55x 3d 100^3 axis0 361x
Minimum 3.4x across every probed variation (NumPy pays the argwhere materialization we avoid).
## Tests (net8.0 + net10.0): 25 pass
1-D modes, all-zero/empty, 2-D/3-D bounding box, axis int/sequence/empty, errors (invalid trim,
repeated & out-of-range axis), 0-d, float NaN/-0.0, bool, all-15-dtype coverage, view
write-through, strided/reversed inputs. FuzzMatrix differential gate remains green (0 failures).
Second pass hunting for NumPy 2.4.2 divergences in np.trim_zeros. Found NONE behavioral
(1000/1000 differential-fuzz cases bit-exact) -- this commit locks the newly-probed edges
into regression tests. No source change: the first-pass implementation already matches.
## What was probed this pass (all confirmed bit-exact vs NumPy 2.4.2)
- Non-contiguous layouts: reversed rows / F-contiguous / transposed 2-D (np.any + np.nonzero
correctly honor strides), plus 600 randomized layout-transform cases (rev0/revLast/stride2/
transpose/fortran/revAll) x 8 dtypes.
- Zero-dimension shapes: (0,5), (3,0), (0,0), (2,0,3); and (0,5) axis=1 -> (0,0) (an untrimmed
but already-empty axis stays 0, matching NumPy's per-axis slice composition).
- All-nonzero -> full array (no trim); interior all-zero hyperplane preserved inside the box;
corner-only 2-D; 4-D bounding box.
- Value semantics across every dtype: NaN/inf are non-zero (kept), -0.0 is zero (trimmed),
complex 0+0j zero / 0+2j non-zero / NaN+0j non-zero; Char '\0', Half, Decimal value-correct.
- Broadcast (read-only) input trims to a view.
- Invalid trim with whitespace (" fb"/"fb "/" ") -> ArgumentException with the verbatim
"unexpected character(s) in `trim`: '...'" text (NumPy does NOT strip whitespace).
## Sole (intentional) divergence -- left as-is
trim=null: NumPy raises AttributeError ('NoneType' has no 'lower'); NumSharp coalesces null to
"" and raises ArgumentException("unexpected character(s) in `trim`: ''"). Both reject; NumSharp's
message is clearer than a raw NullReferenceException, and neither AttributeError nor
ArgumentNullException is a "more correct" analog to NumPy's, so no special-case added.
## Tests: +9 (25 -> 34), pass on net8.0 + net10.0
TwoD_NonContiguousLayouts, ZeroDimensionShapes, AllNonzero_ReturnsFull,
InteriorZeroHyperplane_Preserved, FourD_BoundingBox, NaN_IsNonZero_NotTrimmed,
BroadcastInput_TrimsToView, TrimSpec_Whitespace_Throws, HalfDecimalComplex_Values.
FuzzMatrix differential gate remains green (0 failures).
…oot cause)
Root-cause fix for the empty-view defect surfaced while hardening np.rot90.
flatten() on ANY empty array that carries a non-zero offset threw
ArgumentOutOfRangeException — e.g. arr["1:,1:,1:1"].flatten() with no rot90
involved. NumPy returns an empty array (shape (0,)); NumSharp crashed.
Cause (src/NumSharp.Core/Backends/Unmanaged/UnmanagedStorage.Cloning.cs):
An empty slice keeps its parent's offset while its own backing
IArraySlice.Count has already collapsed to 0. CloneData's contiguous
branch then ran InternalArray.Slice(_shape.offset, _shape.size) =
Slice(8, 0) on a zero-length buffer, and ArraySlice<T>.Slice throws when
start (8) > Count (0). ravel()/copy()/astype() route through NDIter.Copy
and were unaffected; only flatten() (via CloneData) hit the raw Slice.
Fix: CloneData short-circuits _shape.size == 0 up front, returning a fresh
zero-length buffer of the dtype — an empty array clones to an empty array
regardless of layout/offset, matching NumPy's flatten/ravel/copy/astype.
This is the general fix; every op that materialises an empty offset view
benefits, not just rot90.
With the root cause fixed, the two rot90 empty-array work-arounds from the
prior commit are no longer needed and are removed (src/.../Manipulation/
np.rot90.cs):
* k==0 goes back to the plain Storage.Alias(m.Shape) view (NumPy m[:]),
dropping the "return a fresh array for empty" special-case.
* FlipAxisView no longer special-cases empty; it just guards the offset
advance for a zero-length axis (dims[axis] == 0 has no last element).
It now returns a proper negative-stride VIEW for empty inputs too, so
rot90 stays a pure view composition — rot90(empty, k=0/2) now shares
memory like NumPy (k=1/3 still defer to Transpose's empty handling).
Tests:
* NDArray.flatten.UsingTests.Flatten_EmptyViewWithNonZeroOffset_ReturnsEmpty
— the general regression (flatten/ravel/copy/astype of a (3,2,0) offset
view + a 1-D arange(10)["5:5"]).
* rot90's Rot90_EmptySlicedOffsetView_IsFlattenable already exercises the
rot90 side across all k.
Verified: full suite 11,439 passed / 0 failed (net10.0); rot90 + flatten
38/38 on net8.0 and net10.0; the 4,800-case rot90 differential fuzz now
flattens EVERY result (incl. every empty) with 4000/4000 value + 800/800
error parity against NumPy 2.4.2.
…ack / cumprod
Detected via differential probing (41 ops x 16 degenerate-view recipes x 2
dtypes, NumSharp vs NumPy 2.4.2). After the CloneData fix the crash class was
clean (0 crashes), but the sweep surfaced a sibling family of empty-array
SHAPE/DTYPE divergences, all now proven and fixed:
1) np.expand_dims no-op on empty arrays (root cause of 3 bugs)
expand_dims had `if (a.size == 0 || a.Shape.IsEmpty) return a;`. IsEmpty is
the uninitialized-shape sentinel (hashCode==0) and is correct to pass
through, but `a.size == 0` is a GENUINE empty array (real dims, one == 0)
that must still gain the new axis. NumPy: expand_dims((0,3),0) -> (1,0,3);
NumSharp returned (0,3). Shape.ExpandDimension already handles empty shapes
correctly, so the guard was pure breakage. Removed `a.size == 0` from both
overloads.
Transitive fallout fixed for free (both build on expand_dims):
* np.stack([e,e]) concatenated instead of stacking — (3,0) gave (6,0)
where NumPy gives (2,3,0); now correct across 1-D/2-D/3-D empties.
* np.vstack of an empty 1-D array (via atleast_2d) — (0,) gave (0,)
where NumPy gives (2,0); now correct.
2) np.cumprod wrong shape AND dtype on empty (ReduceCumMul)
ReduceCumMul did `if (shape.IsEmpty || shape.size == 0) return arr;` —
returning the input verbatim, so a multi-dim empty kept its shape (cumprod
((0,3)) gave (0,3) where NumPy ravels to (0,)) and kept the source dtype
(no NEP50 accumulator widening: cumprod(empty int32) must be int64). The
sibling ReduceCumAdd already did this correctly. Mirrored its empty branch:
fresh array of the accumulator dtype, axis=None -> (0,), axis=k -> dims
preserved, and an out-of-range axis now raises (NumPy AxisError) instead of
a silent no-op. Non-empty path untouched.
Proof / probing: the degenerate-view differential sweep goes from
CRASH=0/shapeMismatch=64 to CRASH=0/shapeMismatch=0 (the residual 44
"artifacts" are sort/argsort where the oracle forced axis=None vs NumSharp's
default axis=-1; the 4 "permissive" are np.ptp where the probe called the
NumPy-2.0-removed ndarray.ptp()). expand_dims/stack/vstack/hstack/concat and
cumprod/cumsum now match NumPy bit-for-bit on (0,), (0,3), (3,0), (2,0,3).
Tests: ExpandDims_EmptyArray_StillInsertsAxis, Stack_EmptyArrays_AddsNewAxis,
VStack_Empty1D_Promotes, CumProd_EmptyArray_FlattensAndWidensLikeCumSum
(shape + NEP50 dtype + axis-error). Full suite 11,444 passed / 0 failed
(net10.0); new tests green on net8.0 and net10.0.
# Conflicts: # .claude/CLAUDE.md
# Conflicts: # .claude/CLAUDE.md
…rim_zeros into the differential-fuzz gate
The six manipulation functions merged into journey2 (from worktree-flip and
worktree-transpose-aliases) shipped without differential-fuzz coverage — only
rot90 (worktree-rot90) had extended gen_oracle.py + OpRegistry.cs. This closes
that gap so every newly-merged manipulation op is proven bit-identical to
NumPy 2.4.2 across the layout/dtype matrix, exactly like the pre-existing ops.
test/oracle/gen_oracle.py — gen_manip():
* flip (reverse ALL axes) added to the dtype-agnostic base job list; on a 0-d
fixture NumPy returns the scalar (m[()]), matching NumSharp.
* New `if nd >= 1:` block — flipud, single-axis flip (int overload), and the
value-dependent trim_zeros in its f / b / fb / axis=int forms. trim_zeros is
the one op here doing real work: the int/uint pools are front-loaded with 0
and the float pool carries 0.0/-0.0 amid nan/inf, so every trim spec
exercises genuine leading/trailing edge cropping (not a no-op pass-through).
* Extended `if nd >= 2:` block — fliplr, int[]-axes flip, permute_dims (the
transpose alias), matrix_transpose (swap last two axes), and trim_zeros with
an explicit int[] axis. permute_dims with an explicit axis-roll permutation
added under nd >= 3.
These ride the existing char_tier("manip") weaving, so Char coverage (the
uint16 proxy relabelled to char) is picked up automatically — no separate wiring.
test/NumSharp.UnitTest/Fuzz/OpRegistry.cs:
* Added the six op-name -> NumSharp-call cases, pairing 1:1 with the generator.
"axes" routes to the int[] overload, "axis" to the int overload, neither to
the default (axis=null / reverse-all) — matching each function's real signature
(np.flip int?/int[], np.permute_dims int[], np.trim_zeros int/int[]).
test/NumSharp.UnitTest/Fuzz/corpus/manip.jsonl:
* Regenerated deterministically with numpy==2.4.2 (`python gen_oracle.py manip`).
6,510 -> 10,290 cases: flip 966, fliplr 266, flipud 336, permute_dims 336,
matrix_transpose 266, trim_zeros 1,610 (rot90 1,204 unchanged). The wide diff
is the global case-id counter renumbering, not semantic churn.
Gate: `dotnet test --filter TestCategory=FuzzMatrix` — FuzzCorpusTests.Manip and
all 40 op-corpus tiers pass (0 failed) bit-exact against NumPy 2.4.2. No
divergences surfaced, so nothing added to MisalignedRegistry / OpenBugs.
…x_transpose/trim_zeros into the op matrix
The seven manipulation functions merged into journey2 shipped with performance
claims (all pure O(1)/O(ndim) views except trim_zeros, which advertised a
3.4-361x edge over NumPy) but no benchmark-harness coverage, so nothing kept
those claims honest or caught regressions. This adds the C# + NumPy twin so the
official op matrix (benchmark/run_benchmark.py) measures them every run.
benchmark/NumSharp.Benchmark.CSharp/Benchmarks/Manipulation/FlipRotBenchmarks.cs (new):
* BenchmarkBase (float64, [Params(Medium, Large)] = 100K/10M) mirroring the
sibling ReshapeBenchmarks/DimsBenchmarks. Seven [Benchmark] methods on a 2-D
array — Flip/FlipLr/FlipUd/Rot90/PermuteDims/MatrixTranspose/TrimZeros.
* In namespace ...Benchmarks.Manipulation, so run_benchmark.py's existing
"manipulation": "*Benchmarks.Manipulation.*" filter picks it up with no
orchestrator change. Descriptions ("np.flip(a)", ...) normalize 1:1 onto the
NumPy names for the (op, dtype, N) merge join.
benchmark/NumSharp.Benchmark.Python/numpy_benchmark.py:
* Seven NumPy twins appended to run_manipulation_benchmarks (already wired into
main()'s "manipulation"/"all" dispatch and run_benchmark.py) — same float64
arr_2d, names "np.flip".."np.trim_zeros" that normalize onto the C# labels.
Smoke-tested (the chosen scope — no full measured run):
* `dotnet build -c Release` of the benchmark project succeeds.
* BenchmarkDotNet `--list flat` discovers all 7 FlipRotBenchmarks methods (so
the official run includes them). The out-of-process toolchain can't run here
(sibling worktrees hold same-named projects — the official run uses
InProcessEmit), so discovery + a direct-call execution smoke stand in.
* Direct-call smoke: all 7 bodies execute on a (100,100) float64 array and
return the expected shapes/dtype.
* NumPy side: `numpy_benchmark.py --suite manipulation` emits all 7 rows —
the six views at ~0 ms (O(1)) and np.trim_zeros at ~0.96 ms on 100K (the
O(N) nonzero bounding-box scan, where NumSharp's separable box should lead).
Correctness of the ops themselves is proven separately by the differential-fuzz
gate (prior commit). A measured NPY/NS run + benchmark/history snapshot remains
the post-release benchmark.yml ritual.
… (int? axis primary), closing the scalar-axis gap
Signature-parity audit of the seven manipulation members merged into journey2
against NumPy 2.4.2 (inspect.signature):
np.flip(m, axis=None) <- flip(m, int? axis=null) [+ int[] tuple overload] EXACT
np.fliplr(m) <- fliplr(m) EXACT
np.flipud(m) <- flipud(m) EXACT
np.rot90(m, k=1, axes=(0,1)) <- rot90(m, int k=1, int[] axes=null) EXACT*
np.permute_dims(a, axes=None) <- permute_dims(a, int[] axes=null) EXACT
np.matrix_transpose(x, /) <- matrix_transpose(x) EXACT**
ndarray.mT (get-only property) <- NDArray mT { get; } EXACT
np.trim_zeros(filt, trim='fb', axis=None) <- was int[]-axis canonical FIXED (below)
* axes default is null (resolves to (0,1)); C# can't default an array literal.
** x is positional-only in NumPy ('/'); C# has no positional-only parameters.
Parameter names, order, and defaults matched NumPy exactly for all seven. The
int-vs-int[] pairs (flip, trim_zeros) are the necessary C# modelling of NumPy's
single polymorphic `axis` (int | sequence | None) — one param can't be all three.
The one genuine defect: trim_zeros made the **int[]** overload the defaulted/
canonical one, so a scalar axis with the default trim — NumPy's common
`np.trim_zeros(filt, axis=0)` — did not compile (CS1503: int -> int[]), and it
was inconsistent with how flip is structured. Fixed by mirroring flip:
- primary : trim_zeros(NDArray filt, string trim = "fb", int? axis = null)
— reads exactly like NumPy's trim_zeros(filt, trim='fb', axis=None);
delegates to the sequence overload.
- sequence: trim_zeros(NDArray filt, string trim, int[] axis) [trim required]
— holds the implementation; trim is required so it can't collide with
the primary on trim_zeros(filt) / trim_zeros(filt, "f").
C# cannot make BOTH a scalar (int?) and a sequence (int[]) overload fully
defaulted (trim_zeros(filt) would be ambiguous), so exactly one bare-axis form
must carry trim. This picks the NumPy-common scalar `axis: 0` over the rare
1-element `axis: new[]{0}` (which now takes an explicit trim). No existing caller
is affected: OpRegistry and every unit test already pass trim with int[] axes.
Verified: build clean; 34 np.trim_zeros unit tests pass; the Manip differential-
fuzz gate stays green (same body, corpus unchanged); trim_zeros(filt, axis: 0)
now compiles and runs.
…y 2.4.2 parity)
Implements the three remaining `as*`/conversion functions from NumPy's creation
family, each verified 1-to-1 against NumPy 2.4.2 across the layout/dtype/edge-case
matrix (contiguous, C/F, strided, transposed, reversed, broadcast, empty, 0-D, all
15 dtypes). All three are pure additions — no existing code changed; full suite is
green (11480 passed, 0 failed) on net8.0 + net10.0.
── np.asarray_chkfinite(a, dtype=None, order='K') ───────────────────────────────
`asarray` then raise ValueError("array must not contain infs or NaNs") if the result
is a FLOAT-FAMILY dtype (Half/Single/Double/Complex — NumPy's typecodes['AllFloat'])
and holds any inf/NaN. Integer/bool/char/Decimal arrays skip the check (can't be
non-finite), exactly matching NumPy's `a.dtype.char in typecodes['AllFloat']` gate.
Complex is finite iff both real and imag are finite.
Perf: NumPy computes this as `np.isfinite(a).all()` — a full boolean temp plus a
second reduce pass. The naive NumSharp composition of that was 0.04–0.5x NumPy. We
instead FUSE both into one streaming pass with zero allocation
(Backends/Kernels/FiniteScan.cs):
* NaN-poison accumulation for float/double: `x - x` is +0 for a finite value and
NaN for ±inf/NaN, and NaN is absorbing under `+`, so `acc += (v - v)` stays +0
while everything is finite and poisons to NaN the instant any lane is non-finite.
One EqualsAll(acc, 0) decides the whole array — no per-vector branch, so the loop
streams at memory bandwidth. 4 independent accumulators break the FP-add chain.
Width-agnostic Vector<T> (one body serves f32 and f64; JIT bakes V128/V256/V512).
* Half: no Vector<Half> arithmetic in the BCL, so scan the raw 16-bit pattern —
±inf/NaN ⟺ the 5 exponent bits are all set ((bits & 0x7C00) == 0x7C00), SIMD on
Vector<ushort>.
* Complex: a contiguous Complex[N] is 2·N interleaved doubles, so it reinterprets
straight into the double scan.
* Layout coverage: a dense C-/F-contiguous buffer takes the flat SIMD path; strided/
transposed/negative-stride/broadcast views take an incremental-offset odometer that
still SIMD-scans any contiguous inner axis. Genuinely-sparse inner strides use an
AVX2-gather NaN-poison path; |stride|==1 (reversed) is recognized as a contiguous
block (finiteness is order-independent) and scanned forward — no wasteful backward
gather.
Result vs NumPy (NPY/NS, >1 = NumSharp faster, arrays pre-created both sides, Release):
contiguous f64 1.7x · f32 3.0-3.3x · f16 17-23x · c128 2.0-6.4x
reversed / row-strided 1.8-3.4x
sparse [::2] 1.17-1.23x (gather-throughput bound — NumPy hits the same wall)
Faster than NumPy on every probed variation.
── np.require(a, dtype=None, requirements=None) ─────────────────────────────────
Parses C/F/A/W/O/E requirement flags (+ NumPy's alias table), resolves an order ('A'
default, else 'C'/'F'; both ⇒ ValueError "Cannot specify both C and F order"), routes
through asarray, then makes ONE copy (in the resolved order) if any remaining
ALIGNED/WRITEABLE/OWNDATA flag is unsatisfied. No requirements ⇒ asanyarray. In
NumSharp ALIGNED is always true (managed allocations), so only WRITEABLE (broadcast
views) and OWNDATA (views) force a copy — verified to match NumPy's flag results
across 19 view/copy scenarios incl. the subtle 1-D-is-both-C-and-F and reshape/slice
OWNDATA cases. Single-string `requirements` iterate BY CHARACTER like NumPy, so "F"
is the Fortran flag, "CF" requests both (and raises), and "F_CONTIGUOUS" as one string
raises on '_' (pass a single-element array for the full alias). 'ENSUREARRAY' is
accepted and stripped (NumSharp has no ndarray subclasses to demote). The only divergence
is the invalid-token exception type (NumPy KeyError → NumSharp ValueError; both throw).
── np.asmatrix(data, dtype=None) ────────────────────────────────────────────────
Returns a 2-D VIEW (no copy) following NumPy's matrix.__array_finalize__ exactly:
0-D→(1,1), 1-D of length N→(1,N), 2-D unchanged, >2-D drops length-1 axes and must land
on 2-D else ValueError("shape too large to be a matrix."). The view is preserved for
strided/transposed/reversed inputs via stride-preserving Shape.ExpandDimension; a dtype
change casts first (a copy), then coerces. NumSharp has no `matrix` subclass (NumPy's is
pending-deprecated), so the special operators (`*`-as-matmul, `**`-as-matpow, .H, .I) are
intentionally NOT provided — only the conversion/shape semantics. Also parses matrix
strings ("1 2; 3 4"): rows split on ';', columns on commas/whitespace, brackets stripped,
integer-typed unless any element needs floating point, ragged rows ⇒ ValueError("Rows not
the same size.").
Tests: Creation/np.{asarray_chkfinite,require,asmatrix}.Tests.cs (74 cases). Docs:
registered in .claude/CLAUDE.md Array-Creation list.
…finite/require/asmatrix
Adversarial second pass against NumPy 2.4.2 across special float values, SIMD/gather
boundaries, exotic layouts, flag combinations and string-parse corners. Two behavior
gaps found and fixed; everything else already matched and is now pinned by 23 new tests
(97 total across the three functions; full suite 11503 passed / 0 failed on net8+net10).
Fixes
─────
* asmatrix: an element-free matrix string ("" or " ") now infers **Double**, matching
NumPy's `np.array([[]])` → float64 empty-array default (was Int64 — the integer path was
taken because there were no float tokens to trip the inference).
* require: added the **`like`** keyword (NumPy's `require(a, dtype, requirements, *,
like=None)`) for signature parity — accepted as a no-op, same as np.asarray's `like`
(NumSharp has no __array_function__ dispatch).
Verified-and-already-correct (no code change needed), now covered by tests
─────────────────────────────────────────────────────────────────────────
asarray_chkfinite:
* Exhaustive position sweep — NaN/inf placed at EVERY index for sizes 1..129 across
f64/f32/f16/c128 on contiguous, strided [::2] and reversed views: 0 mismatches. Proves
the 4×-unrolled SIMD body, 1-vector remainder and scalar tail all detect a non-finite
wherever it sits, and that the gather path reads only selected elements (a NaN parked on
a skipped odd index does NOT false-positive).
* Signaling NaN detected (f64/f32 via `x-x` poison; f16 via the 0x7C00 exponent mask);
signed zero, subnormals and max-finite all pass — matches NumPy bit-for-bit.
* Exotic layouts all match: 5-D full/transposed/strided (strided correctly skips a NaN it
doesn't select), column-broadcast (inner stride 0), scalar-broadcast, F-contiguous +
F-slice, stride-3 gather hit/skip, partial (1,N)→(M,N) broadcast, 2-D [::-1,::-1].
* The finiteness check runs AFTER the dtype cast: f64→f32/f16 values that overflow to inf
during narrowing raise, and NaN cast to an integer dtype passes (int skips the check).
require:
* ~30 flag combinations on C-contig views and F-contig owners match NumPy's C/F/OWNDATA/
WRITEABLE flags and same-vs-copy outcome exactly — including 'O'/'W' forcing a copy even
when the order flag is already satisfied, lowercase + full-name aliases, duplicate flags,
broadcast (row and column) copying to C-contig, and empty-array + dtype-with-order paths.
asmatrix:
* Degenerate empty >2-D (e.g. (0,3,1)) raises like NumPy's reshape error; 0-D from integer
indexing preserves its offset (reads the right element); non-contiguous 2-D shares memory;
1-D broadcast → (1,N) stays non-writeable; multi-singleton squeeze (1,3,1,4,1)→(3,4);
string tokens with signs / leading dot / scientific notation type-infer like NumPy.
…(NumPy 2.x parity)
Implements the four missing stacking/joining functions, all probed against
live NumPy 2.4.2 and pinned by 114 unit tests (66 new + 2 concatenate layout
regressions + existing 68 concatenate staying green).
np.block (Creation/np.block.cs) — full port of numpy/_core/shape_base.py:
- _block_check_depths_match walk with verbatim NumPy error texts:
ValueError "List depths are mismatched. First element was at depth N..."
/ "List at arrays[i] cannot be empty"; TypeError for ITuple ("arrays[i]
is a tuple. Only lists can be used to arrange blocks...").
- C# nesting map documented in xmldoc: object[]/NDArray[]/1-D primitive
arrays/IList = Python list; NDArray/scalars/rank>=2 rectangular arrays =
leaves. The walk normalizes the tree once (lists -> object[], leaves ->
NDArray) so later passes skip re-classification.
- BOTH NumPy algorithms behind NumPy's exact threshold
list_ndim * final_size > 2*512*512: _block_concatenate (repeated
np.concatenate along axes -1..-(depth); intermediate level results
disposed eagerly instead of queueing on the finalizer) and
_block_slicing (result_type over all leaves, order = 'F' if all-F and
not all-C, single allocation with fillZeros:false, per-leaf NDIter.Copy
into (Ellipsis,)+slice windows).
- _atleast_nd leading-1 padding as views (expand_dims chain; empty arrays
reshape since expand_dims early-returns them).
- Depth-0 (bare array/scalar) returns a COPY - probed; the NumPy docstring
claims otherwise but its _block_concatenate copies.
- Differential test reproduces a mixed-dtype 2x2 block matrix bit-exactly
against NumPy (MT19937 seed parity), and the slicing path is asserted
equal to the nested-concatenate reference at 600x800.
np.column_stack (Creation/np.column_stack.cs):
- NumPy's array(a, ndmin=2).T for sub-2-D inputs built as ONE storage
alias (dims (N,1)/(1,1), stride preserved so strided 1-D inputs stay
views), then concatenate(axis=1). ndim>=2 passes through untouched
(3-D+ concatenates along axis 1, like NumPy). Column-view wrappers are
released post-concat.
np.concat (Creation/np.concat.cs) — NumPy 2.0 Array-API alias:
- Mirrors every concatenate overload verbatim (array form with
axis/out/dtype/casting + the 8 tuple-arity conveniences).
np.unstack (Creation/np.unstack.cs) — NumPy 2.1:
- Returns NDArray[] of write-through VIEWS: drops the axis from
dims/strides and advances offset by i*strides[axis] per child - no
moveaxis call, no Slice machinery, no data movement. One shared
dims/strides pair serves all children (Shape treats them as immutable).
- ValueError "Input array must be at least 1-d." for 0-d, AxisError-parity
via check_and_adjust_axis, empty-axis -> empty result array, empties
elsewhere -> fresh empty children (nothing to alias).
np.concatenate layout-vote fix (NumPy parity):
- F-contiguous output now requires allF AND NOT allC, matching
PyArray_CreateMultiSortedStridePerm ambiguity resolution - inputs that
are BOTH C&F (e.g. (N,1) columns from column_stack) produce C order.
Previously NumSharp emitted F there. Probed: both-CF x2 -> C; F-only x2
-> F; mixed F-only+both-CF -> F; C+F -> C.
np.concatenate uniform-tiny-slab interleave fast path:
- (N,1)-style concats degenerate to outerCount*nsrc dynamic-size
Buffer.MemoryCopy calls (~4.5ns each through Memmove dispatch; 2M
8-byte copies for two 1M columns). When every per-outer slab is the
same 1/2/4/8/16 bytes, a constant-size load/store interleave loop runs
~10-20x faster (~0.2ns/copy). Applies at any outerCount (no setup
beyond the O(nsrc) uniformity check); works for both C- and F-path
addressing since it mirrors the existing slab accumulation.
UnmanagedStorage.GetData<T>() window fix (latent house bug, surfaced by
unstack, also affected np.split):
- Contiguity is offset-independent, so a CONTIGUOUS view at a non-zero
offset (np.split/np.unstack children) previously returned the WHOLE
backing buffer from Data<T>()/GetData<T>() - np.split(np.arange(6),3)[1]
.Data<int>() yielded all 6 elements. Now narrows to the shape's
[offset, offset+size) window first (zero-copy ArraySlice window, still
writes through), and the cross-dtype cast path casts only the window.
Benchmarks (Release, best-of-7, prompt disposal; ratio = NumPy/NumSharp,
>1 = NumSharp faster): block 600x600 5.55x, block 1-D 2.16x, block
100x100 1.02x, block scalars 0.97x, column_stack 2-D 3.22x, column_stack
1-D 1M 3.05x (was 0.84x before the interleave), 1-D 1K 0.83x, unstack
small 2.57x, unstack 16x1000 0.47x (view-construction bound: ~300ns per
NDArray+storage wrapper pair vs CPython's ~130ns PyArrayObject), concat
100K 6.24x. Sub-1.0 cases are allocator/wrapper-bound, not data-path.
Tests: full suite 11,483 green on net10.0 (0 failed) and the new-op +
split classes green on net8.0; FuzzMatrix corpus (112 concatenate + 336
stack-family cases) stays bit-exact through the layout + interleave
changes (corpus compares C-order logical bytes). Corpus generator NOT
extended: block needs nested-structure operand encoding and unstack a
multi-result schema, neither of which the harness supports today.
…ation
Adds WriteabilityMatrixTests — a 27-op matrix over a writeable owned source and a read-only
broadcast source, asserting Shape.IsWriteable against NumPy 2.4.2's flags['WRITEABLE'] for
every (source, op) pair, so the propagation rules established while hardening mmap can't
silently regress. Encodes the full rule set:
- a VIEW inherits its source's writeability (slice/index/reshape/ravel/T/swapaxes/moveaxis/
expand_dims/view-of-view read-only off a read-only source);
- reshape/ravel/flatten off a broadcast COPY (non-contiguous → writeable), off a contiguous
source are views (the contiguous read-only case is covered in NpyMemmapTests);
- copies / computed results (copy/astype/fancy/mask/arithmetic) are always writeable;
- diagonal is a read-only view even off a writeable source (NumPy parity, pinned explicitly);
- writing through a read-only view throws (flag is enforced, not cosmetic).
Documented_Divergences pins the two intentional, memory-safe differences from NumPy (not
propagation bugs — NumSharp returns a different kind of object and the flag is correct for it):
a[i,j] is a writeable 0-d view (NumPy: immutable scalar) and squeeze-of-broadcast is a
writeable copy (NumPy: read-only view). If either is ever "fixed" to match NumPy, this test
flags it for a conscious update.
5 tests green on net8.0 + net10.0.
These five methods lived on NDArray as instance methods, but in NumPy they
are numpy.linalg *module* functions, never ndarray methods (verified against
the vendored src/numpy: numpy/linalg/_linalg.py defines each as a top-level
`def`, and none is registered as an ndarray method in _add_newdocs.py). On top
of being in the wrong home, every one was an unimplemented stub — so per the
"delete what's mis-homed; if it isn't implemented don't bother relocating it"
decision, they are removed outright rather than moved to a np.linalg surface.
Removed source stubs (src/NumSharp.Core/LinearAlgebra/):
- NdArray.Inv.cs inv() -> return null; (real body commented out)
- NdArray.LstSq.cs lstqr(b, rcon) -> return null; (real body commented out)
- NdArray.multi_dot.cs multi_dot(...) -> return null; (real body commented out)
- NdArray.QR.cs qr() -> return default; (real body commented out)
- NdArray.SVD.cs svd() -> return default; (real body commented out)
SetData(object) -> throws NotImplementedException (dead
single-arg orphan; every live .SetData
call binds to the 2-arg overloads in
NDArray.cs / UnmanagedStorage.Setters.cs)
API-shape notes for whoever eventually implements the real np.linalg surface:
- "lstqr" is a misspelling of NumPy's lstsq; NumPy's is
np.linalg.lstsq(a, b, rcond=None) returning (x, residuals, rank, s) — a
4-tuple, not a single NDArray, and the param is rcond (not "rcon").
- multi_dot has the wrong call shape: NumPy's np.linalg.multi_dot(arrays,*,out=None)
takes a *sequence* and computes the optimal product order; the stub made the
receiver an implicit first operand.
Removed the four dedicated tests for these stubs (their [TestMethod]s were
already commented out, so they were dead scaffolding that only referenced the
deleted APIs):
- test/.../LinearAlgebra/NdArray.{Inv,LstSq,QR,SVD}.Test.cs
Fixed the one remaining caller: NumSharp.ConsumePackage/Program.cs was a
package-consumption smoke test calling A.lstqr(b); switched it to A.dot(b), a
real, NumPy-correct ndarray method that still validates the package loads.
Left in place: NDArray.matrix_power (also a numpy.linalg function by rights, but
it is actually implemented, so relocating it is a separate concern and out of
scope here). NDArray.dot / np.dot / np.matmul / np.outer are correctly placed
(dot genuinely is an ndarray method in NumPy; the others live in the main np
namespace).
Both NumSharp.Core and NumSharp.UnitTest build clean (0 errors).
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.
Overview
Adds a batch of NumPy 2.4.2-parity shape-manipulation functions — all pure O(1)/O(ndim) views except
trim_zeros— together with the differential-fuzz correctness gate and benchmark-harness coverage that keep them honest.New APIs
Reversal (
Manipulation/np.flip.cs) —np.flip,np.fliplr,np.flipudm[..., ::-1, ...]slice path for every layout (F-contiguous / transposed / stepped / broadcast; a flip of a read-only broadcast stays read-only).flip(m, axis=None)flips all axes;flip(m, int[])is the tuple form. 0-d:flip(m)returns the scalar,flip(m, 0)raises AxisError.fliplrrequires ndim ≥ 2,flipudndim ≥ 1 (verbatim ValueError messages).Rotation (
Manipulation/np.rot90.cs) —np.rot90(m, k=1, axes=(0,1))m.kis taken mod 4 (Python-style). Validation mirrors NumPy verbatim ("len(axes) must be 2.", then"Axes must be different."— fired before the range check).Transpose aliases (
Manipulation/np.permute_dims.cs,np.matrix_transpose.cs,Backends/NDArray.cs) —np.permute_dims,np.matrix_transpose,ndarray.mTpermute_dims(a, axes=None)— alias oftranspose.matrix_transpose(x)— swaps the two innermost axes (swapaxes(-1, -2)), requires ndim ≥ 2.ndarray.mT— same, raising NumPy's verbatim"matrix transpose with ndim < 2 is undefined".Bounding-box crop (
Manipulation/np.trim_zeros.cs) —np.trim_zeros(filt, trim="fb", axis=None)axis=Nonetrims the whole-array box; an int / int[] axis trims those axes only. Implementation is not NumPy'sargwhere.min/max(which materializes every non-zero coordinate) — the box is separable, so it takes the first/last non-zero index fromnp.nonzero(1-D) ornp.nonzero(np.any(filt, other_axes))(N-D). Measured 3.4×–361× faster than NumPy 2.4.2.Core fixes (empty-array NumPy parity)
expand_dims/stack/vstack/cumprodon empty arrays.CloneData/flattenof an empty offset view (root cause).transposeaxis-handling: an explicit length-0axes, wrong-length, and repeated / out-of-range axes now raise the NumPy-exact error types.Correctness gate — differential-fuzz oracle
All six new ops are wired into the NumPy-oracle fuzz pipeline (
test/oracle/gen_oracle.py+test/NumSharp.UnitTest/Fuzz/OpRegistry.cs), andmanip.jsonlwas regenerated withnumpy==2.4.2(6,510 → 10,290 cases; the Char tier is woven in automatically). TheFuzzMatrixgate proves every case bit-identical to NumPy 2.4.2 across the layout × dtype matrix — all 40 op-corpus tiers pass, 0 divergences.Benchmark coverage
Added
benchmark/NumSharp.Benchmark.CSharp/Benchmarks/Manipulation/FlipRotBenchmarks.cs(7 methods) plus their NumPy twins innumpy_benchmark.py, auto-included by the existingmanipulationsuite so the official op matrix measures them every run.Signature parity
Audited all added members against NumPy 2.4.2 (
inspect.signature); parameter names, order, and defaults match. Fixedtrim_zerosto expose anint? axis = nullprimary (mirroringflip) so NumPy's commontrim_zeros(filt, axis=0)call compiles.Verification
FuzzMatrixdifferential gate: green (bit-exact vs NumPy 2.4.2).trim_zeros/flip/rot90/permute_dims/matrix_transposeunit tests: pass.