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()