Libmir Archive

ndslice vs NumPy — Side-by-Side

A practical comparison of D's mir ndslice and Python's NumPy. Every common NumPy pattern mapped to its ndslice equivalent, with notes on performance and differences.

If you know NumPy and want to use D's Mir for numerical computing, this guide maps the operations you already know. Both libraries share the same core concepts — N-dimensional arrays with strided memory, lazy views, broadcasting — but the syntax and idioms differ.

Setup

NumPy (Python)

import numpy as np

ndslice (D)

import mir.ndslice;
import mir.ndslice.topology;
import mir.algorithm.iteration;

Creating arrays

OperationNumPyndslice
Zerosnp.zeros((3, 4))slice!double(3, 4) (zero-initialized)
From listnp.array([1,2,3,4])[1,2,3,4].sliced
Reshape from listnp.array([...]).reshape(2,3)[...].sliced(2,3)
Integer rangenp.arange(12)iota(12)
2-D rangenp.arange(12).reshape(3,4)iota(3,4)
Linspacenp.linspace(0,1,100)linspace!double(0.0,1.0,100)
Onesnp.ones((3,4))slice!double(3,4) then s[] = 1.0
Identitynp.eye(4)slice!double(4,4) then fill diagonal

Array properties

a = np.zeros((3, 4))
a.shape    # (3, 4)
a.ndim     # 2
a.size     # 12
a.dtype    # float64
auto a = slice!double(3, 4);
a.shape           // [3, 4]
a.shape.length    // 2  (ndim)
a.elementCount    // 12
// Type is part of the Slice template parameter

Indexing

a[1, 2]        # scalar
a[0]           # first row
a[:, 2]        # column 2
a[1:3, 1:4]    # sub-matrix
a[-1]          # last row
a[1, 2]           // scalar
a[0]              // first row (1-D slice)
a[0..$, 2]        // column 2 (1-D slice)
a[1..3, 1..4]     // sub-matrix (view)
a[$ - 1]          // last row

Reshaping and transposing

a.reshape(4, 3)
a.T              # transpose
a.flatten()      # 1-D copy
a.ravel()        # 1-D view if possible
a.reshape(4, 3)    // same element count required
a.transposed       // zero-copy view
a.flattened        // 1-D view (contiguous only)

Key difference: a.T in NumPy is always a view; a.transposed in ndslice is also a view but changes the SliceKind to Universal, which carries strides. Some functions require Contiguous — use .as!double.slice to materialise.

Element-wise math

a * 2
a + b
np.sin(a)
a ** 2
import mir.ndslice.topology : map;
import mir.math.common : sin;

a.map!(x => x * 2)        // lazy
a.map!((x, y) => x + y)   // doesn't work for two slices — use zip+map:
import mir.ndslice.topology : zip;
zip(a, b).map!(t => t[0] + t[1])

a.map!sin                  // element-wise sin
a.map!(x => x * x)        // element-wise square

For in-place operations:

a[] *= 2;          // in-place scale
a[] = a.map!sin;   // in-place sin

Reductions

a.sum()
a.sum(axis=0)   # column sums
a.max()
a.min()
np.mean(a)
import mir.algorithm.iteration : reduce;
import mir.math.stat : mean;

a.flattened.reduce!"a + b"(0.0)         // total sum
// axis-wise: iterate manually or use topology
a.map!(x => x).each!...                 // no built-in axis reduce yet

a.flattened.reduce!fmax(double.nan)     // max
a.flattened.reduce!fmin(double.nan)     // min
a.mean                                  // from mir.math.stat

Broadcasting

NumPy broadcasts automatically based on shape rules. ndslice requires explicit broadcasting via repeat or zip:

# NumPy: add row vector to each row of matrix
m + np.array([1, 2, 3, 4])  # auto-broadcast
// ndslice: explicit repeat
import mir.ndslice.topology : repeat;
auto row = [1.0, 2.0, 3.0, 4.0].sliced;
auto tiled = row.repeat(3);  // 3×4 lazy tiled view
zip(m, tiled).map!(t => t[0] + t[1])

Linear algebra

np.dot(a, b)
np.linalg.inv(a)
np.linalg.solve(A, b)
// via lubeck (kaleidicassociates) or mir-blas
import lubeck : mtimes, inv, solve;

auto C = mtimes(A, B);     // matrix multiply
auto Ainv = inv(A);
auto x = solve(A, b);

lubeck is a higher-level linear algebra layer on top of mir-blas and mir-lapack.

Performance comparison

AspectNumPyndslice
Memory layoutC or Fortran orderContiguous or strided
Lazy evaluationNo (eager)Yes — topology operations are lazy
GC overheadPython GCOptional (use rcslice / @nogc)
SIMDVia MKL/OpenBLASAuto-vectorized by compiler
AllocationOn every operationOnly on explicit slice!T(...)
InteropC via ctypesDirect pointer, zero overhead

The biggest practical difference: ndslice lazy operations compose without intermediate allocations. m.transposed.map!(x => x * 2).flattened.reduce!"a+b"(0.0) allocates nothing — the entire pipeline is fused at compile time.

On this page