Interfaces#

Representation and model#

Decision. The representation (ssm_rep_t) is plain data with public components, separate from the model with parameters (ssm_model_t), as statsmodels separates Representation and KalmanFilter from MLEModel.

Why. The algorithms need only the matrices; the separation lets other languages fill the matrices directly, and lets one model produce many representations (at different parameters) without copying its logic.

In Python. Models own their representation. A MappedModel or MLEModel is set up on the model itself, with mod[name] = value and the initialize_* methods, which pass through to it; a user building a model need not handle a Representation. It stays public as the system at given parameter values, on which the algorithms of DK Part I run (model.representation(params)). A MappedModel also accepts one in place of the data, to start from existing matrices.

The C interface#

Decision. The library exports bind(C) routines with opaque handles, copies inputs in, copies outputs into caller-allocated buffers, returns a status code from every routine, and uses 1-based indices.

Why. Opaque handles keep Fortran’s derived types out of the interface. Copying in and out makes ownership plain: the caller owns its buffers, the library owns its objects, and nothing is shared across the boundary. Status codes, not stop, leave error handling to the caller.

Alternatives. Exposing pointers into the library’s arrays would save copies, but ties the caller to the library’s memory layout and lifetime. The copies cost little next to a filter run.

Where. statespace_capi, statespace_capi_models, statespace_capi_extras; C interface.

Python binding and build#

Decision. The Python package calls the C interface through ctypes. CMake builds the shared library and scikit-build-core packages it into a wheel; fpm remains the build for Fortran users and for the tests.

Why. ctypes is part of the standard library, needs no compiled extension, and releases the GIL during calls, which fit_many relies on. The wheel is then independent of the Python version. fpm does not build shared libraries; CMake does. Both compile the copy of L-BFGS-B bundled in third_party/lbfgsb, so a build needs no network access.

Alternatives. cffi adds a dependency whose wheels can lag new Python versions; Cython or f2py add a compiled extension.

Scope. The package returns numpy arrays. pandas indexes and plotting were added and then removed as outside the library’s scope.

Three kinds of user model#

Decision. Python users define models in three ways: built-in components (StructuralModel), a declared map from parameters to matrix entries (MappedModel), or Python code that sets the matrices (MLEModel).

Why. Speed depends on whether Python runs inside the optimization. The first two run entirely in Fortran and can be fitted in parallel. The third is the most general and familiar to statsmodels users, but calls Python at every likelihood evaluation, and 2k more times per gradient because the score differentiates update numerically.

Callback failures. An exception in update must not leave the model broken. The library then sets H to NaN, so the evaluation fails and the optimizer backs off; it saves H first and restores it at the next call. Python stores the exception and raises it when the library returns. Callback models cannot run on fit_many’s threads; both the Python layer and the library reject them.

Where. statespace_mapped, statespace_callback; python/src/ssfortran/models.py.