Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
2 changes: 2 additions & 0 deletions src/NumSharp.Core/Backends/Unmanaged/ArraySlice`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,8 @@ public void DangerousFree()

public bool IsReleased => MemoryBlock.IsReleased;

public bool IsUniquelyReferenced => MemoryBlock.IsUniquelyReferenced;


/// <summary>
/// Copies the contents of this span into a new array. This heap
Expand Down
10 changes: 10 additions & 0 deletions src/NumSharp.Core/Backends/Unmanaged/Interfaces/IArraySlice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,5 +99,15 @@ public interface IArraySlice : IMemoryBlock, ICloneable, IEnumerable
/// Diagnostic: <c>true</c> once the underlying buffer has been freed.
/// </summary>
bool IsReleased { get; }

/// <summary>
/// <c>true</c> when the underlying <see cref="MemoryBlock"/> is held by
/// at most one logical reference (this owner), i.e. no other
/// <see cref="NDArray"/> / view shares its buffer. Non-owning wraps
/// (external / pinned memory) report <c>true</c> since their refcount is
/// immortal and meaningless. Used by <c>ndarray.resize</c>'s refcheck to
/// mirror NumPy's "references or is referenced by another array" guard.
/// </summary>
bool IsUniquelyReferenced { get; }
}
}
14 changes: 14 additions & 0 deletions src/NumSharp.Core/Backends/Unmanaged/UnmanagedMemoryBlock`1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,12 @@ public void Free()

public bool IsReleased => _disposer.IsReleased;

/// <summary>
/// <c>true</c> when this block is held by at most one logical reference.
/// Forwards to the inner <see cref="Disposer"/>.
/// </summary>
public bool IsUniquelyReferenced => _disposer.IsUniquelyReferenced;

[MethodImpl(OptimizeAndInline)]
public IEnumerator<T> GetEnumerator()
{
Expand Down Expand Up @@ -1300,6 +1306,14 @@ public void Release()
/// <summary>Diagnostic: true once the buffer has been freed.</summary>
public bool IsReleased => Volatile.Read(ref _freed) != 0;

/// <summary>
/// <c>true</c> when at most one logical reference is held (refcount &lt;= 1).
/// Non-owning wraps are immortal (refcount is meaningless) and report
/// <c>true</c>. Used by resize's refcheck to detect buffer sharing.
/// </summary>
public bool IsUniquelyReferenced =>
_type == AllocationType.Wrap || Interlocked.Read(ref _refCount) <= 1;

/// <summary>
/// Backwards-compatible "force free" entry point. Maps to the
/// finalizer's behavior: drops any remaining refs and frees.
Expand Down
163 changes: 163 additions & 0 deletions src/NumSharp.Core/Manipulation/NDArray.resize.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
using System;
using System.Runtime.InteropServices;
using NumSharp.Backends;

namespace NumSharp
{
public partial class NDArray
{
/// <summary>
/// Change shape and size of this array <b>in-place</b>.
/// <para>
/// If the new array is larger than the original array, the new array is filled with
/// <b>zeros</b> (note: this differs from <see cref="np.resize(NDArray, Shape)"/> which
/// fills with repeated copies). If smaller, the data is truncated (in C-order for
/// C-contiguous arrays, memory-order for F-contiguous ones).
/// </para>
/// Multi-argument form: <c>a.resize(2, 3)</c>. A no-argument call <c>a.resize()</c> is a
/// no-op (matches NumPy's <c>a.resize()</c> / <c>a.resize(None)</c>).
/// </summary>
/// <param name="new_shape">Shape of resized array (one value per dimension).</param>
/// <remarks>https://numpy.org/doc/stable/reference/generated/numpy.ndarray.resize.html</remarks>
/// <exception cref="IncorrectShapeException">
/// 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.
/// </exception>
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);
}

/// <summary>
/// Change shape and size of this array <b>in-place</b>.
/// <para>Primary overload — see <see cref="resize(long[])"/> for the fill/truncate semantics.</para>
/// </summary>
/// <param name="new_shape">Shape of resized array. A 0-d shape resizes to a scalar.</param>
/// <param name="refcheck">
/// If <c>true</c> (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 <c>false</c> to skip that check.
/// </param>
/// <remarks>https://numpy.org/doc/stable/reference/generated/numpy.ndarray.resize.html</remarks>
/// <exception cref="IncorrectShapeException">
/// If this array is not single-segment (contiguous); if growing/shrinking an array that
/// does not own its data or (with <paramref name="refcheck"/>) is referenced by another
/// array; or if a dimension is negative.
/// </exception>
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>();
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);
}
}

/// <summary>Row-major (C-order) strides for <paramref name="dims"/>.</summary>
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;
}

/// <summary>Column-major (F-order) strides for <paramref name="dims"/>.</summary>
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;
}
}
}
93 changes: 93 additions & 0 deletions src/NumSharp.Core/Manipulation/np.resize.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
using System;
using NumSharp.Backends;

namespace NumSharp
{
public static partial class np
{
/// <summary>
/// Return a new array with the specified shape.
/// <para>
/// If the new array is larger than the original array, then the new array is filled
/// with repeated copies of <paramref name="a"/> (iterating over <paramref name="a"/>
/// in C-order, cycling back from the start). Note that this behavior is different from
/// <see cref="NDArray.resize(long[])"/> which fills with zeros instead.
/// </para>
/// <para>
/// NumPy's <c>np.resize</c> takes <c>new_shape</c> as a <b>single</b> argument (an int or a
/// sequence) — the multi-argument form <c>np.resize(a, 2, 3)</c> is a NumPy <c>TypeError</c>,
/// so it is not offered here. Pass the shape as an int, a value tuple, an array, or a
/// <see cref="Shape"/>; all resolve through <see cref="Shape"/>'s implicit conversions:
/// <c>np.resize(a, 6)</c>, <c>np.resize(a, (2, 3))</c>, <c>np.resize(a, new[]{2, 3})</c>,
/// <c>np.resize(a, new Shape(2, 3))</c>.
/// </para>
/// </summary>
/// <param name="a">Array to be resized.</param>
/// <param name="new_shape">Shape of resized array (int / tuple / array / <see cref="Shape"/>).</param>
/// <returns>
/// 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 <paramref name="a"/>.
/// </returns>
/// <remarks>https://numpy.org/doc/stable/reference/generated/numpy.resize.html</remarks>
/// <exception cref="ArgumentNullException">If <paramref name="a"/> is null.</exception>
/// <exception cref="ArgumentException">If any element of <paramref name="new_shape"/> is negative.</exception>
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<long>();
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;
}
}
}
Loading
Loading