Jaime López

Data Science Systems Developer

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:

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)

English