From 337bc45c09dd6bca201a5ea99eccf191dc1dce02 Mon Sep 17 00:00:00 2001 From: AlQuemist <alquemist@Lyriks> Date: Wed, 3 Jan 2024 16:05:41 +0100 Subject: [PATCH] Add PyCore tests [WIP] --- Tests/Unit/PyBinding/CMakeLists.txt | 6 +- Tests/Unit/PyBinding/Embedded.cpp | 325 ++++------------------------ 2 files changed, 47 insertions(+), 284 deletions(-) diff --git a/Tests/Unit/PyBinding/CMakeLists.txt b/Tests/Unit/PyBinding/CMakeLists.txt index 5c8f2e4816f..176f983d306 100644 --- a/Tests/Unit/PyBinding/CMakeLists.txt +++ b/Tests/Unit/PyBinding/CMakeLists.txt @@ -2,8 +2,6 @@ # BornAgain/Tests/Unit/PyBinding/CMakeLists.txt ############################################################################ -return() - include(GoogleTest) # provides gtest_discover_tests set(test TestPyBinding) @@ -18,6 +16,8 @@ target_include_directories(${test} ${AUTO_WRAP_DIR} ) -target_link_libraries(${test} BornAgainSim gtest ${Python3_LIBRARIES}) +target_link_libraries(${test} BornAgainSim BornAgainPyCore gtest ${Python3_LIBRARIES}) + +target_compile_definitions(${test} PRIVATE -DBORNAGAIN_PYTHON) gtest_discover_tests(${test} TEST_PREFIX Unit.PyBinding.) diff --git a/Tests/Unit/PyBinding/Embedded.cpp b/Tests/Unit/PyBinding/Embedded.cpp index 43abeb72777..6a4aeb620a3 100644 --- a/Tests/Unit/PyBinding/Embedded.cpp +++ b/Tests/Unit/PyBinding/Embedded.cpp @@ -11,301 +11,64 @@ // // ************************************************************************************************ -#include "BABuild.h" -#include "Base/Py/PyCore.h" -#include "Base/Py/PyFmt.h" -#include "Base/Py/PyUtil.h" -#include "Sample/Multilayer/MultiLayer.h" -#include "Sample/Multilayer/PyImport.h" -#include "Sample/StandardSamples/ExemplarySamples.h" -#include "Sim/Export/ExportToPython.h" +//#include "BABuild.h" +#include "PyCore/Embed/PyInterpreter.h" +#include "PyCore/Embed/PyObjectPtr.h" +#include "PyCore/Embed/WithNumpy.h" +#include "PyCore/Sample/ImportMultiLayer.h" #include "Tests/GTestWrapper/google_test.h" -#include <iostream> -#include <sstream> -//! Importing numpy and accessing its version string. - -TEST(Embedded, ImportNumpy) -{ - Py_Initialize(); - - PyObject* pmod = PyImport_ImportModule("numpy"); - EXPECT_TRUE(pmod); - - PyObject* pvar = PyObject_GetAttrString(pmod, "__version__"); - Py_DecRef(pmod); - EXPECT_TRUE(pvar); - - auto version_string = Base::Python::toString(pvar); - Py_DecRef(pvar); - std::cout << "numpy_version_string=" << version_string << std::endl; - - Py_Finalize(); - - EXPECT_TRUE(!version_string.empty()); -} - -//! Creating instance of Cylinder and calling its method in embedded Python. - -TEST(Embedded, MethodCall) -{ - const double radius(5.0), height(6.0); - Py_Initialize(); - - PyObject* sysPath = PySys_GetObject((char*)"path"); - PyList_Append(sysPath, PyString_FromString(".")); - PyList_Append(sysPath, PyString_FromString(BABuild::buildLibDir().c_str())); - - PyObject* pmod = PyImport_ImportModule("bornagain"); - EXPECT_TRUE(pmod); - - PyObject* pclass = PyObject_GetAttrString(pmod, "Cylinder"); - Py_DecRef(pmod); - EXPECT_TRUE(pclass); - - PyObject* pargs = Py_BuildValue("(dd)", radius, height); - EXPECT_TRUE(pargs); - - PyObject* pinst = PyObject_Call(pclass, pargs, nullptr); - Py_DecRef(pclass); - Py_DecRef(pargs); - EXPECT_TRUE(pinst); - - // result of Cylinder - PyObject* pmeth = PyObject_GetAttrString(pinst, "height"); - Py_DecRef(pinst); - EXPECT_TRUE(pmeth); - - PyObject* pargs2 = Py_BuildValue("()"); - EXPECT_TRUE(pargs2); - - PyObject* pres = PyObject_Call(pmeth, pargs2, nullptr); - Py_DecRef(pmeth); - Py_DecRef(pargs); - - EXPECT_TRUE(pres); - - double value(0); - EXPECT_TRUE(PyArg_Parse(pres, "d", &value)); - - Py_DecRef(pres); - - Py_Finalize(); - - EXPECT_TRUE(value == height); -} - -//! From https://www.awasu.com/weblog/embedding-python/calling-python-code-from-your-program/ - -TEST(Embedded, CompiledFunction) -{ - Py_Initialize(); - - // compile our function - std::stringstream buf; - buf << "def add( n1 , n2 ) :" << std::endl << " return n1+n2" << std::endl; - - PyObject* pCompiledFn = Py_CompileString(buf.str().c_str(), "", Py_file_input); - EXPECT_TRUE(pCompiledFn); - - // create a module - PyObject* pModule = PyImport_ExecCodeModule((char*)"test", pCompiledFn); - EXPECT_TRUE(pModule); - - // locate the "add" function (it's an attribute of the module) - PyObject* pAddFn = PyObject_GetAttrString(pModule, "add"); - EXPECT_TRUE(pAddFn); - - // clean up - Py_DecRef(pAddFn); - Py_DecRef(pModule); - Py_DecRef(pCompiledFn); - - // ------------------------ - // using compiled function - // ------------------------ - - // create a new tuple with 2 elements - PyObject* pPosArgs = PyTuple_New(2); - - PyObject* pVal1 = PyInt_FromLong(10); - EXPECT_TRUE(pVal1); - - int rc = PyTuple_SetItem(pPosArgs, 0, pVal1); // nb: tuple position 0 - EXPECT_TRUE(rc == 0); - - PyObject* pVal2 = PyInt_FromLong(20); - EXPECT_TRUE(pVal2); +#include <vector> +#include <string> - rc = PyTuple_SetItem(pPosArgs, 1, pVal2); // nb: tuple position 0 - EXPECT_TRUE(rc == 0); - - // create a new dictionary - PyObject* pKywdArgs = PyDict_New(); - EXPECT_TRUE(pKywdArgs); - - // call our function - PyObject* pResult = PyObject_Call(pAddFn, pPosArgs, pKywdArgs); - EXPECT_TRUE(pResult); - - // convert the result to a string - PyObject* pResultRepr = PyObject_Repr(pResult); - std::string result = Base::Python::toString(pResultRepr); - Py_DecRef(pResultRepr); - - Py_Finalize(); - - EXPECT_TRUE(result == "30"); -} - -//! Creating MultiLayer in Python and extracting object to C++. -//! https://stackoverflow.com/questions/9040669/how-can-i-implement-a-c-class-in-python-to-be-called-by-c/ - -TEST(Embedded, ObjectExtract) -{ - PyObject* pmod = Base::Python::import_bornagain({BABuild::buildLibDir()}); - EXPECT_TRUE(pmod); - - PyObject* ml = PyObject_GetAttrString(pmod, "MultiLayer"); - Py_DecRef(pmod); - EXPECT_TRUE(ml); - - PyObject* instance = PyObject_CallFunctionObjArgs(ml, NULL); - - void* argp1 = nullptr; - swig_type_info* pTypeInfo = SWIG_TypeQuery("MultiLayer *"); - - const int res = SWIG_ConvertPtr(instance, &argp1, pTypeInfo, 0); - EXPECT_TRUE(SWIG_IsOK(res)); - - auto* sample = reinterpret_cast<MultiLayer*>(argp1); - std::string name = sample->className(); - - Py_DecRef(instance); - Py_DecRef(ml); - - Py_Finalize(); - - EXPECT_TRUE(name == "MultiLayer"); -} - -//! Running Python snippet which creates a sample in embedded way. -//! Casting resulting PyObject to C++ MultiLayer. +//! Importing numpy and accessing its version string. -TEST(Embedded, EmbeddedMultiLayer) +TEST(Embedded, PyInterpreterTest) { - PyObject* pmod = Base::Python::import_bornagain({BABuild::buildLibDir()}); - EXPECT_TRUE(pmod); - - // compile our function - std::stringstream buf; - buf << "import bornagain as ba\n"; - buf << "\n"; - buf << "def get_simulation():\n"; - buf << " m_vacuum = ba.RefractiveMaterial(\"Vacuum\", 0.0, 0.0)\n"; - buf << " vacuum_layer = ba.Layer(m_vacuum)\n"; - buf << " sample = ba.MultiLayer()\n"; - buf << " sample.addLayer(vacuum_layer)\n"; - buf << " return sample\n"; - - PyObject* pCompiledFn = Py_CompileString(buf.str().c_str(), "", Py_file_input); - EXPECT_TRUE(pCompiledFn); - - // create a module - PyObject* pModule = PyImport_ExecCodeModule((char*)"test", pCompiledFn); - EXPECT_TRUE(pModule); - - // locate the "get_simulation" function (it's an attribute of the module) - PyObject* pAddFn = PyObject_GetAttrString(pModule, "get_simulation"); - EXPECT_TRUE(pAddFn); + // initialize Python interpreter + PyInterpreter::initialize(); + EXPECT_TRUE(PyInterpreter::isInitialized()); - PyObject* instance = PyObject_CallFunctionObjArgs(pAddFn, NULL); - EXPECT_TRUE(instance); + // add Python path + PyInterpreter::addPythonPath("/Some/Extra/Python/Path/"); + EXPECT_FALSE(PyInterpreter::checkError()); - // clean up - Py_DecRef(pAddFn); - Py_DecRef(pModule); - Py_DecRef(pCompiledFn); + // import Numpy + PyObjectPtr numpy_module = PyInterpreter::import("numpy"); + EXPECT_TRUE(numpy_module.valid()); - void* argp1 = nullptr; - swig_type_info* pTypeInfo = SWIG_TypeQuery("MultiLayer *"); + // get runtime info + std::string runtime_info = PyInterpreter::runtimeInfo(); + std::cout << "Python runtime info:\n" << runtime_info << std::endl; - const int res = SWIG_ConvertPtr(instance, &argp1, pTypeInfo, 0); - EXPECT_TRUE(SWIG_IsOK(res)); + // initialize Numpy + PyInterpreter::Numpy::initialize(); + EXPECT_TRUE(PyInterpreter::Numpy::isInitialized()); - auto* sample = reinterpret_cast<MultiLayer*>(argp1); - size_t n_layers = sample->numberOfLayers(); + // create Numpy 1D and 2D arrays from a C-array + const int n_rows = 3, n_cols = 4, a_size = n_rows * n_cols; + double c_array[a_size]; + for (int ii = 0; ii < a_size; ++ii) + c_array[ii] = ii + 1; - Py_DecRef(instance); + PyObjectPtr np_array1d = PyInterpreter::Numpy::createArray1DfromC(c_array, static_cast<PyInterpreter::Numpy::np_size_t>(a_size)); + EXPECT_TRUE(np_array1d.valid()); - Py_Finalize(); + PyInterpreter::Numpy::np_size_t dims[2] = {n_rows, n_cols}; + PyObjectPtr np_array2d = PyInterpreter::Numpy::createArray2DfromC(c_array, dims); + EXPECT_TRUE(np_array2d.valid()); - EXPECT_TRUE(n_layers == 1); -} - -//! We use one of our standard sample builders to build a sample, then generate Python snippet -//! using our standard ExportToPython machinery. Ditto for a cloned sample. -//! Two exported code snippets must be identical. + PyObjectPtr np_as_array2d = PyInterpreter::Numpy::CArrayAsNpy2D(c_array, dims); + EXPECT_TRUE(np_as_array2d.valid()); -TEST(Embedded, CloneAndExportToPython) -{ - std::unique_ptr<MultiLayer> sample1(ExemplarySamples::createMultiLayerWithNCRoughness()); + double* np_array2d_ptr = PyInterpreter::Numpy::getDataPtr(np_array2d.get()); + EXPECT_TRUE(bool(np_array2d_ptr)); - const std::string code1 = Py::Export::sampleCode(*sample1); - std::cout << "Test ExportToPythonAndBack: code1:\n" << code1 << std::endl; - - std::unique_ptr<MultiLayer> sample2(sample1->clone()); - const std::string code2 = Py::Export::sampleCode(*sample2); - if (code2 != code1) - std::cout << "Test ExportToPythonAndBack: code2:\n" << code2 << std::endl; - else - std::cout << "Test ExportToPythonAndBack: code2 = code1" << std::endl; - EXPECT_TRUE(code2 == code1); -} - -//! We use one of our standard sample builders to build a sample, then generate Python snippet -//! using our standard ExportToPython machinery. -//! Given snippet is compiled and executed in embedded interpretor. Resulting multi layer -//! is casted back to C++ object and used again, to generate code snippet. -//! Two code snippets must be identical. - -TEST(Embedded, ExportToPythonAndBack) -{ - std::unique_ptr<MultiLayer> sample1(ExemplarySamples::createCylindersAndPrisms()); - - const std::string code1 = Py::Export::sampleCode(*sample1); - std::cout << "Test ExportToPythonAndBack: code1:\n" << code1 << std::endl; - - const std::string snippet = - "import bornagain as ba\n" + Py::Fmt::printImportedSymbols(code1) + "\n\n" + code1; - const auto sample2 = - Py::Import::createFromPython(snippet, "get_sample", BABuild::buildLibDir()); - const std::string code2 = Py::Export::sampleCode(*sample2); - if (code2 != code1) - std::cout << "Test ExportToPythonAndBack: code2:\n" << code2 << std::endl; - else - std::cout << "Test ExportToPythonAndBack: code2 = code1" << std::endl; - EXPECT_TRUE(code2 == code1); -} - -//! Retrieves list of functions from the imported script and checks, that there is -//! one function in a dictionary with name "get_simulation". - -TEST(Embedded, ModuleFunctionsList) -{ - // compile our function - std::stringstream buf; - buf << "import bornagain as ba\n"; - buf << "\n"; - buf << "def get_simulation():\n"; - buf << " m_vacuum = ba.RefractiveMaterial(\"Vacuum\", 0.0, 0.0)\n"; - buf << " vacuum_layer = ba.Layer(m_vacuum)\n"; - buf << " sample = ba.MultiLayer()\n"; - buf << " sample.addLayer(vacuum_layer)\n"; - buf << " return sample\n"; + // create an empty N-dimensional Numpy array + std::vector<std::size_t> dimensions{1, 2, 3}; + PyObjectPtr np_arrayNd = PyInterpreter::Numpy::arrayND(dimensions); + EXPECT_TRUE(np_arrayNd.valid()); - auto listOfFunc = Py::Import::listOfFunctions(buf.str(), BABuild::buildLibDir()); - for (auto s : listOfFunc) - std::cout << "AAA" << s << std::endl; - EXPECT_TRUE(listOfFunc.size() == 1 && listOfFunc.at(0) == "get_simulation"); + // set Python path + PyInterpreter::setPythonPath("/Some/Extra/Python/Path/"); } -- GitLab