Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4cd8467
feat(manipulation): add np.flip, np.fliplr, np.flipud (NumPy 2.x parity)
Nucs Jul 18, 2026
c761b46
feat(manipulation): add np.permute_dims, np.matrix_transpose, ndarray…
Nucs Jul 18, 2026
b1c546c
feat(manipulation): add np.rot90 (NumPy 2.x parity)
Nucs Jul 18, 2026
72df390
fix(manipulation): align transpose axis-handling to NumPy parity (emp…
Nucs Jul 18, 2026
d66135e
fix(manipulation): rot90 empty-array k=0 consistency + widen fuzz to …
Nucs Jul 18, 2026
fb98024
feat(manipulation): add np.trim_zeros (NumPy 2.4.2 N-D bounding box, …
Nucs Jul 18, 2026
f23cef9
test(manipulation): second-pass edge coverage for np.trim_zeros
Nucs Jul 18, 2026
636ca5b
fix(core): CloneData/flatten of an empty offset view (NumPy parity, r…
Nucs Jul 18, 2026
1fcd6f1
fix(core): NumPy-parity for empty arrays in expand_dims / stack / vst…
Nucs Jul 18, 2026
fbe8eb6
Merge branch 'worktree-flip' into journey2
Nucs Jul 18, 2026
bec1621
Merge branch 'worktree-rot90' into journey2
Nucs Jul 18, 2026
9a737e5
Merge branch 'worktree-transpose-aliases' into journey2
Nucs Jul 18, 2026
ca8672e
test(oracle): wire flip/fliplr/flipud/permute_dims/matrix_transpose/t…
Nucs Jul 18, 2026
7676d31
bench(manipulation): wire flip/fliplr/flipud/rot90/permute_dims/matri…
Nucs Jul 18, 2026
f67fc58
fix(manipulation): align np.trim_zeros overloads to NumPy's signature…
Nucs Jul 18, 2026
4ca53c8
feat(conversion): np.asarray_chkfinite, np.require, np.asmatrix (NumP…
Nucs Jul 18, 2026
1febfcb
test+fix(conversion): second-pass edge-case hardening for asarray_chk…
Nucs Jul 18, 2026
6e54fa7
feat(stacking): add np.block, np.column_stack, np.concat, np.unstack …
Nucs Jul 18, 2026
88bdb9d
test(view): pin the WRITEABLE (source × op) matrix as living document…
Nucs Jul 17, 2026
4825112
refactor(linalg): delete mis-homed, unimplemented NDArray linalg stubs
Nucs Jul 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,10 +238,37 @@ nd["..., -1"] // Ellipsis fills dimensions
Tested against NumPy 2.x.

### Array Creation
`arange`, `array`, `asanyarray`, `asarray`, `ascontiguousarray`, `asfortranarray`, `copy`, `empty`, `empty_like`, `eye`, `frombuffer`, `full`, `full_like`, `identity`, `linspace`, `meshgrid`, `mgrid`, `ones`, `ones_like`, `zeros`, `zeros_like`
`arange`, `array`, `asanyarray`, `asarray`, `asarray_chkfinite`, `ascontiguousarray`, `asfortranarray`, `asmatrix`, `copy`, `empty`, `empty_like`, `eye`, `frombuffer`, `full`, `full_like`, `identity`, `linspace`, `meshgrid`, `mgrid`, `ones`, `ones_like`, `require`, `zeros`, `zeros_like`

The `as*` conversion family mirrors NumPy: `asarray_chkfinite(a, dtype=None, order='K')` = `asarray` then raise `ValueError("array must not contain infs or NaNs")` if a **float-family** dtype (Half/Single/Double/Complex — NumPy's `typecodes['AllFloat']`; Decimal/int/bool skip the check) holds any inf/NaN, via a **fused single-pass NaN-poison SIMD reduction** (`Backends/Kernels/FiniteScan.cs`: `acc += v - v` — +0 for finite, absorbing-NaN for non-finite; AVX2 gather + reversed-contiguous fast path for strided/negative-stride views; ~2–27× NumPy contiguous, ≥1× strided). `require(a, dtype=None, requirements=None)` parses C/F/A/W/O/E flags (+aliases; single-string requirements iterate by char like NumPy, so `"F_CONTIGUOUS"` as one string raises), resolves an order and copies only if a remaining ALIGNED/WRITEABLE/OWNDATA flag is unsatisfied (ALIGNED is always true in NumSharp, so only broadcast-non-writeable and views force a copy). `asmatrix(data, dtype=None)` returns a **2-D view** (NumSharp has no `matrix` subclass — the deprecated NumPy one; no `*`-as-matmul/`.H`/`.I`): 0-D→(1,1), 1-D→(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.")`; also parses matrix strings (`"1 2; 3 4"`). See `Creation/np.{asarray_chkfinite,require,asmatrix}.cs`.

### Shape Manipulation
`append`, `array_split`, `atleast_1d`, `atleast_2d`, `atleast_3d`, `concatenate`, `delete`, `dsplit`, `dstack`, `expand_dims`, `flatten`, `hsplit`, `hstack`, `insert`, `moveaxis`, `pad`, `ravel`, `repeat`, `reshape`, `resize`, `roll`, `rollaxis`, `split`, `squeeze`, `stack`, `swapaxes`, `tile`, `transpose`, `unique`, `vsplit`, `vstack`
`append`, `array_split`, `atleast_1d`, `atleast_2d`, `atleast_3d`, `block`, `column_stack`, `concat`, `concatenate`, `delete`, `dsplit`, `dstack`, `expand_dims`, `flatten`, `flip`, `fliplr`, `flipud`, `hsplit`, `hstack`, `insert`, `matrix_transpose`, `moveaxis`, `pad`, `permute_dims`, `ravel`, `repeat`, `reshape`, `resize`, `roll`, `rollaxis`, `rot90`, `split`, `squeeze`, `stack`, `swapaxes`, `tile`, `transpose`, `trim_zeros`, `unique`, `unstack`, `vsplit`, `vstack`

`flip`/`fliplr`/`flipud` return **O(ndim) views** (stride negation + base-offset shift via `Storage.Alias`,
the Transpose pattern — no slice resolution, no data movement), bit-identical to the `m[..., ::-1, ...]`
slice path for every layout incl. F-contiguous/transposed/stepped/broadcast (flip of a read-only broadcast
stays read-only). `flip(m, axis: null)` flips ALL axes; `flip(m, int[])` is the tuple form (empty array
= flip nothing; the axes are normalized NumPy-style: full `AxisError` pass — reporting the original
negative axis — THEN `ValueError("repeated axis")`, so `(0,0,5)` raises AxisError). 0-d: `flip(m)` returns
the scalar (NumPy's `m[()]`), `flip(m, 0)` raises AxisError. `fliplr` requires ndim≥2 / `flipud` ndim≥1
(`ValueError("Input must be >= 2-d." / ">= 1-d.")` verbatim). See `Manipulation/np.flip.cs`.

`rot90(m, k=1, axes=(0,1))` rotates in the plane of two axes by `90°·k` (counterclockwise from the first axis toward the second). Like NumPy it is a **pure view composition** of axis-flips + a transpose — no data moves, so the result shares memory with `m` (read-only when the source is, e.g. a broadcast view). `k` is taken mod 4 (Python-style, so negatives wrap); `k∈{0}` returns a full view, `k∈{2}` flips both axes, `k∈{1,3}` flip-then-transpose. Validation mirrors NumPy verbatim (`ArgumentException`): `len(axes)!=2` → "len(axes) must be 2.", `axes[0]==axes[1] || |axes[0]-axes[1]|==ndim` → "Axes must be different." (this fires *before* the range check, so e.g. `axes=(0,2)` on 2-D reports "different", not out-of-range), else out-of-range. See `Manipulation/np.rot90.cs`.

`np.trim_zeros(filt, trim="fb", axis=null)` crops leading/trailing all-zero hyperplanes (NumPy 2.2.0+ N-dimensional bounding box; probed against 2.4.2). `trim` is case-insensitive ('f'=front, 'b'=back, "fb"/"bf"=both) and validated (`ArgumentException "unexpected character(s) in \`trim\`: '…'"`). `axis=null` trims the smallest bounding box over all axes; an explicit axis/axis-list (int overload + `int[]` overload) trims only those (negative + repeat/out-of-range checks → `ArgumentException "repeated axis in \`axis\` argument"` / `AxisOutOfRangeException`); an **empty** axis list or a **0-d** input returns the input unmodified. Always returns a **view** (dtype & ndim preserved); all-zero/empty inputs collapse the trimmed axes to length 0 (trim spec ignored, matching NumPy). Implementation is NOT NumPy's `argwhere.min/max` (which materializes every non-zero coordinate) — the bounding box is separable, so it takes the first/last non-zero index from `np.nonzero` (1-D) or `np.nonzero(np.any(filt, other_axes))` (N-D), touching only the trimmed axes. Measured **3.4×–361× faster than NumPy 2.4.2** (min on tiny 1-D, up to 361× on 3-D single-axis). Differential-fuzzed 400/400 bit-exact across 1–3D × {int/uint/float/bool} × sparsity × trim × axis. See `Manipulation/np.trim_zeros.cs`.

`permute_dims` and `matrix_transpose` are the NumPy 2.x / Array-API transpose aliases (both pure O(1) views). `np.permute_dims(a, axes=null)` is a straight alias of `transpose` (delegates to `TensorEngine.Transpose`). `np.matrix_transpose(x)` swaps the two innermost axes (`(..., M, N)→(..., N, M)`, i.e. `swapaxes(x, -1, -2)`) and requires `ndim ≥ 2`, else `ArgumentException "Input array must be at least 2-dimensional, but it is {ndim}"`; the companion property `ndarray.mT` does the same but raises the NumPy-verbatim `"matrix transpose with ndim < 2 is undefined"` (distinct message, both probed against 2.4.2). Read-only/broadcast inputs stay non-writeable through the alias. See `Manipulation/np.permute_dims.cs`, `Manipulation/np.matrix_transpose.cs`, `Backends/NDArray.cs` (`mT`).

`Transpose` axis-handling is NumPy-exact (probed against 2.4.2, shared by `transpose`/`permute_dims`/`swapaxes`/`moveaxis`/`rollaxis`): only `axes=null` reverses — an explicit **length-0** `axes` (`new int[0]`, NumPy's `()`/`[]`) is a length-0 permutation that matches **only a 0-d array** and raises `ArgumentException "axes don't match array"` for any `ndim ≥ 1` (NumPy's `ValueError`); wrong-length and repeated/negative-duplicate axes raise `ArgumentException` ("axes don't match array" / "repeated axis in transpose", NumPy `ValueError`); out-of-range axes raise `AxisOutOfRangeException` (NumPy `AxisError`). See `Backends/Default/ArrayManipulation/Default.Transpose.cs`.

**Stacking/joining details** (all probed against NumPy 2.4.2; tests in `Creation/np.{block,column_stack,concat,unstack}.Test.cs`):
- `concat` — NumPy 2.0 Array-API alias of `concatenate`; mirrors every overload (array form with `axis`/`out`/`dtype`/`casting` + tuple-arity forms). `Creation/np.concat.cs`.
- `unstack(x, axis=0)` (NumPy 2.1) — returns `NDArray[]` of **views** (shared memory; write-through), each `x.shape` minus the axis entry. Built by dropping the axis from dims/strides and advancing offset — no moveaxis call, no iterator. 1-D input → 0-d views; 0-d input → `ValueError("Input array must be at least 1-d.")` verbatim. `Creation/np.unstack.cs`.
- `column_stack(tup)` — sub-2-D inputs become columns via a single-alias `(N,1)` view (NumPy's `array(a, ndmin=2).T`), then `concatenate(axis=1)`; ndim≥2 pass through untouched (3-D+ works, like NumPy). `Creation/np.column_stack.cs`.
- `block(arrays)` — full port of `numpy/_core/shape_base.py`: depth-check walk (verbatim ValueError texts for depth mismatch / empty lists; `TypeError` for `ITuple`s), `_atleast_nd` leading-1 view padding, and BOTH algorithms behind NumPy's exact `list_ndim * final_size > 2*512*512` threshold — repeated-concatenate (intermediates disposed eagerly) vs single-allocation `_block_slicing` (per-leaf `NDIter.Copy`, `order='F' if allF and not allC`). C# nesting map: `object[]`/`NDArray[]`/1-D primitive arrays/`IList` = Python list; `NDArray`/scalars/rank≥2 rectangular arrays = leaves; `np.block(x)` depth-0 returns a **copy**. C#-scalar dtype note: leaves keep their C# type (`10` → int32), unlike Python ints (int64). `Creation/np.block.cs`.
- `concatenate` layout vote fixed to NumPy's ambiguity resolution: **F output only when all inputs are F-contiguous AND not all are also C-contiguous** — both-C&F inputs (e.g. `(N,1)` columns) now produce C order, matching `PyArray_CreateMultiSortedStridePerm`. Also added the uniform-tiny-slab interleave fast path (constant-size loads/stores when every per-outer slab is the same 1/2/4/8/16 bytes — the `(N,1)` column pattern; ~10-20× over per-slab `Buffer.MemoryCopy`).
- `UnmanagedStorage.GetData<T>()` now narrows to the shape's `[offset, offset+size)` window before returning/casting (zero-copy, still writes through) — previously a **contiguous view at a non-zero offset** (np.split / np.unstack children) returned the WHOLE backing buffer from `Data<T>()`.

`resize` ships as both `np.resize(a, new_shape)` (function — fills the enlarged output with **repeated copies** of `a` in C-order via an exact-sized doubling byte-tile; empty source / zero new-size → `zeros`; always C-contiguous; any input layout is raveled first) and `ndarray.resize(new_shape, refcheck=true)` (**in-place** — grows with **zeros**, shrinks by truncation, operates on the raw contiguous buffer so an F-contiguous resize relabels memory column-major). The method mirrors NumPy's guards verbatim (`IncorrectShapeException`): single-segment only, and when the byte size changes it must own its data (not a view) and — under `refcheck` — not be shared (`IArraySlice.IsUniquelyReferenced`, backed by the ARC block refcount); `refcheck:false` bypasses. Same-size resize is a pure in-place reshape (no ownership/reference check). See `Manipulation/np.resize.cs`, `Manipulation/NDArray.resize.cs`.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using BenchmarkDotNet.Attributes;
using NumSharp;
using NumSharp.Benchmark.CSharp.Infrastructure;

namespace NumSharp.Benchmark.CSharp.Benchmarks.Manipulation;

/// <summary>
/// Benchmarks for the reversal / rotation / transpose-alias view ops merged into journey2, plus the
/// value-dependent trim_zeros crop: flip, fliplr, flipud, rot90, permute_dims, matrix_transpose,
/// trim_zeros. The first six are O(1)/O(ndim) pure views (stride negation / axis permutation via
/// Storage.Alias — no data movement), so their timing tracks view-construction overhead; trim_zeros
/// is the one doing an O(N) nonzero bounding-box scan (the op with the standout NumPy-vs-NumSharp
/// speedup). NumPy twins live in numpy_benchmark.py::run_manipulation_benchmarks (float64), and the
/// [Benchmark(Description)] labels normalize 1:1 onto those names for the (op, dtype, N) merge join.
/// </summary>
[BenchmarkCategory("Manipulation", "FlipRot")]
public class FlipRotBenchmarks : BenchmarkBase
{
private NDArray _arr2D = null!;

[Params(ArraySizeSource.Medium, ArraySizeSource.Large)]
public override int N { get; set; }

[GlobalSetup]
public void Setup()
{
np.random.seed(Seed);
var rows = (int)Math.Sqrt(N);
var cols = N / rows;
_arr2D = np.random.rand(rows, cols) * 100;
}

[GlobalCleanup]
public void Cleanup()
{
_arr2D = null!;
GC.Collect();
}

// ------------------------------------------------------------------------
// Reversal views (stride negation)
// ------------------------------------------------------------------------

[Benchmark(Description = "np.flip(a)")]
[BenchmarkCategory("Flip")]
public NDArray Flip() => np.flip(_arr2D);

[Benchmark(Description = "np.fliplr(a)")]
[BenchmarkCategory("Flip")]
public NDArray FlipLr() => np.fliplr(_arr2D);

[Benchmark(Description = "np.flipud(a)")]
[BenchmarkCategory("Flip")]
public NDArray FlipUd() => np.flipud(_arr2D);

// ------------------------------------------------------------------------
// Rotation (flip + transpose composition)
// ------------------------------------------------------------------------

[Benchmark(Description = "np.rot90(a)")]
[BenchmarkCategory("Rot90")]
public NDArray Rot90() => np.rot90(_arr2D);

// ------------------------------------------------------------------------
// Transpose aliases (NumPy 2.x / Array-API)
// ------------------------------------------------------------------------

[Benchmark(Description = "np.permute_dims(a)")]
[BenchmarkCategory("Transpose")]
public NDArray PermuteDims() => np.permute_dims(_arr2D);

[Benchmark(Description = "np.matrix_transpose(a)")]
[BenchmarkCategory("Transpose")]
public NDArray MatrixTranspose() => np.matrix_transpose(_arr2D);

// ------------------------------------------------------------------------
// Value-dependent crop (O(N) nonzero bounding-box scan)
// ------------------------------------------------------------------------

[Benchmark(Description = "np.trim_zeros(a)")]
[BenchmarkCategory("TrimZeros")]
public NDArray TrimZeros() => np.trim_zeros(_arr2D, "fb");
}
39 changes: 39 additions & 0 deletions benchmark/NumSharp.Benchmark.Python/numpy_benchmark.py
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,45 @@ def np_stack(): return np.stack([arr_1d, arr_1d_b])
r.name, r.category, r.suite, r.dtype = "np.stack", "Stack", "Manipulation", dtype_name
results.append(r)

# Reversal / rotation / transpose-alias views + the trim_zeros crop (journey2 additions —
# twins of Benchmarks/Manipulation/FlipRotBenchmarks.cs). The first six are O(1)/O(ndim) pure
# views; trim_zeros does an O(N) nonzero bounding-box scan. Names normalize 1:1 onto the C#
# [Benchmark(Description)] labels ("np.flip(a)" -> "np.flip", ...) for the (op, dtype, N) join.
def np_flip(): return np.flip(arr_2d)
r = benchmark(np_flip, n, iterations=iterations)
r.name, r.category, r.suite, r.dtype = "np.flip", "Flip", "Manipulation", dtype_name
results.append(r)

def np_fliplr(): return np.fliplr(arr_2d)
r = benchmark(np_fliplr, n, iterations=iterations)
r.name, r.category, r.suite, r.dtype = "np.fliplr", "Flip", "Manipulation", dtype_name
results.append(r)

def np_flipud(): return np.flipud(arr_2d)
r = benchmark(np_flipud, n, iterations=iterations)
r.name, r.category, r.suite, r.dtype = "np.flipud", "Flip", "Manipulation", dtype_name
results.append(r)

def np_rot90(): return np.rot90(arr_2d)
r = benchmark(np_rot90, n, iterations=iterations)
r.name, r.category, r.suite, r.dtype = "np.rot90", "Rot90", "Manipulation", dtype_name
results.append(r)

def np_permute_dims(): return np.permute_dims(arr_2d)
r = benchmark(np_permute_dims, n, iterations=iterations)
r.name, r.category, r.suite, r.dtype = "np.permute_dims", "Transpose", "Manipulation", dtype_name
results.append(r)

def np_matrix_transpose(): return np.matrix_transpose(arr_2d)
r = benchmark(np_matrix_transpose, n, iterations=iterations)
r.name, r.category, r.suite, r.dtype = "np.matrix_transpose", "Transpose", "Manipulation", dtype_name
results.append(r)

def np_trim_zeros(): return np.trim_zeros(arr_2d, 'fb')
r = benchmark(np_trim_zeros, n, iterations=iterations)
r.name, r.category, r.suite, r.dtype = "np.trim_zeros", "TrimZeros", "Manipulation", dtype_name
results.append(r)

return results

# =============================================================================
Expand Down
3 changes: 3 additions & 0 deletions docs/website-src/api/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ Functions for changing array shape and dimensions.
| `np.swapaxes(a, ax1, ax2)` | Swap two axes |
| `np.moveaxis(a, src, dst)` | Move axes to new positions |
| `np.rollaxis(a, axis)` | Roll axis backwards |
| `np.flip(a, axis)` | Reverse element order along axes (returns view) |
| `np.fliplr(a)` | Flip left/right — reverse axis 1 (returns view) |
| `np.flipud(a)` | Flip up/down — reverse axis 0 (returns view) |
| `np.atleast_1d(a)` | Convert to at least 1-D |
| `np.atleast_2d(a)` | Convert to at least 2-D |
| `np.atleast_3d(a)` | Convert to at least 3-D |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,11 @@ public override NDArray Transpose(NDArray nd, int[] premute = null)
int i, n;
var permutation = new int[nd.ndim];
var reverse_permutation = new int[nd.ndim];
if (premute == null || premute.Length == 0)
// NumPy distinguishes axes=None (reverse all axes) from an explicit length-0
// permutation axes=() / []: the empty permutation only matches a 0-d array,
// and raises "axes don't match array" for any ndim >= 1. So only a null
// `premute` means "reverse"; an empty array falls through to the length check.
if (premute == null)
{
n = nd.ndim;
for (i = 0; i < n; i++)
Expand All @@ -142,7 +146,7 @@ public override NDArray Transpose(NDArray nd, int[] premute = null)
n = premute.Length;
int[] axes = premute;
if (n != nd.ndim)
throw new Exception("axes don't match array");
throw new ArgumentException("axes don't match array");

for (i = 0; i < n; i++)
reverse_permutation[i] = -1;
Expand All @@ -151,7 +155,7 @@ public override NDArray Transpose(NDArray nd, int[] premute = null)
{
int axis = check_and_adjust_axis(nd, axes[i]);
if (reverse_permutation[axis] != -1)
throw new Exception("repeated axis in transpose");
throw new ArgumentException("repeated axis in transpose");

reverse_permutation[axis] = i;
permutation[i] = axis;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,27 @@ public override unsafe NDArray ReduceCumMul(NDArray arr, int? axis_, NPTypeCode?
}

var shape = arr.Shape;

// Empty: cumprod returns a FRESH array of the accumulator dtype (NEP50 widening applies
// even when empty — cumprod(empty int32) is int64, like cumsum). axis=None ravels to a
// 1-D (0,) result; with an axis the shape is preserved. Returning `arr` unchanged (as
// before) kept the source dtype AND, for a multi-dim source, the un-raveled shape —
// e.g. cumprod((0,3)) gave (0,3) where NumPy gives (0,). Mirrors ReduceCumAdd.
if (shape.IsEmpty || shape.size == 0)
return arr;
{
var emptyRetType = typeCode ?? arr.GetTypeCode.GetAccumulatingType();
if (axis_ != null)
{
int nd = Math.Max(arr.ndim, 1);
int ax = axis_.Value < 0 ? axis_.Value + nd : axis_.Value;
if (ax < 0 || ax >= nd)
throw new ArgumentOutOfRangeException(nameof(axis_),
$"axis {axis_.Value} is out of bounds for array of dimension {nd}");
}

return new NDArray(emptyRetType,
axis_ == null ? Shape.Vector((int)shape.size) : new Shape(shape.dimensions), false);
}

if (shape.IsScalar || shape.size == 1 && shape.dimensions.Length == 1)
return typeCode.HasValue ? Cast(arr, typeCode.Value, copy: true) : arr.Clone();
Expand Down
Loading
Loading