Skip to content

Type Promotion

When performing operations on arrays with different data types, NDArray must decide what type the result should be. This process is called type promotion, and it follows NumPy-compatible rules that balance precision, memory efficiency, and mathematical correctness.

The Two Axes of Promotion

Type promotion operates along two independent axes:

  1. Kind: The fundamental category of the data type
  2. Precision: The width within a given kind

Kind Hierarchy

The kind hierarchy determines the broad category of the result:

Integer → Float → Complex

When types of different kinds are combined, the result is the "higher" kind:

CombinationResult KindReason
Integer + FloatFloatFloat can represent all integers
Integer + ComplexComplexComplex can represent all real numbers
Float + ComplexComplexComplex includes the real line

Precision Rules

Within the same kind, the wider (higher precision) type wins:

CombinationResultReason
Int32 + Int64Int64Higher precision
Float32 + Float64Float64Higher precision
Complex64 + Complex128Complex128Higher precision

Putting It Together

The full promotion ladder:

Bool → UInt8 → UInt16 → UInt32 → UInt64

Int8 → Int16 → Int32 → Int64

Float32 → Float64

Complex64 → Complex128

Binary Type Promotion (Array × Array)

When two arrays are combined in an operation, both kind and precision are considered:

php
use PhpMlKit\NDArray\DType;
use PhpMlKit\NDArray\NDArray;

// Different precision, same kind
$a = NDArray::array([1, 2], DType::Int32);
$b = NDArray::array([1, 2], DType::Int64);
$result = $a->add($b);
echo $result->dtype();  // Int64

// Different kinds
$int = NDArray::array([1, 2], DType::Int64);
$float = NDArray::array([1.5, 2.5], DType::Float32);
$result = $int->add($float);
echo $result->dtype();  // Float32 (float kind wins)

// Integer + Complex
$int = NDArray::array([1, 2], DType::Int64);
$complex = NDArray::array([
    new Complex(1, 2),
    new Complex(3, 4),
], DType::Complex128);
$result = $int->add($complex);
echo $result->dtype();  // Complex128 (complex kind wins)

Scalar Type Promotion (Array × Scalar)

When an array is combined with a scalar, the scalar's type is first determined, then promotion proceeds as with two arrays:

php
// Int array + float scalar = Float64
$int = NDArray::array([1, 2, 3], DType::Int64);
$result = $int->add(1.5);
echo $result->dtype();  // Float64

// Float array + int scalar = Float64
$float = NDArray::array([1.0, 2.0, 3.0], DType::Float64);
$result = $float->add(10);
echo $result->dtype();  // Float64 (float kind wins)

// Int array + complex scalar = Complex128
$int = NDArray::array([1, 2, 3], DType::Int64);
$result = $int->add(new Complex(1, 2));
echo $result->dtype();  // Complex128

Scalar Type Inference

Scalars are assigned types based on their PHP type:

PHP TypeInferred DType
intInt64
floatFloat64
ComplexComplex128

This means $intArray + 1.5 promotes the scalar to Float64 first, then promotes the result to Float64.

Unsigned + Signed Promotion

A special rule handles the combination of unsigned and signed integers. Since unsigned types cannot represent negative values, the result must be a signed type wide enough to hold the full range:

php
$u8 = NDArray::array([1, 2], DType::UInt8);   // 0 to 255
$i8 = NDArray::array([1, 2], DType::Int8);    // -128 to 127

// UInt8 + Int8 = Int16 (neither Int8 nor UInt8 can hold all results)
$result = $u8->add($i8);
echo $result->dtype();  // Int16

The rule is: unsigned + signed → signed type with at least one extra bit of precision.

Complex Number Promotion

Complex types follow the same principles, with an additional sub-kind hierarchy:

Real (Int/Float) → Complex

When a real array is combined with a complex array or scalar, the result is complex:

php
$real = NDArray::array([1.0, 2.0], DType::Float64);
$complex = NDArray::array([
    new Complex(1, 2),
    new Complex(3, 4),
], DType::Complex64);

// Float64 + Complex64 = Complex64
$result = $real->add($complex);
echo $result->dtype();  // Complex64

// Float64 + Complex128 = Complex128
$complex128 = NDArray::array([
    new Complex(1, 2),
], DType::Complex128);
$result = $real->add($complex128);
echo $result->dtype();  // Complex128

Note

The real part of a complex result retains the original real value, and the imaginary part is zero for elements that were originally real.

Why This Matters

Understanding type promotion helps you:

  1. Predict result types: Know what dtype your operation will produce
  2. Avoid precision loss: Be aware when Float32 results get promoted to Float64
  3. Manage memory: Complex arrays use twice the memory of float arrays
  4. Debug unexpected results: Integer division truncates, float division does not

Common Pitfalls

Integer Division

php
$a = NDArray::array([5, 7], DType::Int64);
$b = NDArray::array([2, 3], DType::Int64);

// Integer division truncates
$result = $a->divide($b);
print_r($result->toArray());  // [2, 2] — not [2.5, 2.33]

// Solution: cast to float first
$result = $a->astype(DType::Float64)->divide($b);
print_r($result->toArray());  // [2.5, 2.33]

Unexpected Complex Results

php
$int = NDArray::array([1, 2, 3], DType::Int64);

// Adding a complex scalar promotes everything to Complex128
$result = $int->add(new Complex(0, 1));
echo $result->dtype();  // Complex128

// If you only need the real part, extract it
$realOnly = $result->real();
echo $realOnly->dtype();  // Float64

Float32 vs Float64

php
$f32 = NDArray::array([1.0, 2.0], DType::Float32);
$f64 = NDArray::array([1.0, 2.0], DType::Float64);

// Result is Float64 (higher precision)
$result = $f32->add($f64);
echo $result->dtype();  // Float64

Best Practices

1. Be Explicit When It Matters

php
// If you need Float32 results, ensure both inputs are Float32
$a = NDArray::array([1, 2, 3], DType::Float32);
$b = NDArray::array([4, 5, 6], DType::Float32);
$result = $a->add($b);  // Float32

2. Check Types in Debug Mode

php
function debugOperation(NDArray $a, NDArray $b, string $op): void {
    $result = match($op) {
        'add' => $a->add($b),
        'mul' => $a->multiply($b),
        default => throw new \InvalidArgumentException("Unknown op: $op"),
    };

    echo "Input A: {$a->dtype()->name}, Input B: {$b->dtype()->name}, Result: {$result->dtype()->name}\n";
}

3. Use astype() for Control

php
$data = NDArray::array([1, 2, 3]);  // Int64

// Force float division
$result = $data->astype(DType::Float64)->divide(2);
print_r($result->toArray());  // [0.5, 1.0, 1.5]

Next Steps

Released under the MIT License.