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