From 42d9c5c0bd53a2544f02621733eabbb2e48a8ffe Mon Sep 17 00:00:00 2001 From: Eli Belash Date: Sat, 18 Jul 2026 06:56:47 +0300 Subject: [PATCH] feat(manipulation): add np.resize and ndarray.resize (NumPy 2.x parity) Implements both NumPy resize entry points with their distinct fill semantics, plus the ARC-refcount plumbing that ndarray.resize's refcheck requires. np.resize(a, new_shape) - function (Manipulation/np.resize.cs) * Enlarges by tiling REPEATED COPIES of `a` in C-order; result is always C-contiguous and any input layout is raveled first. * Fills via an exact-sized doubling byte-tile: allocates exactly new_size and grows the filled region by self-copying (dtype-agnostic raw memory, O(new_size) bytes, O(log(new_size/size)) memcpys) - beats NumPy's concatenate((a,)*repeats)[:new_size] which over-allocates then slices. * Empty source or zero new-size -> zeros(new_shape, a.dtype). * Negative dim -> ArgumentException ("all elements of `new_shape` must be non-negative"), matching np.resize's wording. ndarray.resize(new_shape, refcheck=true) - in-place (Manipulation/NDArray.resize.cs) * Grows with ZEROS, shrinks by truncation, operating on the raw contiguous buffer (so an F-contiguous resize relabels memory column-major). * Mirrors NumPy's guards verbatim (IncorrectShapeException): single-segment only; when byte size changes it must own its data (not a view) and - under refcheck - not be shared. refcheck:false bypasses the sharing check. * Same-size resize is a pure in-place reshape (no ownership/reference check). * ARC-correct buffer swap: add-ref the fresh block, release the old (freed when uniquely owned, kept alive for any sharer under refcheck:false). * params overload takes long[] (long-indexing aligned), not int[]. IArraySlice.IsUniquelyReferenced - new refcount probe * Added to IArraySlice, ArraySlice, UnmanagedMemoryBlock (+ inner Disposer). True when the block is held by <= 1 logical reference; non-owning Wrap allocations (external/pinned) report true (immortal refcount). Backs resize's "references or is referenced by another array" guard. Tests: 39 ResizeTests (Manipulation/np.resize.Test.cs) - function tiling/zeros/ scalar/empty/negative/dtype + method grow/shrink/reshape/no-op + view & F-order + refcheck sharing guards + error parity. All green on this base. Docs: .claude/CLAUDE.md Shape Manipulation section documents both entry points. --- .claude/CLAUDE.md | 4 +- .../Backends/Unmanaged/ArraySlice`1.cs | 2 + .../Unmanaged/Interfaces/IArraySlice.cs | 10 + .../Unmanaged/UnmanagedMemoryBlock`1.cs | 14 + .../Manipulation/NDArray.resize.cs | 163 +++++++ src/NumSharp.Core/Manipulation/np.resize.cs | 93 ++++ .../Manipulation/np.resize.Test.cs | 438 ++++++++++++++++++ 7 files changed, 723 insertions(+), 1 deletion(-) create mode 100644 src/NumSharp.Core/Manipulation/NDArray.resize.cs create mode 100644 src/NumSharp.Core/Manipulation/np.resize.cs create mode 100644 test/NumSharp.UnitTest/Manipulation/np.resize.Test.cs diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 5b6155b70..f0a9fa284 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -241,7 +241,9 @@ Tested against NumPy 2.x. `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` ### 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`, `roll`, `rollaxis`, `split`, `squeeze`, `stack`, `swapaxes`, `tile`, `transpose`, `unique`, `vsplit`, `vstack` +`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` + +`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`. ### Broadcasting `are_broadcastable`, `broadcast`, `broadcast_arrays`, `broadcast_to` diff --git a/src/NumSharp.Core/Backends/Unmanaged/ArraySlice`1.cs b/src/NumSharp.Core/Backends/Unmanaged/ArraySlice`1.cs index d28dd3077..46dba5505 100644 --- a/src/NumSharp.Core/Backends/Unmanaged/ArraySlice`1.cs +++ b/src/NumSharp.Core/Backends/Unmanaged/ArraySlice`1.cs @@ -587,6 +587,8 @@ public void DangerousFree() public bool IsReleased => MemoryBlock.IsReleased; + public bool IsUniquelyReferenced => MemoryBlock.IsUniquelyReferenced; + /// /// Copies the contents of this span into a new array. This heap diff --git a/src/NumSharp.Core/Backends/Unmanaged/Interfaces/IArraySlice.cs b/src/NumSharp.Core/Backends/Unmanaged/Interfaces/IArraySlice.cs index 5cdc7f66e..4cc029229 100644 --- a/src/NumSharp.Core/Backends/Unmanaged/Interfaces/IArraySlice.cs +++ b/src/NumSharp.Core/Backends/Unmanaged/Interfaces/IArraySlice.cs @@ -99,5 +99,15 @@ public interface IArraySlice : IMemoryBlock, ICloneable, IEnumerable /// Diagnostic: true once the underlying buffer has been freed. /// bool IsReleased { get; } + + /// + /// true when the underlying is held by + /// at most one logical reference (this owner), i.e. no other + /// / view shares its buffer. Non-owning wraps + /// (external / pinned memory) report true since their refcount is + /// immortal and meaningless. Used by ndarray.resize's refcheck to + /// mirror NumPy's "references or is referenced by another array" guard. + /// + bool IsUniquelyReferenced { get; } } } diff --git a/src/NumSharp.Core/Backends/Unmanaged/UnmanagedMemoryBlock`1.cs b/src/NumSharp.Core/Backends/Unmanaged/UnmanagedMemoryBlock`1.cs index 59da716de..9745a50cd 100644 --- a/src/NumSharp.Core/Backends/Unmanaged/UnmanagedMemoryBlock`1.cs +++ b/src/NumSharp.Core/Backends/Unmanaged/UnmanagedMemoryBlock`1.cs @@ -876,6 +876,12 @@ public void Free() public bool IsReleased => _disposer.IsReleased; + /// + /// true when this block is held by at most one logical reference. + /// Forwards to the inner . + /// + public bool IsUniquelyReferenced => _disposer.IsUniquelyReferenced; + [MethodImpl(OptimizeAndInline)] public IEnumerator GetEnumerator() { @@ -1300,6 +1306,14 @@ public void Release() /// Diagnostic: true once the buffer has been freed. public bool IsReleased => Volatile.Read(ref _freed) != 0; + /// + /// true when at most one logical reference is held (refcount <= 1). + /// Non-owning wraps are immortal (refcount is meaningless) and report + /// true. Used by resize's refcheck to detect buffer sharing. + /// + public bool IsUniquelyReferenced => + _type == AllocationType.Wrap || Interlocked.Read(ref _refCount) <= 1; + /// /// Backwards-compatible "force free" entry point. Maps to the /// finalizer's behavior: drops any remaining refs and frees. diff --git a/src/NumSharp.Core/Manipulation/NDArray.resize.cs b/src/NumSharp.Core/Manipulation/NDArray.resize.cs new file mode 100644 index 000000000..fc5b16365 --- /dev/null +++ b/src/NumSharp.Core/Manipulation/NDArray.resize.cs @@ -0,0 +1,163 @@ +using System; +using System.Runtime.InteropServices; +using NumSharp.Backends; + +namespace NumSharp +{ + public partial class NDArray + { + /// + /// Change shape and size of this array in-place. + /// + /// If the new array is larger than the original array, the new array is filled with + /// zeros (note: this differs from which + /// fills with repeated copies). If smaller, the data is truncated (in C-order for + /// C-contiguous arrays, memory-order for F-contiguous ones). + /// + /// Multi-argument form: a.resize(2, 3). A no-argument call a.resize() is a + /// no-op (matches NumPy's a.resize() / a.resize(None)). + /// + /// Shape of resized array (one value per dimension). + /// https://numpy.org/doc/stable/reference/generated/numpy.ndarray.resize.html + /// + /// If this array is not single-segment (contiguous); if growing/shrinking an array that + /// does not own its data or is referenced by another array; or if a dimension is negative. + /// + public void resize(params long[] new_shape) + { + // NumPy: a.resize() and a.resize(None) return None (no-op). C# collapses both to an + // empty/null params array. Explicit scalar (a.resize(())) is reachable via the Shape + // overload with a 0-d shape. + if (new_shape == null || new_shape.Length == 0) + return; + + resize(new Shape(new_shape), refcheck: true); + } + + /// + /// Change shape and size of this array in-place. + /// Primary overload — see for the fill/truncate semantics. + /// + /// Shape of resized array. A 0-d shape resizes to a scalar. + /// + /// If true (default), reference counting is used to check that this array's buffer + /// is not shared with another array before resizing (when the total size changes). + /// Set to false to skip that check. + /// + /// https://numpy.org/doc/stable/reference/generated/numpy.ndarray.resize.html + /// + /// If this array is not single-segment (contiguous); if growing/shrinking an array that + /// does not own its data or (with ) is referenced by another + /// array; or if a dimension is negative. + /// + public unsafe void resize(Shape new_shape, bool refcheck = true) + { + var shape = this.Shape; + + // 1. Single-segment (contiguous) requirement — always enforced first, exactly like + // NumPy's PyArray_ISONESEGMENT gate. 0-d arrays are trivially single-segment. + if (shape.NDim != 0 && !shape.IsContiguous && !shape.IsFContiguous) + throw new IncorrectShapeException("resize only works on single-segment arrays"); + + // 2. Validate dimensions and compute the new element count. NumPy stops at the first + // zero dim (so a negative dim following a zero is NOT reported) and rejects negatives + // otherwise, with this exact message (distinct from np.resize's wording). + var newDims = new_shape.dimensions ?? System.Array.Empty(); + long newSize = 1; + for (int i = 0; i < newDims.Length; i++) + { + if (newDims[i] == 0) { newSize = 0; break; } + if (newDims[i] < 0) + throw new IncorrectShapeException("negative dimensions not allowed"); + newSize *= newDims[i]; + } + + long itemsize = this.dtypesize; + long oldSize = this.size; + long oldBytes = oldSize * itemsize; + long newBytes = newSize * itemsize; + + // The physical memory layout to preserve: F only when strictly F-contiguous (and not C), + // matching NumPy's _array_fill_strides which keys off the array's current flags. 1-D and + // 0-d arrays are C. + char order = (shape.IsFContiguous && !shape.IsContiguous) ? 'F' : 'C'; + + if (oldBytes != newBytes) + { + // 3. Reallocation is required. Ownership and reference checks fire ONLY here (a + // same-size resize is a pure reshape and skips them, exactly like NumPy). + + // a. Must own its data (not a slice/reshape/transpose view). + if (this.Storage.IsView) + throw new IncorrectShapeException("cannot resize this array: it does not own its data"); + + // b. Must not be shared with another array (unless the caller opts out via refcheck). + if (refcheck && !this.Storage.InternalArray.IsUniquelyReferenced) + throw new IncorrectShapeException( + "cannot resize an array that references or is referenced\n" + + "by another array in this way.\n" + + "Use the np.resize function or refcheck=False"); + + // Allocate the fresh buffer (uninitialized), copy the surviving prefix of the raw + // memory (dtype-agnostic — resize operates on the contiguous byte buffer, which is + // why an F-contiguous grow re-labels the same bytes with the new column-major + // strides), then zero any grown tail. + var freshStrides = order == 'F' ? FortranStrides(newDims) : ContiguousStrides(newDims); + var fresh = new NDArray(this.typecode, new Shape(newDims, freshStrides), fillZeros: false); + + byte* src = this.Storage.Address + shape.offset * itemsize; + byte* dst = fresh.Storage.Address; + long copyBytes = Math.Min(oldBytes, newBytes); + if (copyBytes > 0) + Buffer.MemoryCopy(src, dst, newBytes, copyBytes); + if (newBytes > copyBytes) + NativeMemory.Clear(dst + copyBytes, (nuint)(newBytes - copyBytes)); + + // ARC-correct in-place swap: take a reference on the fresh buffer for `this`, drop + // `this`'s reference to the old buffer (freed here when uniquely owned; kept alive + // for any other sharer under refcheck:false), then discard the fresh wrapper so the + // net effect is `this` solely owning the new buffer. + var newStorage = fresh.Storage; + newStorage.InternalArray.TryAddRef(); + this.Storage.InternalArray?.Release(); + this.Storage = newStorage; + fresh.Dispose(); + } + else + { + // 4. Same total byte size → reshape in place. Preserve the physical order and any + // view offset/buffer, so a same-size resize of an F-contiguous array (or a + // single-segment slice) relabels the existing memory just as NumPy does. + var strides = order == 'F' ? FortranStrides(newDims) : ContiguousStrides(newDims); + var target = new Shape(newDims, strides, shape.offset, shape.bufferSize); + this.Storage.SetShapeUnsafe(ref target); + } + } + + /// Row-major (C-order) strides for . + private static long[] ContiguousStrides(long[] dims) + { + var strides = new long[dims.Length]; + long acc = 1; + for (int i = dims.Length - 1; i >= 0; i--) + { + strides[i] = acc; + acc *= dims[i]; + } + return strides; + } + + /// Column-major (F-order) strides for . + private static long[] FortranStrides(long[] dims) + { + var strides = new long[dims.Length]; + long acc = 1; + for (int i = 0; i < dims.Length; i++) + { + strides[i] = acc; + acc *= dims[i]; + } + return strides; + } + } +} diff --git a/src/NumSharp.Core/Manipulation/np.resize.cs b/src/NumSharp.Core/Manipulation/np.resize.cs new file mode 100644 index 000000000..cdfb36aae --- /dev/null +++ b/src/NumSharp.Core/Manipulation/np.resize.cs @@ -0,0 +1,93 @@ +using System; +using NumSharp.Backends; + +namespace NumSharp +{ + public static partial class np + { + /// + /// Return a new array with the specified shape. + /// + /// If the new array is larger than the original array, then the new array is filled + /// with repeated copies of (iterating over + /// in C-order, cycling back from the start). Note that this behavior is different from + /// which fills with zeros instead. + /// + /// + /// NumPy's np.resize takes new_shape as a single argument (an int or a + /// sequence) — the multi-argument form np.resize(a, 2, 3) is a NumPy TypeError, + /// so it is not offered here. Pass the shape as an int, a value tuple, an array, or a + /// ; all resolve through 's implicit conversions: + /// np.resize(a, 6), np.resize(a, (2, 3)), np.resize(a, new[]{2, 3}), + /// np.resize(a, new Shape(2, 3)). + /// + /// + /// Array to be resized. + /// Shape of resized array (int / tuple / array / ). + /// + /// The new array is formed from the data in the old array, repeated if necessary to + /// fill out the required number of elements. The data are repeated iterating over the + /// array in C-order. Result is C-contiguous; dtype matches . + /// + /// https://numpy.org/doc/stable/reference/generated/numpy.resize.html + /// If is null. + /// If any element of is negative. + public static unsafe NDArray resize(NDArray a, Shape new_shape) + { + if (a is null) throw new ArgumentNullException(nameof(a)); + + // Validate dimensions and compute the target element count. NumPy's np.resize + // rejects negative dims with this exact message (distinct from ndarray.resize's + // "negative dimensions not allowed"). + long newSize = 1; + var newDims = new_shape.dimensions ?? Array.Empty(); + for (int i = 0; i < newDims.Length; i++) + { + if (newDims[i] < 0) + throw new ArgumentException("all elements of `new_shape` must be non-negative"); + newSize *= newDims[i]; + } + + // The result is always C-contiguous (NumPy reshapes the concatenated 1-D read-out), + // so normalize to C-order dims — independent of any strides carried by new_shape. + var outShape = new Shape(newDims); + + // Flatten to a 1-D C-order read-out. ravel returns a view when a is already + // contiguous, else a fresh contiguous copy — either way a dense run of `a.size` + // elements starting at flat.Shape.offset. Disposed after the tiling copy. + using var flat = ravel(a); + long srcSize = flat.size; + + // First case must zero-fill (empty source has nothing to repeat); the second + // would repeat zero times. NumPy: np.zeros_like(a, shape=new_shape). + if (srcSize == 0 || newSize == 0) + return zeros(outShape, a.typecode); + + // Allocate the exact-sized C-contiguous output and tile the source bytes into it. + // This beats NumPy's concatenate((a,)*repeats)[:new_size] which over-allocates a + // repeats*size buffer then slices — we allocate exactly new_size and fill by + // doubling block copies (dtype-agnostic raw memory, O(new_size) bytes, + // O(log(new_size/size)) memcpy calls). + var result = new NDArray(a.typecode, outShape, fillZeros: false); + + long itemsize = a.dtypesize; + byte* src = flat.Storage.Address + (long)flat.Shape.offset * itemsize; + byte* dst = result.Storage.Address; + long totalBytes = newSize * itemsize; + long srcBytes = srcSize * itemsize; + + // Seed with the source (truncated when shrinking), then repeatedly double the + // filled region from dst onto itself until the whole output is covered. + long filled = Math.Min(srcBytes, totalBytes); + Buffer.MemoryCopy(src, dst, totalBytes, filled); + while (filled < totalBytes) + { + long chunk = Math.Min(filled, totalBytes - filled); + Buffer.MemoryCopy(dst, dst + filled, totalBytes - filled, chunk); + filled += chunk; + } + + return result; + } + } +} diff --git a/test/NumSharp.UnitTest/Manipulation/np.resize.Test.cs b/test/NumSharp.UnitTest/Manipulation/np.resize.Test.cs new file mode 100644 index 000000000..297b03091 --- /dev/null +++ b/test/NumSharp.UnitTest/Manipulation/np.resize.Test.cs @@ -0,0 +1,438 @@ +using System; +using System.Numerics; + +namespace NumSharp.UnitTest.Manipulation +{ + /// + /// Battle tests for np.resize (function — repeats copies) and + /// ndarray.resize (in-place method — zero fills). All expected values + /// verified against NumPy 2.4.2. + /// + [TestClass] + public class ResizeTests + { + private static long[] L(NDArray a) + { + var r = new long[a.size]; + for (int i = 0; i < a.size; i++) r[i] = Convert.ToInt64(a.GetAtIndex(i)); + return r; + } + + // ================================================================== + // np.resize (function) — fills with REPEATED copies of a (C-order) + // ================================================================== + + [TestMethod] + public void Resize_Func_DocExample_2x3() + { + // np.resize([[0,1],[2,3]], (2,3)) -> [[0,1,2],[3,0,1]] + var a = np.array(new int[,] { { 0, 1 }, { 2, 3 } }); + var r = np.resize(a, (2, 3)); + r.shape.Should().Equal(2L, 3L); + L(r).Should().Equal(0, 1, 2, 3, 0, 1); + } + + [TestMethod] + public void Resize_Func_DocExample_1x4() + { + var a = np.array(new int[,] { { 0, 1 }, { 2, 3 } }); + var r = np.resize(a, (1, 4)); + r.shape.Should().Equal(1L, 4L); + L(r).Should().Equal(0, 1, 2, 3); + } + + [TestMethod] + public void Resize_Func_DocExample_2x4_Repeats() + { + var a = np.array(new int[,] { { 0, 1 }, { 2, 3 } }); + var r = np.resize(a, (2, 4)); + r.shape.Should().Equal(2L, 4L); + L(r).Should().Equal(0, 1, 2, 3, 0, 1, 2, 3); + } + + [TestMethod] + public void Resize_Func_IntShape_Truncates() + { + var a = np.array(new int[,] { { 0, 1 }, { 2, 3 } }); + var r = np.resize(a, 3); + r.shape.Should().Equal(3L); + L(r).Should().Equal(0, 1, 2); + } + + [TestMethod] + public void Resize_Func_Grow_Tiles() + { + // np.resize([1,2,3], (3,3)) -> rows all [1,2,3] + var r = np.resize(np.array(new[] { 1, 2, 3 }), (3, 3)); + r.shape.Should().Equal(3L, 3L); + L(r).Should().Equal(1, 2, 3, 1, 2, 3, 1, 2, 3); + } + + [TestMethod] + public void Resize_Func_NonMultiple_Tiles() + { + // np.resize(arange(7), (11,)) cycles: 0..6,0..3 + var r = np.resize(np.arange(7), 11); + r.shape.Should().Equal(11L); + L(r).Should().Equal(0, 1, 2, 3, 4, 5, 6, 0, 1, 2, 3); + } + + [TestMethod] + public void Resize_Func_ScalarInput_Repeats() + { + var r = np.resize(np.array(5), (2, 2)); + r.shape.Should().Equal(2L, 2L); + L(r).Should().Equal(5, 5, 5, 5); + } + + [TestMethod] + public void Resize_Func_EmptySource_ZeroFills() + { + // np.resize([], (2,2)) -> zeros + var r = np.resize(np.array(new int[] { }), (2, 2)); + r.shape.Should().Equal(2L, 2L); + L(r).Should().Equal(0, 0, 0, 0); + } + + [TestMethod] + public void Resize_Func_NewSizeZero_ReturnsEmptyShaped() + { + var r = np.resize(np.array(new[] { 1, 2, 3 }), 0); + r.shape.Should().Equal(0L); + r.size.Should().Be(0); + + var r2 = np.resize(np.array(new[] { 1, 2, 3 }), (2, 0, 3)); + r2.shape.Should().Equal(2L, 0L, 3L); + r2.size.Should().Be(0); + } + + [TestMethod] + public void Resize_Func_EmptyShape_ReturnsScalarFirstElement() + { + // np.resize(a, ()) -> array(0) (0-d, first element) + var r = np.resize(np.arange(6), new Shape(new long[0])); + r.ndim.Should().Be(0); + Convert.ToInt64(r.GetAtIndex(0)).Should().Be(0); + } + + [TestMethod] + public void Resize_Func_PreservesDtype() + { + var r = np.resize(np.array(new float[] { 1.5f, 2.5f }), (2, 3)); + r.dtype.Should().Be(typeof(float)); + r.shape.Should().Equal(2L, 3L); + Convert.ToDouble(r.GetAtIndex(0)).Should().Be(1.5); + Convert.ToDouble(r.GetAtIndex(5)).Should().Be(2.5); + } + + [TestMethod] + public void Resize_Func_NegativeDimension_Throws() + { + Action act = () => np.resize(np.array(new[] { 1, 2, 3 }), (-1, 2)); + act.Should().Throw().WithMessage("*non-negative*"); + } + + [TestMethod] + public void Resize_Func_NullArray_Throws() + { + Action act = () => np.resize((NDArray)null, (2, 2)); + act.Should().Throw(); + } + + // ---- np.resize on non-contiguous inputs (ravel materializes C-order) ---- + + [TestMethod] + public void Resize_Func_Transposed_ReadsCOrder() + { + // arange(6).reshape(2,3).T ravels C-order to [0,3,1,4,2,5] + var r = np.resize(np.arange(6).reshape(2, 3).T, (2, 4)); + r.shape.Should().Equal(2L, 4L); + L(r).Should().Equal(0, 3, 1, 4, 2, 5, 0, 3); + } + + [TestMethod] + public void Resize_Func_Strided() + { + var r = np.resize(np.arange(20)["::2"], (2, 3)); + r.shape.Should().Equal(2L, 3L); + L(r).Should().Equal(0, 2, 4, 6, 8, 10); + } + + [TestMethod] + public void Resize_Func_NegativeStride() + { + var r = np.resize(np.arange(6)["::-1"], (2, 4)); + r.shape.Should().Equal(2L, 4L); + L(r).Should().Equal(5, 4, 3, 2, 1, 0, 5, 4); + } + + [TestMethod] + public void Resize_Func_Broadcast() + { + var r = np.resize(np.broadcast_to(np.arange(3), new Shape(2, 3)), (2, 5)); + r.shape.Should().Equal(2L, 5L); + L(r).Should().Equal(0, 1, 2, 0, 1, 2, 0, 1, 2, 0); + } + + [TestMethod] + public void Resize_Func_DoesNotMutateSource() + { + var a = np.arange(4); + var r = np.resize(a, (2, 4)); + a.shape.Should().Equal(4L); + L(a).Should().Equal(0, 1, 2, 3); + } + + // ================================================================== + // ndarray.resize (in-place) — fills with ZEROS when growing + // ================================================================== + + [TestMethod] + public void Resize_Method_Grow_ZeroFills() + { + var a = np.array(new[] { 1, 2, 3 }); + a.resize(2, 3); + a.shape.Should().Equal(2L, 3L); + L(a).Should().Equal(1, 2, 3, 0, 0, 0); + } + + [TestMethod] + public void Resize_Method_Shrink_Truncates() + { + var a = np.arange(1, 7); + a.resize(2, 2); + a.shape.Should().Equal(2L, 2L); + L(a).Should().Equal(1, 2, 3, 4); + } + + [TestMethod] + public void Resize_Method_SameSize_Reshapes() + { + var a = np.arange(6); + a.resize(2, 3); + a.shape.Should().Equal(2L, 3L); + L(a).Should().Equal(0, 1, 2, 3, 4, 5); + } + + [TestMethod] + public void Resize_Method_Grow_3to4() + { + var a = np.array(new[] { 1, 2, 3 }); + a.resize(2, 2); + a.shape.Should().Equal(2L, 2L); + L(a).Should().Equal(1, 2, 3, 0); + } + + [TestMethod] + public void Resize_Method_ScalarInput_ZeroFills() + { + var a = np.array(5); + a.resize(2, 2); + a.shape.Should().Equal(2L, 2L); + L(a).Should().Equal(5, 0, 0, 0); + } + + [TestMethod] + public void Resize_Method_To3D() + { + var a = np.array(new[] { 1, 2, 3 }); + a.resize(2, 2, 2); + a.shape.Should().Equal(2L, 2L, 2L); + L(a).Should().Equal(1, 2, 3, 0, 0, 0, 0, 0); + } + + [TestMethod] + public void Resize_Method_ToZero() + { + var a = np.array(new[] { 1, 2, 3 }); + a.resize(0); + a.shape.Should().Equal(0L); + a.size.Should().Be(0); + } + + [TestMethod] + public void Resize_Method_NoArgs_IsNoOp() + { + var a = np.array(new[] { 1, 2, 3 }); + a.resize(); + a.shape.Should().Equal(3L); + L(a).Should().Equal(1, 2, 3); + } + + [TestMethod] + public void Resize_Method_GrowLarge_TailIsZero() + { + var a = np.arange(10); + a.resize(100); + a.size.Should().Be(100); + for (int i = 0; i < 10; i++) Convert.ToInt64(a.GetAtIndex(i)).Should().Be(i); + for (int i = 10; i < 100; i++) Convert.ToInt64(a.GetAtIndex(i)).Should().Be(0); + } + + [TestMethod] + public void Resize_Method_ReturnsNothing_MutatesInPlace() + { + var a = np.arange(3); + var before = a; // same reference + a.resize(5); + ReferenceEquals(a, before).Should().BeTrue(); + a.size.Should().Be(5); + } + + // ---- F-contiguous: resize operates on the raw memory buffer ---- + + [TestMethod] + public void Resize_Method_FContiguous_Grow_RelabelsMemory() + { + // asfortranarray([[0,1,2],[3,4,5]]) memory = [0,3,1,4,2,5]; grow to (3,3) + // -> [0,3,1,4,2,5,0,0,0] read F-order -> [[0,4,0],[3,2,0],[1,5,0]] + var a = np.asfortranarray(np.arange(6).reshape(2, 3)); + a.resize(3, 3); + a.Shape.IsFContiguous.Should().BeTrue(); + // logical [[0,4,0],[3,2,0],[1,5,0]] read in C-order + L(a).Should().Equal(0, 4, 0, 3, 2, 0, 1, 5, 0); + } + + [TestMethod] + public void Resize_Method_FContiguous_SameSize_RelabelsMemory() + { + // memory [0,3,1,4,2,5] relabeled to (3,2) F-order -> [[0,4],[3,2],[1,5]] + var a = np.asfortranarray(np.arange(6).reshape(2, 3)); + a.resize(3, 2); + a.Shape.IsFContiguous.Should().BeTrue(); + L(a).Should().Equal(0, 4, 3, 2, 1, 5); + } + + // ================================================================== + // ndarray.resize guards (all match NumPy 2.4.2 ValueError messages) + // ================================================================== + + [TestMethod] + public void Resize_Method_NonContiguous_Throws() + { + var nc = np.arange(12).reshape(3, 4)[":, ::2"]; // non-contiguous view + Action act = () => nc.resize(3, 2); + act.Should().Throw().WithMessage("*single-segment*"); + } + + [TestMethod] + public void Resize_Method_View_DoesNotOwnData_Throws() + { + var v = np.arange(6).reshape(2, 3); // reshape view + Action act = () => v.resize(3, 3); // size change on non-owning view + act.Should().Throw().WithMessage("*does not own its data*"); + } + + [TestMethod] + public void Resize_Method_Referenced_Throws() + { + var b = np.arange(6); + var view = b["::"]; // b now referenced by view + Action act = () => b.resize(3, 3); + act.Should().Throw().WithMessage("*references or is referenced*"); + GC.KeepAlive(view); + } + + [TestMethod] + public void Resize_Method_RefcheckFalse_Bypasses() + { + var b = np.arange(6); + var view = b["::"]; + b.resize(new Shape(new long[] { 3, 3 }), refcheck: false); + b.size.Should().Be(9); + GC.KeepAlive(view); + } + + [TestMethod] + public void Resize_Method_NegativeDimension_Throws() + { + var b = np.arange(6); + Action act = () => b.resize(-1, 2); + act.Should().Throw().WithMessage("*negative dimensions not allowed*"); + } + + [TestMethod] + public void Resize_Method_ZeroThenNegative_NoError() + { + // NumPy stops at the first zero dim, so (0,-1) is accepted. + var b = np.arange(6); + b.resize(new Shape(new long[] { 0, -1 })); + b.shape[0].Should().Be(0); + b.shape[1].Should().Be(-1); + } + + [TestMethod] + public void Resize_Method_SameSizeView_AllowedNoOwnershipCheck() + { + // Same total size skips the ownership check — a view reshapes in place. + var v = np.arange(6).reshape(2, 3); + v.resize(3, 2); + v.shape.Should().Equal(3L, 2L); + L(v).Should().Equal(0, 1, 2, 3, 4, 5); + } + + // ================================================================== + // Dtype coverage — all 15 NumSharp types + // ================================================================== + + [TestMethod] + public void Resize_Func_AllDtypes_TileAndDtypePreserved() + { + AssertFuncDtype(np.array(new bool[] { true, false, true }), typeof(bool)); + AssertFuncDtype(np.array(new byte[] { 1, 2, 3 }), typeof(byte)); + AssertFuncDtype(np.array(new sbyte[] { 1, 2, 3 }), typeof(sbyte)); + AssertFuncDtype(np.array(new short[] { 1, 2, 3 }), typeof(short)); + AssertFuncDtype(np.array(new ushort[] { 1, 2, 3 }), typeof(ushort)); + AssertFuncDtype(np.array(new int[] { 1, 2, 3 }), typeof(int)); + AssertFuncDtype(np.array(new uint[] { 1, 2, 3 }), typeof(uint)); + AssertFuncDtype(np.array(new long[] { 1, 2, 3 }), typeof(long)); + AssertFuncDtype(np.array(new ulong[] { 1, 2, 3 }), typeof(ulong)); + AssertFuncDtype(np.array(new char[] { 'a', 'b', 'c' }), typeof(char)); + AssertFuncDtype(np.array(new Half[] { (Half)1, (Half)2, (Half)3 }), typeof(Half)); + AssertFuncDtype(np.array(new float[] { 1, 2, 3 }), typeof(float)); + AssertFuncDtype(np.array(new double[] { 1, 2, 3 }), typeof(double)); + AssertFuncDtype(np.array(new decimal[] { 1, 2, 3 }), typeof(decimal)); + AssertFuncDtype(np.array(new Complex[] { 1, 2, 3 }), typeof(Complex)); + } + + [TestMethod] + public void Resize_Method_AllDtypes_GrowZeroFillAndDtypePreserved() + { + AssertMethodDtype(() => np.array(new bool[] { true, false, true }), typeof(bool)); + AssertMethodDtype(() => np.array(new byte[] { 1, 2, 3 }), typeof(byte)); + AssertMethodDtype(() => np.array(new sbyte[] { 1, 2, 3 }), typeof(sbyte)); + AssertMethodDtype(() => np.array(new short[] { 1, 2, 3 }), typeof(short)); + AssertMethodDtype(() => np.array(new ushort[] { 1, 2, 3 }), typeof(ushort)); + AssertMethodDtype(() => np.array(new int[] { 1, 2, 3 }), typeof(int)); + AssertMethodDtype(() => np.array(new uint[] { 1, 2, 3 }), typeof(uint)); + AssertMethodDtype(() => np.array(new long[] { 1, 2, 3 }), typeof(long)); + AssertMethodDtype(() => np.array(new ulong[] { 1, 2, 3 }), typeof(ulong)); + AssertMethodDtype(() => np.array(new char[] { 'a', 'b', 'c' }), typeof(char)); + AssertMethodDtype(() => np.array(new Half[] { (Half)1, (Half)2, (Half)3 }), typeof(Half)); + AssertMethodDtype(() => np.array(new float[] { 1, 2, 3 }), typeof(float)); + AssertMethodDtype(() => np.array(new double[] { 1, 2, 3 }), typeof(double)); + AssertMethodDtype(() => np.array(new decimal[] { 1, 2, 3 }), typeof(decimal)); + AssertMethodDtype(() => np.array(new Complex[] { 1, 2, 3 }), typeof(Complex)); + } + + private static void AssertFuncDtype(NDArray src, Type dtype) + { + var r = np.resize(src, (2, 4)); // tile 3 -> 8 + r.dtype.Should().Be(dtype); + r.shape.Should().Equal(2L, 4L); + // element i equals src[i % 3] + for (int i = 0; i < 8; i++) + r.GetAtIndex(i).Should().Be(src.GetAtIndex(i % 3)); + } + + private static void AssertMethodDtype(Func make, Type dtype) + { + var a = make(); + var first = a.GetAtIndex(0); + a.resize(2, 4); // grow 3 -> 8, zero-filled tail + a.dtype.Should().Be(dtype); + a.shape.Should().Equal(2L, 4L); + a.GetAtIndex(0).Should().Be(first); + } + } +}