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
2 changes: 1 addition & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ The primary array type representing N-dimensional arrays.

**Requirements**:
- [x] 6.1.1: `$array->reshape($new_shape)` - Change shape (view if possible)
- [ ] 6.1.2: Support for `-1` in shape (infer dimension)
- [x] 6.1.2: Support for `-1` in shape (infer dimension)
- [x] 6.1.3: `$array->flatten()` - 1D view/copy
- [x] 6.1.4: `$array->ravel()` - 1D view if possible
- [x] 6.1.5: Maintain element count constraint
Expand Down
29 changes: 28 additions & 1 deletion docs/api/array-manipulation.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,27 @@ public function reshape(array $newShape, string $order = 'C'): NDArray

Returns a new array with the specified shape. Supports both C-order (row-major, order='C') and F-order (column-major, order='F').

One dimension in `$newShape` may be `-1`. When present, that dimension is inferred from the array size and the remaining dimensions. The total number of elements must stay the same.

### Parameters

| Name | Type | Description |
|------|------|-------------|
| `$newShape` | `array<int>` | New shape |
| `$newShape` | `array<int>` | New shape. At most one dimension may be `-1` to infer that dimension automatically. |
| `$order` | `string` | Memory layout: 'C' for row-major, 'F' for column-major. Default: `'C'` |

### Returns

- `NDArray` - Reshaped array. This will be a new view object if the array is contiguous; otherwise, it will be a copy. Note there is no guarantee of the memory layout (C- or Fortran- contiguous) of the returned array.

### Throws

- `ShapeException` - If the requested shape does not contain the same number of elements as the original array.
- `ShapeException` - If more than one dimension is `-1`.
- `ShapeException` - If a dimension is less than `-1`.
- `ShapeException` - If the inferred `-1` dimension would not be an integer.
- `ShapeException` - If the inferred dimension is ambiguous because the product of the known dimensions is zero.

### Examples

```php
Expand All @@ -35,12 +45,29 @@ $matrix = $arr->reshape([3, 4]);
print_r($matrix->shape());
// Output: [3, 4]

// Infer one dimension from the array size
$matrix = $arr->reshape([3, -1]);
print_r($matrix->shape());
// Output: [3, 4]

$rows = $arr->reshape([-1, 6]);
print_r($rows->shape());
// Output: [2, 6]

// Reshape to 3D
$tensor = $arr->reshape([2, 2, 3]);
print_r($tensor->shape());
// Output: [2, 2, 3]
```

Only one dimension can be inferred:

```php
$arr->reshape([-1, -1]); // ShapeException
$arr->reshape([5, -1]); // ShapeException: 12 cannot be divided into rows of 5
$arr->reshape([2, -2]); // ShapeException: -2 is not a valid dimension
```

---

