Skip to content

Python bindings: in-memory NPV cubes are wrongly abstract, no NPVCube is constructible from Python #354

Description

@dazivo

Summary

In the Python bindings, the two in-memory NPV cube classes cannot be instantiated:

>>> import ORE
>>> ORE.DoublePrecisionInMemoryCubeN(asof, {"trade1"}, dates, samples)
AttributeError: No constructor defined - class is abstract

SinglePrecisionInMemoryCubeN fails identically. Since NPVCube and AggregationScenarioData are abstract by design and JointNPVCube only joins existing cubes, no NPV cube can be constructed from Python at all — a cube can only be obtained from a completed OREApp run.

Verified on the released 1.8.15.0 and 1.8.16.0 wheels (cp312, macOS arm64) and reproduced from current master with SWIG 4.4.1. This is distinct from #351 / #353 (the two issues stack: #353 makes an existing cube reusable, this one makes cubes constructible).

Root cause

ORE-SWIG/OREAnalytics-SWIG/SWIG/orea_cube.i declares the base's pure virtuals as

virtual Real getT0(Size id, Size depth = 0) const = 0;                      // :52
virtual Real get(Size id, Size date, Size sample, Size depth = 0) const = 0; // :56

while the InMemoryCubeOpt template overrides them as

QuantLib::Real getT0(QuantLib::Size i, QuantLib::Size d) const override;                 // :124
QuantLib::Real get(QuantLib::Size i, QuantLib::Size j, QuantLib::Size k, QuantLib::Size d) const override; // :126

Two independent mismatches make SWIG fail to recognise these as implementations, so it considers the pure virtuals unimplemented, marks the instantiations abstract, and drops the constructors (%template at :133 and the %shared_ptr declarations at :32 are fine):

  1. Default arguments. SWIG expands a pure virtual with a default argument into one required signature per arity; an override without the default satisfies only the full-arity form. This alone keeps the class abstract even with consistent type spellings.
  2. Type spelling. QuantLib::Size/QuantLib::Real are unresolvable to the SWIG parser — the using QuantLib::Size; that would resolve them (QuantLib-SWIG types.i:38) sits inside a %{ %} block the parser never sees, so QuantLib::Size and the base's Size (typedef size_t Size, types.i:63) are unrelated types. This is the same visibility mechanism as Python bindings: getCube/setCube declare different shared_ptr spellings, so in-memory cube reuse raises TypeError (regression in 1.8.16.0) #351.

Bisection in a minimal SWIG 4.4.1 module (base pure virtual vs. derived override):

base override result
getT0(Size, Size depth = 0) = 0 getT0(Size, Size) abstract
getT0(Size, Size) = 0 getT0(QuantLib::Size, QuantLib::Size) abstract
getT0(Size, Size) = 0 getT0(Size, Size) constructible
getT0(Size, Size depth = 0) = 0 getT0(Size, Size d = 0) constructible

Fix

In the InMemoryCubeOpt block of orea_cube.i (:110-128), spell the overrides exactly like the base: bare Size/Real, and repeat the base's = 0 default on getT0/get. Regenerating oreanalytics.i with that change, both classes gain their constructors —

__init__(DoublePrecisionInMemoryCubeN self, Date asof, StringSet ids, DateVector dates, Size samples, double t=double())

— with an unchanged warning profile, and independently of whether the #353 alias fix is applied.

Environment

  • open-source-risk-engine 1.8.15.0 and 1.8.16.0 (PyPI wheels, cp312, macOS arm64), Python 3.12
  • Regenerated from master (3b62ba2) with SWIG 4.4.1, QuantLib-SWIG at the pinned submodule commit b4fc7fa4

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions