Skip to content

Getting Started

ModelCore owns a time grid and state history, while CoreSolver advances that state. Solves infer their Array-API namespace from the initial state.

import numpy as np

from op_engine import CoreSolver, ModelCore

times = np.linspace(0.0, 2.0, 21)
core = ModelCore(n_states=1, n_subgroups=1, time_grid=times)
core.set_initial_state(np.asarray([[1.0]]))


def decay(_time, state):
    return -0.25 * state


CoreSolver(core).run(decay)
solution = core.state_array

Array-backend solves

array-api-compat discovers the numerical namespace from NumPy, JAX, CuPy, PyTorch, and other supported native arrays. Passing an array to set_initial_state selects the namespace for state storage, history, solver stages, and adaptive error control. No backend argument is needed. An RHS should derive operations from its state when it needs namespace functions:

from op_engine import array_namespace


def portable_decay(_time, state):
    xp = array_namespace(state)
    return xp.multiply(state, -0.25)

Dense implicit and IMEX operators are converted into the state's namespace and solved with its Array-API linalg.solve implementation. This provides the portable correctness path for NumPy, JAX, PyTorch, and other supported backends. Array portability alone does not promise backend compilation, automatic differentiation, sparse support, or random-number semantics.

Sparse acceleration is an optional second tier. SciPy sparse operators retain cached factorization for NumPy state. Installing op-engine[cupy] registers the matching cupyx.scipy.sparse adapter; CuPy is never imported when it is not installed. Each adapter detects only its own sparse objects. Backends without a registered sparse implementation can use dense operators without engine changes.