From 39108fd89af5f2ff9ea320bb67b41fe3af64163f Mon Sep 17 00:00:00 2001 From: Danijel Zivoi Date: Tue, 18 Aug 2026 17:35:28 +0200 Subject: [PATCH] Make the in-memory NPV cubes constructible from Python (#354) SinglePrecisionInMemoryCubeN and DoublePrecisionInMemoryCubeN raised "No constructor defined - class is abstract" on construction, on the released 1.8.15.0 and 1.8.16.0 wheels alike. Since NPVCube and AggregationScenarioData are abstract by design and JointNPVCube only joins existing cubes, no NPV cube could be built from Python at all. The InMemoryCubeOpt overrides in orea_cube.i did not textually match the base's pure virtuals as the SWIG parser sees them, for two independent reasons: - getT0/get dropped the base's `= 0` default argument. SWIG expands a pure virtual with a default into one required signature per arity, so an override without the default satisfies only the full-arity form. - the parameters were spelled QuantLib::Size/QuantLib::Real, which the SWIG parser cannot resolve: the `using QuantLib::Size` that would bridge them to the declared `typedef size_t Size` sits inside a %{ %} block the parser never sees (the same visibility mechanism as #351). Either mismatch alone keeps the class abstract; SWIG then treats the pure virtuals as unimplemented and drops the constructors. Spell the overrides exactly like the base -- bare Size/Real, with the base's default repeated on getT0/get -- and declare %feature("compactdefaultargs") for getT0/get on NPVCube and InMemoryCubeOpt. The feature is load-bearing: without it SWIG emits a reduced-arity call like cube->getT0(i) for the default-argument overload, which does not compile, because the C++ overrides do not repeat the base's default and default arguments are resolved against the static type. It has to be declared for base and overrides alike, and before both classes, or the abstract check stops matching them. Verified by building the bindings and running them in a debian trixie container (clang 19, swig 4.3, apt boost): both classes construct, values round-trip through set/get at either arity, and overload dispatch on a base-typed NPVCube proxy (index vs trade id, with and without the depth argument) resolves correctly. The accompanying test constructs both precisions and round-trips values through set/get; it fails with the abstract-class AttributeError against both released wheels. --- ORE-SWIG/OREAnalytics-SWIG/SWIG/orea_cube.i | 46 +++++++++++++---- ORE-SWIG/test/test_inmemory_cube.py | 56 +++++++++++++++++++++ 2 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 ORE-SWIG/test/test_inmemory_cube.py diff --git a/ORE-SWIG/OREAnalytics-SWIG/SWIG/orea_cube.i b/ORE-SWIG/OREAnalytics-SWIG/SWIG/orea_cube.i index 6d55615697..e17800a382 100644 --- a/ORE-SWIG/OREAnalytics-SWIG/SWIG/orea_cube.i +++ b/ORE-SWIG/OREAnalytics-SWIG/SWIG/orea_cube.i @@ -33,6 +33,15 @@ %shared_ptr(ore::analytics::InMemoryCubeOpt); %shared_ptr(ore::analytics::JointNPVCube) +// getT0/get carry a default argument on their pure-virtual declarations. +// compactdefaultargs (on base and overrides alike, so SWIG's abstract check +// keeps matching them up) generates wrappers that fill the default and call +// C++ with the full argument list; see the comment at InMemoryCubeOpt. +%feature("compactdefaultargs") ore::analytics::NPVCube::getT0; +%feature("compactdefaultargs") ore::analytics::NPVCube::get; +%feature("compactdefaultargs") ore::analytics::InMemoryCubeOpt::getT0; +%feature("compactdefaultargs") ore::analytics::InMemoryCubeOpt::get; + namespace ore { namespace analytics { class NPVCube { @@ -107,24 +116,39 @@ public: } }; +// The overrides below must repeat the exact spelling of the NPVCube +// declarations above, or SWIG treats the base's pure virtuals as +// unimplemented and marks the instantiations abstract, dropping their +// constructors. Two aspects matter: the parameter types have to be the +// bare `Size`/`Real` (the `using QuantLib::Size` that would resolve the +// qualified spelling sits inside a %{ %} block the SWIG parser never +// sees), and getT0/get have to carry the base's default argument, because +// SWIG expands a pure virtual with a default into one required signature +// per arity. +// The compactdefaultargs feature declared above the namespace completes +// the picture: it makes the generated wrappers fill the default themselves +// and always call C++ with the full argument list. Without it SWIG emits a +// reduced-arity call like cube->getT0(i), which does not compile: the C++ +// overrides do not repeat the base's default, and default arguments are +// resolved against the static type. template class InMemoryCubeOpt : public ore::analytics::NPVCube { public: InMemoryCubeOpt(const QuantLib::Date& asof, const std::set& ids, - const std::vector& dates, QuantLib::Size samples, const T& t = T()); + const std::vector& dates, Size samples, const T& t = T()); InMemoryCubeOpt(const QuantLib::Date& asof, const std::set& ids, - const std::vector& dates, QuantLib::Size samples, QuantLib::Size depth, + const std::vector& dates, Size samples, Size depth, const T& t = T()); - QuantLib::Size numIds() const override; - QuantLib::Size numDates() const override; - QuantLib::Size samples() const override; - QuantLib::Size depth() const override; - const std::map& idsAndIndexes() const override; + Size numIds() const override; + Size numDates() const override; + Size samples() const override; + Size depth() const override; + const std::map& idsAndIndexes() const override; const std::vector& dates() const override; QuantLib::Date asof() const override; - QuantLib::Real getT0(QuantLib::Size i, QuantLib::Size d) const override; - void setT0(QuantLib::Real value, QuantLib::Size i, QuantLib::Size d) override; - QuantLib::Real get(QuantLib::Size i, QuantLib::Size j, QuantLib::Size k, QuantLib::Size d) const override; - void set(QuantLib::Real value, QuantLib::Size i, QuantLib::Size j, QuantLib::Size k, QuantLib::Size d) override; + Real getT0(Size i, Size d = 0) const override; + void setT0(Real value, Size i, Size d) override; + Real get(Size i, Size j, Size k, Size d = 0) const override; + void set(Real value, Size i, Size j, Size k, Size d) override; }; } // namespace analytics diff --git a/ORE-SWIG/test/test_inmemory_cube.py b/ORE-SWIG/test/test_inmemory_cube.py new file mode 100644 index 0000000000..681668984d --- /dev/null +++ b/ORE-SWIG/test/test_inmemory_cube.py @@ -0,0 +1,56 @@ +"""Regression tests for the wrongly abstract in-memory NPV cube classes. + +``SinglePrecisionInMemoryCubeN`` and ``DoublePrecisionInMemoryCubeN`` used to +raise ``AttributeError: No constructor defined - class is abstract`` on +construction, leaving Python with no way to build an NPV cube at all. The +overrides in the SWIG interface did not textually match the base's pure +virtuals -- they spelled the parameters ``QuantLib::Size`` (a type the SWIG +parser cannot resolve) and dropped the base's default arguments, which SWIG +expands into one required signature per arity -- so SWIG considered the pure +virtuals unimplemented and dropped the constructors. +""" + +import unittest + +import ORE + + +class InMemoryCubeConstructionTest(unittest.TestCase): + """The in-memory cube instantiations must be concrete.""" + + CUBE_CLASSES = ( + "SinglePrecisionInMemoryCubeN", + "DoublePrecisionInMemoryCubeN", + ) + + @staticmethod + def _cube_args(): + asof = ORE.Date(2, ORE.March, 2026) + ids = {"trade1", "trade2"} + dates = [ORE.Date(2, ORE.March, 2027), ORE.Date(2, ORE.March, 2028)] + return asof, ids, dates, 4 + + def test_cubes_construct(self) -> None: + """Both precisions used to raise 'class is abstract' here.""" + asof, ids, dates, samples = self._cube_args() + for name in self.CUBE_CLASSES: + with self.subTest(cube=name): + cube = getattr(ORE, name)(asof, ids, dates, samples) + self.assertIsInstance(cube, ORE.NPVCube) + self.assertEqual(cube.numIds(), 2) + self.assertEqual(cube.numDates(), 2) + self.assertEqual(cube.samples(), 4) + self.assertEqual(str(cube.asof()), str(asof)) + + def test_cube_set_get_roundtrip(self) -> None: + """Values written into a constructed cube must read back.""" + asof, ids, dates, samples = self._cube_args() + cube = ORE.DoublePrecisionInMemoryCubeN(asof, ids, dates, samples) + cube.setT0(123.25, 0, 0) + self.assertEqual(cube.getT0(0), 123.25) + cube.set(7.5, 1, 1, 3, 0) + self.assertEqual(cube.get(1, 1, 3), 7.5) + + +if __name__ == "__main__": + unittest.main()