ndarray-nim: arreglos multidimensionales para Nim
22 de enero de 2026
ndarray-nim es un wrapper de la librería ndarray-c, estando ambas en
sus primeras etapas de desarrollo. La ventaja principal de esta
combinación es una interfaz fácil de usar que envuelve operaciones C
complejas en funciones simples de Nim, soportando arreglos
multidimensionales con un buen rendimiento.
La optimización se logra mediante paralelización con OpenMP e integración BLAS para operaciones de álgebra lineal, mientras que el manejo de memoria se realiza automáticamente a través de los destructores de Nim.
Dado que tanto la librería C como el wrapper en Nim aún son experimentales, requieren más pruebas y actualmente tienen un número limitado de funciones. La librería se enfoca en operaciones básicas de arreglos para creación, manipulación, aritmética y agregación, siendo útil para tareas básicas de análisis de datos.
El ecosistema Nim ofrece librerías de computación numérica con
diferentes fortalezas. arraymancer es una librería comprehensiva de tensores
enfocada en deep learning que soporta CPU, GPU y dispositivos embebidos,
pero viene con más características y dependencias. neo proporciona álgebra lineal
enfocada para vectores y matrices con integración BLAS/LAPACK,
funcionando bien para álgebra lineal tradicional pero limitada a
estructuras de datos 1D y 2D.
ndarray-nim puede ubicarse entre estas al soportar dimensiones
arbitrarias a diferencia del enfoque 1D/2D de Neo, usar un backend C
para mejor rendimiento e interoperabilidad C, proporcionar una API más
simple, y manejar memoria automáticamente a través de los destructores
de Nim.
La librería ahora soporta tipos nativos int de Nim para dimensiones e
indexación. Previamente, se necesitaba usar tipos estilo C como csize_t:
# Anteriormente - requería tipos C
let arr = newOnes(@[2.csize_t, 3.csize_t])
arr.set(@[1.csize_t, 2.csize_t], 42.0)
Ahora se pueden usar tipos regulares de Nim, como int:
# Ahora - tipos nativos Nim
let arr = newOnes(@[2, 3])
arr.set(@[1, 2], 42.0)
El ejemplo incluido al final de esta nota muestra cómo usar la librería para modelado financiero. Simula trayectorias de precios de acciones usando Movimiento Geométrico Browniano, con base en la siguiente ecuación diferencial estocástica:
$$dS_t = \mu S_t dt + \sigma S_t dW_t$$
Donde $S_t$ es el precio de la acción en el tiempo $t$, $\mu$ es el coeficiente de tendencia (retorno esperado), $\sigma$ es la volatilidad, y $W_t$ es un proceso de Wiener (movimiento browniano). Usando el lema de Itô, podemos resolver esta ecuación para obtener la formulación en tiempo discreto:
$$S_t = S_{t-1} \exp\left(\left(\mu - \frac{1}{2}\sigma^2\right)\Delta t + \sigma\sqrt{\Delta t} Z_t\right)$$
Donde $\Delta t$ es el paso de tiempo y $Z_t \sim \mathcal{N}(0,1)$ son variables aleatorias normales estándar independientes. Esta formulación permite simular múltiples trayectorias de precios generando variables normales aleatorias y aplicando la transformación exponencial en cada paso de tiempo.
El ejemplo demuestra algunas capacidades de la librería. La creación de
arreglos se maneja con newZeros() y newNDArray(), mientras que la
generación de números aleatorios usa newRandomNormal().
La simulación depende de operaciones de copiado, segmentación y
aritmética de arreglos para manipular las trayectorias de precios a
través de los pasos de tiempo. Las funciones matemáticas se aplican a
través de mapFn() para los cálculos exponenciales requeridos por la
fórmula GBM.
Acá están las operaciones principales, en las que primero se copia los datos aleatorios para el paso actual, luego se aplican las transformaciones necesarias, y finalmente se actualizan los precios de las acciones:
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)
La elección de Nim para problemas numéricos ofrece ventajas únicas que esta librería aprovecha. Por un lado, el sistema de tipos de Nim y sus destructores proporcionan seguridad de memoria sin sacrificar rendimiento, algo evidente en el manejo automático de los arreglos multidimensionales. Por otro, la capacidad de Nim para interoperar con C de forma transparente permite aprovechar librerías optimizadas como ndarray-c manteniendo una sintaxis limpia y expresiva. Por último, la expresividad y conveniencia de Nim, facilitan la comprensión y mantenimiento del código, algo crucial en proyectos de análisis de datos.
Referencias:
- ndarray-c - repositorio
- ndarray-c - documentación
- ndarray-nim - repositorio
- ndarray-nim - documentación
- arraymancer
- neo
- Browniam Motion Interactive Guide
- Itô Calculus Interactive Guide
Código fuente:
import std/math
import ndarray
type
GBMParams* = object
mu*: float ## drift coefficient
sigma*: float ## volatility coefficient
r*: float ## risk-free interest rate
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 =
# Auxiliar 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)
# Weimer process
let z = newRandomNormal(@[steps, simpar.I], 0.0, 1.0)
# Auxiliar 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)