## transpose()
Expand Down
3 changes: 2 additions & 1 deletion docs/api/exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ try {

**Common causes:**
- Incompatible shapes for arithmetic operations
- Invalid reshape dimensions
- Invalid reshape dimensions, including reshape sizes that do not preserve the element count
- Invalid reshape inference, such as multiple `-1` dimensions or a `-1` dimension that cannot be inferred evenly
- Broadcasting failures

---
Expand Down
3 changes: 3 additions & 0 deletions docs/guide/fundamentals/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,9 @@ $matrix = $arr->reshape([3, 4]);
// [4, 5, 6, 7],
// [8, 9, 10, 11]]

// Infer one dimension with -1
$arr->reshape([2, -1]); // Shape [2, 6]

// Transpose
$matrix->transpose();
// [[0, 4, 8],
Expand Down
2 changes: 2 additions & 0 deletions docs/guide/getting-started/numpy-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ $mask = $a->gt($b);
| NumPy | NDArray PHP | Notes |
|-------|-------------|-------|
| `a.reshape(3, 4)` | `$a->reshape([3, 4])` | Shape as array |
| `a.reshape(-1, 4)` | `$a->reshape([-1, 4])` | One `-1` dimension is inferred |
| `a.flatten()` | `$a->flatten()` | |
| `a.ravel()` | `$a->ravel()` | |
| `a.squeeze()` | `$a->squeeze()` | |
Expand Down Expand Up @@ -286,6 +287,7 @@ reshaped = arr.reshape(2, 6) # Multiple args
```php
$arr = NDArray::zeros([3, 4]); // Array
$reshaped = $arr->reshape([2, 6]); // Single array argument
$auto = $arr->reshape([-1, 6]); // -1 inference is supported
```

### 6. DType Enum vs Constants
Expand Down
4 changes: 4 additions & 0 deletions docs/guide/getting-started/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@ $reshaped = $arr->reshape([3, 2]);
// [3 4]
// [5 6]]

// Use -1 to infer one dimension from the array size
$auto = $arr->reshape([-1, 2]);
print_r($auto->shape()); // [3, 2]

// Flatten to 1D
$flat = $arr->flatten(); // [1 2 3 4 5 6]

Expand Down
54 changes: 53 additions & 1 deletion src/Traits/HasShapeOps.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,13 @@ public function pad(array|int $padWidth, PadMode $mode = PadMode::Constant, arra
* For contiguous arrays, this returns a zero-copy view with updated metadata.
* For non-contiguous arrays, data is copied to make it contiguous first.
*
* @param array<int> $newShape New shape
* @param array<int> $newShape New shape. One dimension may be -1 to infer it from the array size.
* @param string $order Memory layout: 'C' for row-major, 'F' for column-major
*/
public function reshape(array $newShape, string $order = 'C'): NDArray
{
$oldSize = $this->size();
$newShape = $this->resolveReshapeShape($newShape, $oldSize);
$newSize = (int) array_product($newShape);

if ($oldSize !== $newSize) {
Expand Down Expand Up @@ -550,6 +551,57 @@ public function repeat(array|int|NDArray $repeats, ?int $axis = null): NDArray
return $this->unaryOp('ndarray_repeat', Lib::createShapeArray($repeatsArray), \count($repeatsArray), $axisValue);
}

/**
* Resolve reshape dimensions, including a single inferred -1 dimension.
*
* @param array<int> $newShape Requested shape
* @param int $oldSize Current array size
*
* @return array<int> Shape with any inferred dimension resolved
*/
private function resolveReshapeShape(array $newShape, int $oldSize): array
{
$unknownAxis = null;
$knownSize = 1;

foreach ($newShape as $axis => $dimension) {
if (-1 === $dimension) {
if (null !== $unknownAxis) {
throw new ShapeException('Can only specify one unknown dimension in reshape');
}

$unknownAxis = $axis;

continue;
}

if ($dimension < -1) {
throw new ShapeException("Invalid reshape dimension {$dimension}; dimensions must be non-negative or -1");
}

$knownSize *= $dimension;
}

if (null === $unknownAxis) {
return $newShape;
}

if (0 === $knownSize) {
throw new ShapeException('Cannot infer reshape dimension when the product of known dimensions is zero');
}

if (0 !== $oldSize % $knownSize) {
throw new ShapeException(
"Cannot reshape array of size {$oldSize} into shape ".json_encode($newShape)
.'; inferred dimension would not be an integer'
);
}

$newShape[$unknownAxis] = intdiv($oldSize, $knownSize);

return $newShape;
}

/**
* Normalize pad width to [[before, after], ...] for each axis.
*
Expand Down
53 changes: 53 additions & 0 deletions tests/Unit/ShapeOpsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,59 @@ public function testReshape3D(): void
$this->assertSame([2, 2, 2], $result->shape());
}

public function testReshapeInfersNegativeDimensionForArray(): void
{
$a = NDArray::array([1, 2, 3, 4, 5, 6], DType::Float64);
$result = $a->reshape([2, -1]);

$this->assertSame([2, 3], $result->shape());
$this->assertEqualsWithDelta([[1, 2, 3], [4, 5, 6]], $result->toArray(), 0.0001);
}

public function testReshapeInfersNegativeDimensionForView(): void
{
$a = NDArray::arange(0, 12)->reshape([4, 3]);
$view = $a->slice(['1:3', ':']);
$result = $view->reshape([-1]);

$this->assertSame([6], $result->shape());
$this->assertEqualsWithDelta([3, 4, 5, 6, 7, 8], $result->toArray(), 0.0001);
$this->assertTrue($result->isView());

$result->setAt(0, 999);
$this->assertEqualsWithDelta(999, $a->getAt(3), 0.0001);
}

public function testReshapeRejectsMultipleNegativeDimensions(): void
{
$a = NDArray::array([1, 2, 3, 4], DType::Float64);

$this->expectException(ShapeException::class);
$this->expectExceptionMessage('one unknown dimension');

$a->reshape([-1, -1]);
}

public function testReshapeRejectsNonIntegerInferredDimension(): void
{
$a = NDArray::array([1, 2, 3, 4, 5], DType::Float64);

$this->expectException(ShapeException::class);
$this->expectExceptionMessage('inferred dimension would not be an integer');

$a->reshape([2, -1]);
}

public function testReshapeRejectsInvalidNegativeDimension(): void
{
$a = NDArray::array([1, 2, 3, 4], DType::Float64);

$this->expectException(ShapeException::class);
$this->expectExceptionMessage('dimensions must be non-negative or -1');

$a->reshape([2, -2]);
}

public function testReshapePreservesDtype(): void
{
$a = NDArray::array([1, 2, 3, 4], DType::Int32);
Expand Down
Loading