ndarray-nim: Multidimensional Arrays for Nim
Jan. 22, 2026
ndarray-nim is a wrapper around the ndarray-c library, with both being in
their early stages of development. The main advantage of this
combination is an easy-to-use interface that wraps complex C
operations in simple Nim functions, supporting multidimensional
arrays with good performance.
Optimization is achieved through parallelization with OpenMP and BLAS integration for linear algebra operations, while memory management is handled automatically through Nim's destructors.
Since both the C library and the Nim wrapper are still experimental, they require more testing and currently have a limited number of functions. The library focuses on basic array operations for creation, manipulation, arithmetic, and aggregation, being useful for basic data analysis tasks.
The Nim ecosystem offers numerical computing libraries with
different strengths. arraymancer is a comprehensive tensor library
focused on deep learning that supports CPU, GPU, and embedded devices,
but comes with more features and dependencies. neo provides focused
linear algebra for vectors and matrices with BLAS/LAPACK integration,
working well for traditional linear algebra but limited to
1D and 2D data structures.
ndarray-nim can be positioned between these by supporting arbitrary
dimensions unlike Neo's 1D/2D approach, using a C backend for better
performance and C interoperability, providing a simpler API, and
handling memory automatically through Nim's destructors.
The library now supports native Nim types for dimensions and
indexing. Previously, you needed to use C-style types like csize_t:
# Before - required C types
let arr = newOnes(@[2.csize_t, 3.csize_t])
arr.set(@[1.csize_t, 2.csize_t], 42.0)
Now you can use regular Nim types, like int:
# Now - native Nim types
let arr = newOnes(@[2, 3])
arr.set(@[1, 2], 42.0)
The example include at the end of this note shows how to use the library for financial modeling. It simulates stock price trajectories using Geometric Brownian Motion, based on the following stochastic differential equation:
$$dS_t = \mu S_t dt + \sigma S_t dW_t$$
Where $S_t$ is the stock price at time $t$, $\mu$ is the drift coefficient (expected return), $\sigma$ is the volatility, and $W_t$ is a Wiener process (Brownian motion). Using Itô's lemma, we can solve this equation to obtain the discrete-time formulation:
$$S_t = S_{t-1} \exp\left(\left(\mu - \frac{1}{2}\sigma^2\right)\Delta t + \sigma\sqrt{\Delta t} Z_t\right)$$
Where $\Delta t$ is the time step and $Z_t \sim \mathcal{N}(0,1)$ are independent standard normal random variables. This formulation allows simulating multiple price paths by generating random normal variables and applying the exponential transformation at each time step.
The example demonstrates some of the library's capabilities. Array
creation is handled with newZeros() and newNDArray(), while random
number generation uses newRandomNormal().
The simulation relies on copy, slicing, and arithmetic array operations
to manipulate price trajectories across time steps. Mathematical
functions are applied through mapFn() for the exponential calculations
required by the GBM formula.
Here are the core operations, where first the random data for the current step is copied, then the necessary transformations are applied, and finally the stock prices are updated:
discard z_t.copySlice(0, 0, z, 0, step)
.mulScalar(gbmpar.sigma * sdt)
.addScalar(drift)
.exp()
discard S_t.copySlice(0, 0, S, 0, (step - 1))
.mul(z_t)
The choice of Nim for numerical problems offers unique advantages that this library leverages. On one hand, Nim's type system and its destructors provide memory safety without sacrificing performance, something evident in the automatic handling of multidimensional arrays. On the other hand, Nim's ability to interoperate with C transparently allows leveraging optimized libraries like ndarray-c while maintaining clean and expressive syntax. Lastly, Nim's expressiveness and convenience facilitate code comprehension and maintenance, something crucial in data analysis projects.
References:
- ndarray-c - repository
- ndarray-c - documentation
- ndarray-nim - repository
- ndarray-nim - documentation
- arraymancer
- neo
- Brownian Motion Interactive Guide
- Itô Calculus Interactive Guide
Source code:
import std/math
import ndarray
type
GBMParams* = object
mu*: float ## drift coefficient
sigma*: float ## volatility coefficient
SimParams* = object
T*: float ## time to maturity
M*: int ## number of steps by period
I*: int ## simulation paths
proc generateGBMPaths(gbmpar: GBMParams, simpar: SimParams,
S0: float): NDArray =
# Auxiliary variables
let steps = simpar.M * simpar.T.int + 1
let dt = 1.0 / simpar.M.float
let sdt = sqrt(dt)
let drift = (gbmpar.mu - 0.5 * gbmpar.sigma * gbmpar.sigma) * dt
# Prices array
var S = newZeros(@[steps, simpar.I])
S.fillSlice(0, 0, S0)
# Wiener process
let z = newRandomNormal(@[steps, simpar.I], 0.0, 1.0)
# Auxiliary arrays
var S_t = newNDArray(@[1, simpar.I])
var z_t = newNDArray(@[1, simpar.I])
for step in 1 ..< steps:
echo "Simulating step ", step, " of ", (steps - 1)
discard z_t.copySlice(0, 0, z, 0, step)
.mulScalar(gbmpar.sigma * sdt)
.addScalar(drift)
.exp()
discard S_t.copySlice(0, 0, S, 0, (step - 1))
.mul(z_t)
discard S.copySlice(0, step, S_t, 0, 0)
return S
when isMainModule:
let gbmParams = GBMParams(
mu: 0.05, # 5% drift
sigma: 0.2, # 20% volatility
r: 0.03 # 3% risk-free rate
)
let simParams = SimParams(
T: 1.0, # 1 year to maturity
M: 252, # 252 trading steps (daily)
I: 100 # 100 simulation paths
)
let S0 = 100.0 # Initial stock price
let paths = generateGBMPaths(gbmParams, simParams, S0)
paths.print("Prices", 4)