diff --git a/Framework/PythonInterface/core/inc/MantidPythonInterface/core/NDArray.h b/Framework/PythonInterface/core/inc/MantidPythonInterface/core/NDArray.h index 9939e5d5f57ee5aa6643629513a5dbde1246c11b..56b4bd7b603925bae834530a18f426fc07638a33 100644 --- a/Framework/PythonInterface/core/inc/MantidPythonInterface/core/NDArray.h +++ b/Framework/PythonInterface/core/inc/MantidPythonInterface/core/NDArray.h @@ -33,6 +33,7 @@ public: Py_intptr_t const *get_shape() const; int get_nd() const; void *get_data() const; + char get_typecode() const; NDArray astype(char dtype, bool copy = true) const; }; diff --git a/Framework/PythonInterface/core/src/NDArray.cpp b/Framework/PythonInterface/core/src/NDArray.cpp index c60d4b9f2503529080282b63db170a136d8f3e0b..a6b8b1294eca44cbe080b8bfba67337d3300780b 100644 --- a/Framework/PythonInterface/core/src/NDArray.cpp +++ b/Framework/PythonInterface/core/src/NDArray.cpp @@ -76,6 +76,14 @@ void *NDArray::get_data() const { return PyArray_DATA(reinterpret_cast<PyArrayObject *>(this->ptr())); } +/** + * See https://docs.scipy.org/doc/numpy/reference/arrays.dtypes.html + * @return The character code for the dtype of the array + */ +char NDArray::get_typecode() const { + return PyArray_DESCR(reinterpret_cast<PyArrayObject *>(this->ptr()))->type; +} + /** * Casts (and copies if necessary) the array to the given data type * @param dtype Character code for the numpy data types diff --git a/buildconfig/CMake/FindCxxTest.cmake b/buildconfig/CMake/FindCxxTest.cmake index 27a63e368dcf78f342d72d897425637361471215..98b03bd69c03ae18d5b6539ed536654a7119ac6b 100644 --- a/buildconfig/CMake/FindCxxTest.cmake +++ b/buildconfig/CMake/FindCxxTest.cmake @@ -106,7 +106,7 @@ include ( PrecompiledHeaderCommands ) # CXXTEST_ADD_TEST (public macro to add unit tests) #============================================================= macro(CXXTEST_ADD_TEST _cxxtest_testname) - # output directory + # output directory set (_cxxtest_output_dir ${CMAKE_CURRENT_BINARY_DIR}) if (CXXTEST_OUTPUT_DIR) set (_cxxtest_output_dir ${CXXTEST_OUTPUT_DIR}) diff --git a/buildconfig/CMake/QtTargetFunctions.cmake b/buildconfig/CMake/QtTargetFunctions.cmake index 200f6c907315bcce15e2e73d622773daa8019f0a..056ad2ce5073e6bf65d0d733f34ff3bbe6b8bdfd 100644 --- a/buildconfig/CMake/QtTargetFunctions.cmake +++ b/buildconfig/CMake/QtTargetFunctions.cmake @@ -62,6 +62,7 @@ endfunction() # option: NO_SUFFIX If included, no suffix is added to the target name # option: EXCLUDE_FROM_ALL If included, the target is excluded from target ALL # keyword: TARGET_NAME The name of the target. The target will have -Qt{QT_VERSION} appended to it. +# keyword: OUTPUT_NAME An optional filename for the library # keyword: QT_VERSION The major version of Qt to build against # keyword: SRC .cpp files to include in the target build # keyword: QT4_SRC .cpp files to include in a Qt4 build @@ -92,7 +93,7 @@ endfunction() function (mtd_add_qt_target) set (options LIBRARY EXECUTABLE NO_SUFFIX EXCLUDE_FROM_ALL) set (oneValueArgs - TARGET_NAME QT_VERSION OUTPUT_DIR_BASE OUTPUT_SUBDIR + TARGET_NAME OUTPUT_NAME QT_VERSION OUTPUT_DIR_BASE OUTPUT_SUBDIR INSTALL_DIR INSTALL_DIR_BASE PRECOMPILED) set (multiValueArgs SRC UI MOC NOMOC RES DEFS QT4_DEFS QT5_DEFS INCLUDE_DIRS SYSTEM_INCLUDE_DIRS LINK_LIBS @@ -137,9 +138,12 @@ function (mtd_add_qt_target) if (PARSED_NO_SUFFIX) set (_target ${PARSED_TARGET_NAME}) + set (_output_name ${PARSED_OUTPUT_NAME}) else() _append_qt_suffix (VERSION ${PARSED_QT_VERSION} OUTPUT_VARIABLE _target ${PARSED_TARGET_NAME}) + _append_qt_suffix (VERSION ${PARSED_QT_VERSION} OUTPUT_VARIABLE _output_name + ${PARSED_OUTPUT_NAME}) endif() _append_qt_suffix (VERSION ${PARSED_QT_VERSION} OUTPUT_VARIABLE _mtd_qt_libs ${PARSED_MTD_QT_LINK_LIBS}) @@ -164,6 +168,9 @@ function (mtd_add_qt_target) endif() # Target properties + if ( _output_name ) + set_target_properties ( ${_target} PROPERTIES OUTPUT_NAME ${_output_name} ) + endif () if (PARSED_OUTPUT_DIR_BASE) set ( _output_dir ${PARSED_OUTPUT_DIR_BASE}/qt${PARSED_QT_VERSION} ) if (PARSED_OUTPUT_SUBDIR) diff --git a/qt/widgets/CMakeLists.txt b/qt/widgets/CMakeLists.txt index 3ded62f824ed03e175b2db855544b352950624b8..b633ff6390255a6cf71dca5304ee2a9a529410f5 100644 --- a/qt/widgets/CMakeLists.txt +++ b/qt/widgets/CMakeLists.txt @@ -3,6 +3,7 @@ ########################################################################### add_subdirectory ( common ) add_subdirectory ( legacyqwt ) +add_subdirectory ( mplcpp ) add_subdirectory ( instrumentview ) add_subdirectory ( sliceviewer ) add_subdirectory ( spectrumviewer ) diff --git a/qt/widgets/mplcpp/CMakeLists.txt b/qt/widgets/mplcpp/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..97284a2caf175a33c70abf64933f7a1a75b88291 --- /dev/null +++ b/qt/widgets/mplcpp/CMakeLists.txt @@ -0,0 +1,58 @@ +# Rules for matplotlib cpp library +set ( LIB_SRCS + src/Artist.cpp + src/Axes.cpp + src/Colors.cpp + src/Colormap.cpp + src/Figure.cpp + src/FigureCanvasQt.cpp + src/Line2D.cpp + src/ScalarMappable.cpp +) + +set ( MOC_HEADERS + inc/MantidQtWidgets/MplCpp/FigureCanvasQt.h +) + +set (NOMOC_HEADERS + inc/MantidQtWidgets/MplCpp/Artist.h + inc/MantidQtWidgets/MplCpp/Axes.h + inc/MantidQtWidgets/MplCpp/Colors.h + inc/MantidQtWidgets/MplCpp/Colormap.h + inc/MantidQtWidgets/MplCpp/Figure.h + inc/MantidQtWidgets/MplCpp/Line2D.h + inc/MantidQtWidgets/MplCpp/ScalarMappable.h +) + +find_package ( BoostPython REQUIRED ) + +# Target +mtd_add_qt_library (TARGET_NAME MantidQtWidgetsMplCpp + QT_VERSION 5 + SRC ${LIB_SRCS} + MOC ${MOC_HEADERS} + NOMOC ${NOMOC_HEADERS} + DEFS + IN_MANTIDQT_MPLCPP + INCLUDE_DIRS + inc + ../../../Framework/PythonInterface/core/inc + ${Boost_INCLUDE_DIRS} + ${PYTHON_INCLUDE_PATH} + ${PYTHON_NUMPY_INCLUDE_DIR} + LINK_LIBS + ${TCMALLOC_LIBRARIES_LINKTIME} + ${Boost_LIBRARIES} + ${PYTHON_LIBRARIES} + PythonInterfaceCore + INSTALL_DIR + ${LIB_DIR} + OSX_INSTALL_RPATH + @loader_path/../MacOS + @loader_path/../Libraries + LINUX_INSTALL_RPATH + "\$ORIGIN/../${LIB_DIR}" +) + +# Testing +add_subdirectory ( test ) diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Artist.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Artist.h new file mode 100644 index 0000000000000000000000000000000000000000..2451d2dcdb6f42d45ef1cbe2b664c0a6eda196c8 --- /dev/null +++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Artist.h @@ -0,0 +1,42 @@ +#ifndef MPLCPP_ARTIST_H +#define MPLCPP_ARTIST_H +/* + Copyright © 2018 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge + National Laboratory & European Spallation Source + + This file is part of Mantid. + + Mantid is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + Mantid is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +*/ +#include "MantidQtWidgets/MplCpp/DllConfig.h" +#include "MantidQtWidgets/MplCpp/Python/Object.h" + +namespace MantidQt { +namespace Widgets { +namespace MplCpp { + +/** + * Wraps a matplotlib.artist object with a C++ interface + */ +class MANTID_MPLCPP_DLL Artist : public Python::InstanceHolder { +public: + // Holds a reference to the matplotlib artist object + explicit Artist(Python::Object obj); + + // Remove the artist from the canvas + void remove(); +}; + +} // namespace MplCpp +} // namespace Widgets +} // namespace MantidQt + +#endif // MPLCPP_ARTIST_H diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Axes.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Axes.h new file mode 100644 index 0000000000000000000000000000000000000000..3ebb13b06d230a8114acf8ee124a0fa7129b1931 --- /dev/null +++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Axes.h @@ -0,0 +1,48 @@ +#ifndef MPLCPP_AXES_H +#define MPLCPP_AXES_H +/* + Copyright © 2018 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge + National Laboratory & European Spallation Source + + This file is part of Mantid. + + Mantid is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + Mantid is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +*/ +#include "MantidQtWidgets/MplCpp/DllConfig.h" +#include "MantidQtWidgets/MplCpp/Line2D.h" + +namespace MantidQt { +namespace Widgets { +namespace MplCpp { + +class MANTID_MPLCPP_DLL Axes : public Python::InstanceHolder { +public: + explicit Axes(Python::Object obj); + + /// @name Formatting + /// @{ + void setXLabel(const char *label); + void setYLabel(const char *label); + void setTitle(const char *label); + /// @} + + /// @name Plotting + /// @{ + Line2D plot(std::vector<double> xdata, std::vector<double> ydata, + const char *format = "b-"); + /// @} +}; + +} // namespace MplCpp +} // namespace Widgets +} // namespace MantidQt + +#endif // MPLCPP_AXES_H diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/BackendQt.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/BackendQt.h new file mode 100644 index 0000000000000000000000000000000000000000..9997a483b179c0c989cf8682c063b5f2fb4c811a --- /dev/null +++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/BackendQt.h @@ -0,0 +1,57 @@ +#ifndef MPLCPP_BACKENDQT_H +#define MPLCPP_BACKENDQT_H +/* + Copyright © 2018 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge + National Laboratory & European Spallation Source + + This file is part of Mantid. + + Mantid is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + Mantid is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +*/ +#include "MantidQtWidgets/MplCpp/Python/Object.h" +#include <QtGlobal> + +/* + * Defines constants relating to the matplotlib backend + */ + +#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0) +#error "Qt >= 5 required" +#elif QT_VERSION >= QT_VERSION_CHECK(5, 0, 0) && \ + QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + +/// Define PyQt version that matches the matplotlib backend +const char *PYQT_MODULE = "PyQt5"; + +/// Define matplotlib backend that will be used to draw the canvas +const char *MPL_QT_BACKEND = "matplotlib.backends.backend_qt5agg"; + +#else +#error "Unknown Qt version. Cannot determine matplotlib backend." +#endif + +namespace MantidQt { +namespace Widgets { +namespace MplCpp { + +/// Import and return the backend module for this version of Qt +Python::Object backendModule() { + // Importing PyQt first allows matplotlib to select the correct + // backend + Python::NewRef(PyImport_ImportModule(PYQT_MODULE)); + return Python::NewRef(PyImport_ImportModule(MPL_QT_BACKEND)); +} + +} // namespace MplCpp +} // namespace Widgets +} // namespace MantidQt + +#endif // MPLCPP_BACKENDQT_H diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Colormap.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Colormap.h new file mode 100644 index 0000000000000000000000000000000000000000..520d426e8a811e3eb01b41f9c5c7dab1e1e83be3 --- /dev/null +++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Colormap.h @@ -0,0 +1,45 @@ +#ifndef MPLCPP_COLORMAP_H +#define MPLCPP_COLORMAP_H +/* + Copyright © 2018 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge + National Laboratory & European Spallation Source + + This file is part of Mantid. + + Mantid is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + Mantid is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +*/ +#include "MantidQtWidgets/MplCpp/DllConfig.h" +#include "MantidQtWidgets/MplCpp/Python/Object.h" +#include <QString> + +namespace MantidQt { +namespace Widgets { +namespace MplCpp { + +/** + * @brief Defines a wrapper + */ +class MANTID_MPLCPP_DLL Colormap : public Python::InstanceHolder { +public: + Colormap(Python::Object obj); +}; + +/// Return the matplotlib.cm module +MANTID_MPLCPP_DLL Python::Object cmModule(); + +/// Return the named colormap if it exists +MANTID_MPLCPP_DLL Colormap getCMap(const QString &name); + +} // namespace MplCpp +} // namespace Widgets +} // namespace MantidQt + +#endif // MPLCPP_COLORMAP_H diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Colors.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Colors.h new file mode 100644 index 0000000000000000000000000000000000000000..9c1e592fd0a258c100e37d640610342c5bc2e4e6 --- /dev/null +++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Colors.h @@ -0,0 +1,76 @@ +#ifndef MPLCPP_COLORS_H +#define MPLCPP_COLORS_H +/* + Copyright © 2018 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge + National Laboratory & European Spallation Source + + This file is part of Mantid. + + Mantid is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + Mantid is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +*/ +#include "MantidQtWidgets/MplCpp/DllConfig.h" +#include "MantidQtWidgets/MplCpp/Python/Object.h" + +/** + * @file Contains definitions of wrappers for types in + * matplotlib.colors. These types provide the ability to + * normalize data according to different scale types. + * See https://matplotlib.org/2.2.3/api/colors_api.html + */ + +namespace MantidQt { +namespace Widgets { +namespace MplCpp { + +/** + * @brief C++ base class for Normalize types to allow a common interface + * to distinguish from a general Python::InstanceHolder. + */ +class MANTID_MPLCPP_DLL NormalizeBase : public Python::InstanceHolder { +public: + NormalizeBase(Python::Object obj); +}; + +/** + * @brief The Normalize class provides a simple mapping of data in + * the internal [vmin, vmax] to the interval [0, 1]. + * See + * https://matplotlib.org/2.2.3/api/_as_gen/matplotlib.colors.Normalize.html#matplotlib.colors.Normalize + */ +class MANTID_MPLCPP_DLL Normalize : public NormalizeBase { +public: + Normalize(double vmin, double vmax); +}; + +/** + * @brief Map data values [vmin, vmax] onto a symmetrical log scale. + * See + * https://matplotlib.org/2.2.3/api/_as_gen/matplotlib.colors.SymLogNorm.html#matplotlib.colors.SymLogNorm + */ +class MANTID_MPLCPP_DLL SymLogNorm : public NormalizeBase { +public: + SymLogNorm(double linthresh, double linscale, double vmin, double vmax); +}; + +/** + * @brief Map data values [vmin, vmax] onto a power law scale. + * See + * https://matplotlib.org/2.2.3/api/_as_gen/matplotlib.colors.PowerNorm.html#matplotlib.colors.PowerNorm + */ +class MANTID_MPLCPP_DLL PowerNorm : public NormalizeBase { +public: + PowerNorm(double gamma, double vmin, double vmax); +}; + +} // namespace MplCpp +} // namespace Widgets +} // namespace MantidQt +#endif // MPLCPP_COLORS_H diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/DllConfig.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/DllConfig.h new file mode 100644 index 0000000000000000000000000000000000000000..b46991b2360e4d1a5cb04ebd3ad77a5e6772111f --- /dev/null +++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/DllConfig.h @@ -0,0 +1,12 @@ +#ifndef MANTIDQT_MPLCPP_DLLCONFIG_H_ +#define MANTIDQT_MPLCPP_DLLCONFIG_H_ + +#include "MantidKernel/System.h" + +#ifdef IN_MANTIDQT_MPLCPP +#define MANTID_MPLCPP_DLL DLLExport +#else +#define MANTID_MPLCPP_DLL DLLImport +#endif /* IN_MANTIDQT_MPLCPP */ + +#endif // MANTIDQT_MPLCPP_DLLCONFIG_H_ diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Figure.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Figure.h new file mode 100644 index 0000000000000000000000000000000000000000..83fbda2dbd7749c6b8d7960703fc133bf6764e12 --- /dev/null +++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Figure.h @@ -0,0 +1,50 @@ +#ifndef MPLCPP_FIGURE_H +#define MPLCPP_FIGURE_H +/* + Copyright © 2018 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge + National Laboratory & European Spallation Source + + This file is part of Mantid. + + Mantid is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + Mantid is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +*/ +#include "MantidQtWidgets/MplCpp/Axes.h" +#include "MantidQtWidgets/MplCpp/DllConfig.h" +#include "MantidQtWidgets/MplCpp/Python/Object.h" + +namespace MantidQt { +namespace Widgets { +namespace MplCpp { + +/** + * A thin C++ wrapper to create a new matplotlib figure + */ +class MANTID_MPLCPP_DLL Figure : public Python::InstanceHolder { +public: + Figure(bool tightLayout = true); + + /** + * @param index The index of the axes to return + * @return The axes instance + */ + inline Axes axes(size_t index) const { + return Axes{pyobj().attr("axes")[index]}; + } + + Axes addAxes(double left, double bottom, double width, double height); + Axes addSubPlot(int subplotspec); +}; + +} // namespace MplCpp +} // namespace Widgets +} // namespace MantidQt + +#endif // MPLCPP_FIGURE_H diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/FigureCanvasQt.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/FigureCanvasQt.h new file mode 100644 index 0000000000000000000000000000000000000000..1cc2b81c8241772042eb95061394d68b1179bb49 --- /dev/null +++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/FigureCanvasQt.h @@ -0,0 +1,51 @@ +#ifndef MPLCPP_FIGURECANVASQT_H +#define MPLCPP_FIGURECANVASQT_H +/* + Copyright © 2018 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge + National Laboratory & European Spallation Source + + This file is part of Mantid. + + Mantid is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + Mantid is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +*/ +#include "MantidQtWidgets/MplCpp/DllConfig.h" +#include "MantidQtWidgets/MplCpp/Figure.h" + +#include <QWidget> + +namespace MantidQt { +namespace Widgets { +namespace MplCpp { + +/** + * @brief FigureCanvasQt defines a QWidget that can be embedded within + * another widget to display a matplotlib figure. It roughly follows + * the matplotlib example on embedding a matplotlib canvas: + * https://matplotlib.org/examples/user_interfaces/embedding_in_qt5.html + */ +class MANTID_MPLCPP_DLL FigureCanvasQt : public QWidget, + public Python::InstanceHolder { + Q_OBJECT +public: + FigureCanvasQt(int subplotspec, QWidget *parent = nullptr); + FigureCanvasQt(Figure fig, QWidget *parent = nullptr); + + /// Non-const access to the current active axes instance. + inline Axes &gca() { return m_axes; } + +private: // members + Axes m_axes; +}; + +} // namespace MplCpp +} // namespace Widgets +} // namespace MantidQt +#endif // MPLCPP_FIGURECANVASQT_H diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Line2D.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Line2D.h new file mode 100644 index 0000000000000000000000000000000000000000..24dd93158b065c4074efd28c0e0c8279bf0b0a61 --- /dev/null +++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Line2D.h @@ -0,0 +1,54 @@ +#ifndef MPLCPP_LINE2D_H +#define MPLCPP_LINE2D_H +/* + Copyright © 2018 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge + National Laboratory & European Spallation Source + + This file is part of Mantid. + + Mantid is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + Mantid is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +*/ +#include "MantidQtWidgets/MplCpp/Artist.h" +#include "MantidQtWidgets/MplCpp/DllConfig.h" +#include <vector> + +namespace MantidQt { +namespace Widgets { +namespace MplCpp { + +/** + * @brief Line2D holds an instance of a matplotlib Line2D type. + * This type is designed to hold an existing Line2D instance that contains + * data in numpy arrays that do not own their data but have a view on to an + * existing vector of data. This object keeps the data alive. + */ +class MANTID_MPLCPP_DLL Line2D : public Artist { +public: + Line2D(Python::Object obj, std::vector<double> xdataOwner, + std::vector<double> ydataOwner); + ~Line2D(); + // not copyable + Line2D(const Line2D &) = delete; + Line2D &operator=(const Line2D &) = delete; + // movable + Line2D(Line2D &&) = default; + Line2D &operator=(Line2D &&) = default; + +private: + // Containers that own the data making up the line + std::vector<double> m_xOwner, m_yOwner; +}; + +} // namespace MplCpp +} // namespace Widgets +} // namespace MantidQt + +#endif // MPLCPP_LINE2D_H diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Python/Object.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Python/Object.h new file mode 100644 index 0000000000000000000000000000000000000000..da0f9353bc9eb6e67953de9e8c5686c8d3bca85f --- /dev/null +++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Python/Object.h @@ -0,0 +1,82 @@ +#ifndef MPLCPP_PYTHON_OBJECT_H +#define MPLCPP_PYTHON_OBJECT_H +/* + Copyright © 2017 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge + National Laboratory & European Spallation Source + + This file is part of Mantid. + + Mantid is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + Mantid is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +*/ +#include <boost/python/borrowed.hpp> +#include <boost/python/object.hpp> +#include <stdexcept> + +namespace MantidQt { +namespace Widgets { +namespace MplCpp { +namespace Python { + +// Alias for boost python object wrapper +using Object = boost::python::object; + +// Alias for handle wrapping a raw PyObject* +template <typename T = PyObject> using Handle = boost::python::handle<T>; + +// Alias to borrowed function that increments the reference count +template <typename T> using BorrowedRef = boost::python::detail::borrowed<T>; + +// Helper to create an Object from a new reference to a raw PyObject* +inline Python::Object NewRef(PyObject *obj) { + return Python::Object(Python::Handle<>(obj)); +} + +// Alias for exception indicating Python error handler is set +using ErrorAlreadySet = boost::python::error_already_set; + +/** + * @brief Holds a Python instance of an object with a method to access it + */ +class InstanceHolder { +public: + /** + * Construct an InstanceHolder with an existing Python object. + * @param obj An existing Python instance + */ + explicit InstanceHolder(Object obj) : m_instance(std::move(obj)) {} + + /** + * Construct an InstanceHolder with an existing Python object. The provided + * object is checked to ensure it has the named attr + * @param obj An existing Python instance + * @param attr The name of an attribute that must exist on the wrapped + * object + */ + InstanceHolder(Object obj, const char *attr) : m_instance(std::move(obj)) { + if (PyObject_HasAttrString(pyobj().ptr(), attr) == 0) { + throw std::invalid_argument(std::string("object has no attribute ") + + attr); + } + } + + /// Return the held instance object + inline const Object &pyobj() const { return m_instance; } + +private: + Object m_instance; +}; + +} // namespace Python +} // namespace MplCpp +} // namespace Widgets +} // namespace MantidQt + +#endif // MPLCPP_PYTHON_OBJECT_H diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Python/Sip.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Python/Sip.h new file mode 100644 index 0000000000000000000000000000000000000000..5d495c36c8519f4c5140e6385fd3e5d51662ba2b --- /dev/null +++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/Python/Sip.h @@ -0,0 +1,92 @@ +#ifndef MPLCPP_SIPUTILS_H +#define MPLCPP_SIPUTILS_H +/* + Copyright © 2018 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge + National Laboratory & European Spallation Source + + This file is part of Mantid. + + Mantid is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + Mantid is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +*/ +#include "MantidQtWidgets/MplCpp/Python/Object.h" +#include <sip.h> +#include <stdexcept> + +namespace MantidQt { +namespace Widgets { +namespace MplCpp { +namespace Python { + +namespace detail { +/** + * @return A pointer to the C++ sip api object + */ +const sipAPIDef *sipAPI() { + static const sipAPIDef *sip_API = nullptr; + if (sip_API) + return sip_API; +#if defined(SIP_USE_PYCAPSULE) + sip_API = (const sipAPIDef *)PyCapsule_Import("sip._C_API", 0); +#else + /* Import the SIP module. */ + PyObject *sip_module = PyImport_ImportModule("sip"); + if (sip_module == NULL) + throw std::runtime_error("sip_api() - Error importing sip module"); + + /* Get the module's dictionary. */ + PyObject *sip_module_dict = PyModule_GetDict(sip_module); + + /* Get the "_C_API" attribute. */ + PyObject *c_api = PyDict_GetItemString(sip_module_dict, "_C_API"); + if (c_api == NULL) + throw std::runtime_error( + "sip_api() - Unable to find _C_API attribute in sip dictionary"); + + /* Sanity check that it is the right type. */ + if (!PyCObject_Check(c_api)) + throw std::runtime_error("sip_api() - _C_API type is not a CObject"); + + /* Get the actual pointer from the object. */ + sip_API = (const sipAPIDef *)PyCObject_AsVoidPtr(c_api); +#endif + return sip_API; +} +} // namespace detail + +/** + * Extract a C++ object of type T from the Python object + * @param obj A sip-wrapped Python object + */ +template <typename T> T *extract(const Object &obj) { + const auto sipapi = detail::sipAPI(); + if (!PyObject_TypeCheck(obj.ptr(), sipapi->api_wrapper_type)) { + throw std::runtime_error("extract() - Object is not a sip-wrapped type."); + } + // transfer ownership from python to C++ + sipapi->api_transfer_to(obj.ptr(), 0); + // reinterpret to sipWrapper + auto wrapper = reinterpret_cast<sipSimpleWrapper *>(obj.ptr()); +#if (SIP_API_MAJOR_NR == 8 && SIP_API_MINOR_NR >= 1) || SIP_API_MAJOR_NR > 8 + return static_cast<T *>(sipapi->api_get_address(wrapper)); +#elif SIP_API_MAJOR_NR == 8 + return static_cast<T *>(wrapper->data); +#else + return static_cast<T *>(wrapper->u.cppPtr); +#endif +} + +} // namespace Python +} // namespace MplCpp +} // namespace Widgets + +} // namespace MantidQt + +#endif // MPLCPP_SIPUTILS_H diff --git a/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/ScalarMappable.h b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/ScalarMappable.h new file mode 100644 index 0000000000000000000000000000000000000000..1f356277f99b19a8ab85c1ce3dc865b15d5ab700 --- /dev/null +++ b/qt/widgets/mplcpp/inc/MantidQtWidgets/MplCpp/ScalarMappable.h @@ -0,0 +1,45 @@ +#ifndef MPLCPP_SCALARMAPPABLE_H +#define MPLCPP_SCALARMAPPABLE_H +/* + Copyright © 2018 ISIS Rutherford Appleton Laboratory, NScD Oak Ridge + National Laboratory & European Spallation Source + + This file is part of Mantid. + + Mantid is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + Mantid is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. +*/ +#include "MantidQtWidgets/MplCpp/Colormap.h" +#include "MantidQtWidgets/MplCpp/Colors.h" +#include "MantidQtWidgets/MplCpp/DllConfig.h" + +#include <QRgb> + +namespace MantidQt { +namespace Widgets { +namespace MplCpp { + +/** + * @brief A C++ wrapper around the matplotlib.cm.ScalarMappable + * type to provide the capability to map an arbitrary data + * value to an RGBA value within a given colormap + */ +class MANTID_MPLCPP_DLL ScalarMappable : public Python::InstanceHolder { +public: + ScalarMappable(const NormalizeBase &norm, const Colormap &cmap); + + QRgb toRGBA(double x, double alpha = 1.0) const; +}; + +} // namespace MplCpp +} // namespace Widgets +} // namespace MantidQt + +#endif // MPLCPP_SCALARMAPPABLE_H diff --git a/qt/widgets/mplcpp/src/Artist.cpp b/qt/widgets/mplcpp/src/Artist.cpp new file mode 100644 index 0000000000000000000000000000000000000000..64bd3509d03a15d9af5fcc9cf48d46459397f3b7 --- /dev/null +++ b/qt/widgets/mplcpp/src/Artist.cpp @@ -0,0 +1,21 @@ +#include "MantidQtWidgets/MplCpp/Artist.h" +#include <cassert> + +namespace MantidQt { +namespace Widgets { +namespace MplCpp { + +/** + * @brief Create an Artist instance around an existing matplotlib Artist + * @param obj A Python object pointing to a matplotlib artist + */ +Artist::Artist(Python::Object obj) : InstanceHolder(std::move(obj), "draw") {} + +/** + * Call .remove on the underlying artist + */ +void Artist::remove() { pyobj().attr("remove")(); } + +} // namespace MplCpp +} // namespace Widgets +} // namespace MantidQt diff --git a/qt/widgets/mplcpp/src/Axes.cpp b/qt/widgets/mplcpp/src/Axes.cpp new file mode 100644 index 0000000000000000000000000000000000000000..3107c43d828ee545eb79b7ed20aa908142e08c96 --- /dev/null +++ b/qt/widgets/mplcpp/src/Axes.cpp @@ -0,0 +1,77 @@ +#include "MantidQtWidgets/MplCpp/Axes.h" +#include "MantidPythonInterface/core/Converters/VectorToNDArray.h" +#include "MantidPythonInterface/core/Converters/WrapWithNDArray.h" +#include "MantidPythonInterface/core/ErrorHandling.h" + +namespace MantidQt { +namespace Widgets { +namespace MplCpp { + +using Mantid::PythonInterface::Converters::VectorToNDArray; +using Mantid::PythonInterface::Converters::WrapReadOnly; +using Mantid::PythonInterface::PythonRuntimeError; + +/** + * Construct an Axes wrapper around an existing Axes instance + * @param obj A matplotlib.axes.Axes instance + */ +Axes::Axes(Python::Object obj) : InstanceHolder(std::move(obj), "plot") {} + +/** + * @brief Set the X-axis label + * @param label String for the axis label + */ +void Axes::setXLabel(const char *label) { pyobj().attr("set_xlabel")(label); } + +/** + * @brief Set the Y-axis label + * @param label String for the axis label + */ +void Axes::setYLabel(const char *label) { pyobj().attr("set_ylabel")(label); } + +/** + * @brief Set the title + * @param label String for the title label + */ +void Axes::setTitle(const char *label) { pyobj().attr("set_title")(label); } + +/** + * @brief Take the data and draw a single Line2D on the axes + * @param xdata A vector containing the X data + * @param ydata A vector containing the Y data + * @param format A format string accepted by matplotlib.axes.Axes.plot. The + * default is 'b-' + * @return A new Line2D object that owns the xdata, ydata vectors. If the + * return value is not captured the line will be automatically removed from + * the canvas as the vector data will be destroyed. + */ +Line2D Axes::plot(std::vector<double> xdata, std::vector<double> ydata, + const char *format) { + auto throwIfEmpty = [](const std::vector<double> &data, char vecId) { + if (data.empty()) { + throw std::invalid_argument( + std::string("Cannot plot line. Empty vector=") + vecId); + } + }; + throwIfEmpty(xdata, 'X'); + throwIfEmpty(ydata, 'Y'); + + // Wrap the vector data in a numpy facade to avoid a copy. + // The vector still owns the data so it needs to be kept alive too + // It is transferred to the Line2D for this purpose. + VectorToNDArray<double, WrapReadOnly> wrapNDArray; + Python::Object xarray{Python::NewRef(wrapNDArray(xdata))}, + yarray{Python::NewRef(wrapNDArray(ydata))}; + + try { + return Line2D{pyobj().attr("plot")(xarray, yarray, format)[0], + std::move(xdata), std::move(ydata)}; + + } catch (Python::ErrorAlreadySet &) { + throw PythonRuntimeError(); + } +} + +} // namespace MplCpp +} // namespace Widgets +} // namespace MantidQt diff --git a/qt/widgets/mplcpp/src/Colormap.cpp b/qt/widgets/mplcpp/src/Colormap.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e1da71ebf63ca17a021c065a3ca26c94422e1d7b --- /dev/null +++ b/qt/widgets/mplcpp/src/Colormap.cpp @@ -0,0 +1,42 @@ +#include "MantidQtWidgets/MplCpp/Colormap.h" +#include "MantidQtWidgets/MplCpp/Colors.h" + +#include "MantidPythonInterface/core/ErrorHandling.h" + +using Mantid::PythonInterface::PythonRuntimeError; + +namespace MantidQt { +namespace Widgets { +namespace MplCpp { + +/** + * @brief Construct a Colormap object given a name + */ +Colormap::Colormap(Python::Object obj) + : Python::InstanceHolder(obj, "is_gray") {} + +/** + * @return A reference to the matplotlib.cm module + */ +Python::Object cmModule() { + Python::Object cmModule{ + Python::NewRef(PyImport_ImportModule("matplotlib.cm"))}; + return cmModule; +} + +/** + * @param name The name of an existing colormap. + * @return A new Colormap instance for the named map + * @throws std::invalid_argument if the name is unknown + */ +Colormap getCMap(const QString &name) { + try { + return cmModule().attr("get_cmap")(name.toLatin1().constData()); + } catch (Python::ErrorAlreadySet &) { + throw PythonRuntimeError(); + } +} + +} // namespace MplCpp +} // namespace Widgets +} // namespace MantidQt diff --git a/qt/widgets/mplcpp/src/Colors.cpp b/qt/widgets/mplcpp/src/Colors.cpp new file mode 100644 index 0000000000000000000000000000000000000000..9122de96d55428b084e26fd8bc1803fa312e49ff --- /dev/null +++ b/qt/widgets/mplcpp/src/Colors.cpp @@ -0,0 +1,72 @@ +#include "MantidQtWidgets/MplCpp/Colors.h" +#include "MantidPythonInterface/core/ErrorHandling.h" + +using Mantid::PythonInterface::PythonRuntimeError; + +namespace MantidQt { +namespace Widgets { +namespace MplCpp { + +namespace { +/** + * @return A reference to the matplotlib.colors module + */ +Python::Object colorsModule() { + Python::Object colorsModule{ + Python::NewRef(PyImport_ImportModule("matplotlib.colors"))}; + return colorsModule; +} +} // namespace + +// ------------------------ NormalizeBase--------------------------------------- +/** + * @brief NormalizeBase::NormalizeBase + * @param obj An existing Normalize instance or subtype + */ +NormalizeBase::NormalizeBase(Python::Object obj) + : Python::InstanceHolder(std::move(obj), "autoscale") {} + +// ------------------------ Normalize ------------------------------------------ + +/** + * @brief Construct a Normalize object mapping data from [vmin, vmax] + * to [0, 1] + * @param vmin Minimum value of the data interval + * @param vmax Maximum value of the data interval + */ +Normalize::Normalize(double vmin, double vmax) + : NormalizeBase(colorsModule().attr("Normalize")(vmin, vmax)) {} + +// ------------------------ SymLogNorm ----------------------------------------- + +/** + * @brief Construct a SymLogNorm object mapping data from [vmin, vmax] + * to a symmetric logarithm scale + * @param linthresh The range within which the plot is linear + * @param linscale This allows the linear range (-linthresh to linthresh) to be + * stretched relative to the logarithmic range. + * See + * https://matplotlib.org/2.2.3/api/_as_gen/matplotlib.colors.SymLogNorm.html#matplotlib.colors.SymLogNorm + * @param vmin Minimum value of the data interval + * @param vmax Maximum value of the data interval + */ +SymLogNorm::SymLogNorm(double linthresh, double linscale, double vmin, + double vmax) + : NormalizeBase( + colorsModule().attr("SymLogNorm")(linthresh, linscale, vmin, vmax)) {} + +// ------------------------ PowerNorm ------------------------------------------ + +/** + * @brief Construct a PowerNorm object to map data from [vmin,vmax] to + * [0,1] pn a power-law scale + * @param gamma The exponent for the power-law + * @param vmin Minimum value of the data interval + * @param vmax Maximum value of the data interval + */ +PowerNorm::PowerNorm(double gamma, double vmin, double vmax) + : NormalizeBase(colorsModule().attr("PowerNorm")(gamma, vmin, vmax)) {} + +} // namespace MplCpp +} // namespace Widgets +} // namespace MantidQt diff --git a/qt/widgets/mplcpp/src/Figure.cpp b/qt/widgets/mplcpp/src/Figure.cpp new file mode 100644 index 0000000000000000000000000000000000000000..5e1fb6f3a5de4e9e0371f051ea6047902140ae91 --- /dev/null +++ b/qt/widgets/mplcpp/src/Figure.cpp @@ -0,0 +1,52 @@ +#include "MantidQtWidgets/MplCpp/Figure.h" + +namespace MantidQt { +namespace Widgets { +namespace MplCpp { + +namespace { +Python::Object newFigure(bool tightLayout = true) { + Python::Object figureModule{ + Python::NewRef(PyImport_ImportModule("matplotlib.figure"))}; + auto fig = figureModule.attr("Figure")(); + if (tightLayout) { + auto kwargs = Python::NewRef(Py_BuildValue("{s:f}", "pad", 0.5)); + fig.attr("set_tight_layout")(kwargs); + } + return fig; +} +} // namespace + +/** + * Construct a new default figure. + * @param tightLayout If true set a tight layout on the matplotlib figure + */ +Figure::Figure(bool tightLayout) + : Python::InstanceHolder(newFigure(tightLayout)) {} + +/** + * Add an Axes of the given dimensions to the current figure + * All quantities are in fractions of figure width and height + * @param left The X coordinate of the lower-left corner + * @param bottom The Y coordinate of the lower-left corner + * @param width The width of the Axes + * @param height The heigh of the Axes + * @return A new Axes instance + */ +Axes Figure::addAxes(double left, double bottom, double width, double height) { + return Axes{pyobj().attr("add_axes")( + Python::NewRef(Py_BuildValue("(ffff)", left, bottom, width, height)))}; +} + +/** + * Add a subplot Axes to the figure + * @param subplotspec + * @return + */ +Axes Figure::addSubPlot(int subplotspec) { + return Axes{pyobj().attr("add_subplot")(subplotspec)}; +} + +} // namespace MplCpp +} // namespace Widgets +} // namespace MantidQt diff --git a/qt/widgets/mplcpp/src/FigureCanvasQt.cpp b/qt/widgets/mplcpp/src/FigureCanvasQt.cpp new file mode 100644 index 0000000000000000000000000000000000000000..d6530dbc2e558b2f62144bcef8f9ce8bbcfe5689 --- /dev/null +++ b/qt/widgets/mplcpp/src/FigureCanvasQt.cpp @@ -0,0 +1,75 @@ +#include "MantidQtWidgets/MplCpp/FigureCanvasQt.h" +#include "MantidQtWidgets/MplCpp/BackendQt.h" +#include "MantidQtWidgets/MplCpp/Figure.h" +#include "MantidQtWidgets/MplCpp/Python/Sip.h" + +#include <QVBoxLayout> + +namespace MantidQt { +namespace Widgets { +namespace MplCpp { +namespace { +/** + * @param fig An existing matplotlib Figure instance + * @return A new FigureCanvasQT object + */ +Python::Object createPyCanvasFromFigure(Figure fig) { + return backendModule().attr("FigureCanvasQTAgg")(fig.pyobj()); +} + +/** + * @param subplotspec A matplotlib subplot spec defined as a 3-digit + * integer. + * @return A new FigureCanvasQT object + */ +Python::Object createPyCanvas(int subplotspec) { + Figure fig{true}; + if (subplotspec > 0) + fig.addSubPlot(subplotspec); + return createPyCanvasFromFigure(std::move(fig)); +} +} // namespace + +/** + * @brief Common constructor code for FigureCanvasQt + * @param cppCanvas A pointer to the FigureCanvasQt object + */ +void initLayout(FigureCanvasQt *cppCanvas) { + cppCanvas->setLayout(new QVBoxLayout()); + QWidget *pyCanvas = Python::extract<QWidget>(cppCanvas->pyobj()); + cppCanvas->layout()->addWidget(pyCanvas); + pyCanvas->setMouseTracking(false); + pyCanvas->installEventFilter(cppCanvas); +} + +/** + * @brief Constructor specifying an axes subplot spec and optional parent. + * An Axes with the given subplot specification is created on construction + * See + * https://matplotlib.org/2.2.3/api/_as_gen/matplotlib.figure.Figure.html?highlight=add_subplot#matplotlib.figure.Figure.add_subplot + * @param subplotspec A matplotlib subplot spec defined as a 3-digit integer + * @param parent The owning parent widget + */ +FigureCanvasQt::FigureCanvasQt(int subplotspec, QWidget *parent) + : QWidget(parent), InstanceHolder(createPyCanvas(subplotspec), "draw"), + m_axes(pyobj().attr("figure").attr("axes")[0]) { + // Cannot use delegating constructor here as InstanceHolder needs to be + // initialized before the axes can be created + initLayout(this); +} + +/** + * @brief Constructor specifying an existing axes object and optional + * parent. + * @param axes An existing axes instance + * @param parent The owning parent widget + */ +FigureCanvasQt::FigureCanvasQt(Figure fig, QWidget *parent) + : QWidget(parent), InstanceHolder(createPyCanvasFromFigure(fig), "draw"), + m_axes(fig.axes(0)) { + initLayout(this); +} + +} // namespace MplCpp +} // namespace Widgets +} // namespace MantidQt diff --git a/qt/widgets/mplcpp/src/Line2D.cpp b/qt/widgets/mplcpp/src/Line2D.cpp new file mode 100644 index 0000000000000000000000000000000000000000..2ae28b6755452a9e3b5d627ff772c9312aabb7c5 --- /dev/null +++ b/qt/widgets/mplcpp/src/Line2D.cpp @@ -0,0 +1,35 @@ +#include "MantidQtWidgets/MplCpp/Line2D.h" + +namespace MantidQt { +namespace Widgets { +namespace MplCpp { + +/** + * @brief Contruct a wrapper around an existing matplotlib.lines.Line2D instance + * This object owns the data that is part of the Line2D instance. It assumes the + * Line2D contains numpy arrays that are simple a view on to the existing data + * that is part of the given Y and X arrays. + * @param obj An existing Line2D instance + * @param xdataOwner The source data for X. It is moved into this object + * @param ydataOwner The source data for Y. It is moved into this object + */ +Line2D::Line2D(Python::Object obj, std::vector<double> xdataOwner, + std::vector<double> ydataOwner) + : Artist(obj), m_xOwner(std::move(xdataOwner)), + m_yOwner(std::move(ydataOwner)) {} + +/** + * The data is being deleted so the the line is removed from the axes + * if it is present + */ +Line2D::~Line2D() { + try { + this->remove(); + } catch (Python::ErrorAlreadySet &) { + // line is not attached to an axes + } +} + +} // namespace MplCpp +} // namespace Widgets +} // namespace MantidQt diff --git a/qt/widgets/mplcpp/src/ScalarMappable.cpp b/qt/widgets/mplcpp/src/ScalarMappable.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f5567194dc83a6857859b27b1514e2a2ba4256f6 --- /dev/null +++ b/qt/widgets/mplcpp/src/ScalarMappable.cpp @@ -0,0 +1,68 @@ +#include "MantidQtWidgets/MplCpp/ScalarMappable.h" +#include "MantidQtWidgets/MplCpp/Colormap.h" + +#include "MantidPythonInterface/core/NDArray.h" + +using Mantid::PythonInterface::NDArray; + +namespace MantidQt { +namespace Widgets { +namespace MplCpp { + +namespace { + +Python::Object createScalarMappable(const NormalizeBase &norm, + const Colormap &cmap) { + return cmModule().attr("ScalarMappable")(norm.pyobj(), cmap.pyobj()); +} +} // namespace + +/** + * @brief Construct a ScalarMappable instance with the given normalization + * type and colormap + * @param norm Instance use to define the mapping from data to [0,1] + * @param cmap A Colormap defning the RGBA values to use for drawing + */ +ScalarMappable::ScalarMappable(const NormalizeBase &norm, const Colormap &cmap) + : Python::InstanceHolder(createScalarMappable(norm, cmap), "to_rgba") { + // The internal array needs to be set to some iterable but apparently + // it is not used: + // https://stackoverflow.com/questions/28801803/matplotlib-scalarmappable-why-need-to-set-array-if-norm-set + pyobj().attr("set_array")(Python::NewRef(Py_BuildValue("()"))); +} + +/** + * @brief Convert a data value to an RGBA value + * @param x The data value within the + * @param alpha The alpha value (default = 1) + * @return A QRgb value corresponding to this data point + */ +QRgb ScalarMappable::toRGBA(double x, double alpha) const { + // Sending the first argument as an iterable gives a numpy array back. + // The final argument (bytes=true) forces the return value to be 0->255 + NDArray rgba{pyobj().attr("to_rgba")(Python::NewRef(Py_BuildValue("(f)", x)), + alpha, true)}; + // sanity check + auto shape = rgba.get_shape(); + if (rgba.get_typecode() == 'B' && rgba.get_nd() == 2 && shape[0] == 1 && + shape[1] == 4) { + auto bytes = reinterpret_cast<std::uint8_t *>(rgba.get_data()); + return qRgba(bytes[0], bytes[1], bytes[2], bytes[3]); + } else { + std::string msg = "Unexpected return type from " + "ScalarMappable.to_rgba. Expected " + "np.array(dtype=B) with shape (1,4) but found " + "np.array(dtype="; + msg.append(std::to_string(rgba.get_typecode())) + .append(") with shape (") + .append(std::to_string(shape[0])) + .append(",") + .append(std::to_string(shape[1])) + .append(")"); + throw std::runtime_error(std::move(msg)); + } +} + +} // namespace MplCpp +} // namespace Widgets +} // namespace MantidQt diff --git a/qt/widgets/mplcpp/test/ArtistTest.h b/qt/widgets/mplcpp/test/ArtistTest.h new file mode 100644 index 0000000000000000000000000000000000000000..f2a6f4a60ca7078242d14f5f2f75c12efd05fa74 --- /dev/null +++ b/qt/widgets/mplcpp/test/ArtistTest.h @@ -0,0 +1,37 @@ +#ifndef MPLCPP_ARTISTTEST_H +#define MPLCPP_ARTISTTEST_H + +#include "MantidQtWidgets/MplCpp/Artist.h" + +#include <cxxtest/TestSuite.h> + +using namespace MantidQt::Widgets::MplCpp; + +class ArtistTest : public CxxTest::TestSuite { +public: + static ArtistTest *createSuite() { return new ArtistTest; } + static void destroySuite(ArtistTest *suite) { delete suite; } + +public: + // ----------------- success tests --------------------- + void testConstructWithArtistIsSuccessful() { + auto artistModule( + Python::NewRef(PyImport_ImportModule("matplotlib.artist"))); + Python::Object pyartist = artistModule.attr("Artist")(); + TS_ASSERT_THROWS_NOTHING(Artist drawer(pyartist)); + } + + void testArtistCallsRemoveOnPyObject() { + auto textModule(Python::NewRef(PyImport_ImportModule("matplotlib.text"))); + Artist label(textModule.attr("Text")()); + TS_ASSERT_THROWS(label.remove(), Python::ErrorAlreadySet); + } + // ----------------- failure tests --------------------- + + void testConstructWithNonArtistThrowsInvalidArgument() { + Python::Object none; + TS_ASSERT_THROWS(Artist artist(none), std::invalid_argument); + } +}; + +#endif // ARTISTTEST_H diff --git a/qt/widgets/mplcpp/test/AxesTest.h b/qt/widgets/mplcpp/test/AxesTest.h new file mode 100644 index 0000000000000000000000000000000000000000..a1dff83573a9cc87012aa85ea2c58c41243a90bb --- /dev/null +++ b/qt/widgets/mplcpp/test/AxesTest.h @@ -0,0 +1,88 @@ +#ifndef MPLCPP_AXESTEST_H +#define MPLCPP_AXESTEST_H + +#include "MantidQtWidgets/MplCpp/Axes.h" +#include <cxxtest/TestSuite.h> + +using namespace MantidQt::Widgets::MplCpp; + +class AxesTest : public CxxTest::TestSuite { +public: + static AxesTest *createSuite() { return new AxesTest; } + static void destroySuite(AxesTest *suite) { delete suite; } + +public: + // ----------------- success tests --------------------- + void testConstructWithPyObjectAxes() { + TS_ASSERT_THROWS_NOTHING(Axes axes(pyAxes())); + } + + void testSetXLabel() { + Axes axes(pyAxes()); + axes.setXLabel("X"); + TS_ASSERT_EQUALS("X", axes.pyobj().attr("get_xlabel")()); + } + + void testSetYLabel() { + Axes axes(pyAxes()); + axes.setYLabel("Y"); + TS_ASSERT_EQUALS("Y", axes.pyobj().attr("get_ylabel")()); + } + + void testSetTitle() { + Axes axes(pyAxes()); + axes.setTitle("Title"); + TS_ASSERT_EQUALS("Title", axes.pyobj().attr("get_title")()); + } + + void testPlotGivesLineWithExpectedData() { + Axes axes(pyAxes()); + std::vector<double> xsrc{1, 2, 3}, ysrc{1, 2, 3}; + auto line = axes.plot(xsrc, ysrc); + auto linex = line.pyobj().attr("get_xdata")(true); + auto liney = line.pyobj().attr("get_ydata")(true); + for (size_t i = 0; i < xsrc.size(); ++i) { + TSM_ASSERT_EQUALS("Mismatch in X data", linex[i], xsrc[i]); + TSM_ASSERT_EQUALS("Mismatch in Y data", liney[i], ysrc[i]); + } + } + + void testPlotWithNoFormatUsesDefault() { + Axes axes(pyAxes()); + auto line = axes.plot({1, 2, 3}, {1, 2, 3}); + TS_ASSERT_EQUALS('b', line.pyobj().attr("get_color")()); + TS_ASSERT_EQUALS('-', line.pyobj().attr("get_linestyle")()); + } + + void testPlotUsesFormatStringIfProvided() { + Axes axes(pyAxes()); + const std::string format{"ro"}; + auto line = axes.plot({1, 2, 3}, {1, 2, 3}, format.c_str()); + TS_ASSERT_EQUALS(format[0], line.pyobj().attr("get_color")()); + TS_ASSERT_EQUALS(format[1], line.pyobj().attr("get_marker")()); + } + + // ----------------- failure tests --------------------- + void testPlotThrowsWithEmptyData() { + Axes axes(pyAxes()); + TS_ASSERT_THROWS(axes.plot({}, {}), std::invalid_argument); + TS_ASSERT_THROWS(axes.plot({1}, {}), std::invalid_argument); + TS_ASSERT_THROWS(axes.plot({}, {1}), std::invalid_argument); + } + +private: + Python::Object pyAxes() { + // An Axes requires a figure and rectangle definition + // to be constructible + const Python::Object figureModule{ + Python::NewRef(PyImport_ImportModule("matplotlib.figure"))}; + const Python::Object figure{figureModule.attr("Figure")()}; + const Python::Object rect{ + Python::NewRef(Py_BuildValue("(iiii)", 0, 0, 1, 1))}; + const Python::Object axesModule{ + Python::NewRef(PyImport_ImportModule("matplotlib.axes"))}; + return axesModule.attr("Axes")(figure, rect); + } +}; + +#endif // MPLCPP_AXESTEST_H diff --git a/qt/widgets/mplcpp/test/CMakeLists.txt b/qt/widgets/mplcpp/test/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..7cfe4e760a8a81e14331f61916d167943a33b217 --- /dev/null +++ b/qt/widgets/mplcpp/test/CMakeLists.txt @@ -0,0 +1,32 @@ +# +# Unit tests for MplCpp library +# +set ( TEST_FILES + ArtistTest.h + AxesTest.h + ColorsTest.h + ColormapTest.h + FigureTest.h + FigureCanvasQtTest.h + Line2DTest.h + ScalarMappableTest.h + SipTest.h +) + +set (CXXTEST_EXTRA_HEADER_INCLUDE ${CMAKE_CURRENT_LIST_DIR}/MplCppTestInitialization.h) + +mtd_add_qt_tests (TARGET_NAME MantidQtWidgetsMplCppTest + QT_VERSION 5 + INCLUDE_DIRS + ${CMAKE_CURRENT_LIST_DIR}/../inc + SRC + ${TEST_FILES} + LINK_LIBS + PythonInterfaceCore + ${Boost_LIBRARIES} + ${PYTHON_LIBRARIES} + MTD_QT_LINK_LIBS + MantidQtWidgetsMplCpp + PARENT_DEPENDENCIES + GUITests +) diff --git a/qt/widgets/mplcpp/test/ColormapTest.h b/qt/widgets/mplcpp/test/ColormapTest.h new file mode 100644 index 0000000000000000000000000000000000000000..043c550d863abf25bf307b5ca19dca2e94ccc6b1 --- /dev/null +++ b/qt/widgets/mplcpp/test/ColormapTest.h @@ -0,0 +1,40 @@ +#ifndef MPLCPP_COLORMAPTEST_H +#define MPLCPP_COLORMAPTEST_H + +#include "MantidPythonInterface/core/ErrorHandling.h" +#include "MantidQtWidgets/MplCpp/Colormap.h" + +#include <cxxtest/TestSuite.h> + +using Mantid::PythonInterface::PythonRuntimeError; +using MantidQt::Widgets::MplCpp::Colormap; +using MantidQt::Widgets::MplCpp::Python::Object; +using MantidQt::Widgets::MplCpp::getCMap; + +class ColormapTest : public CxxTest::TestSuite { +public: + static ColormapTest *createSuite() { return new ColormapTest; } + static void destroySuite(ColormapTest *suite) { delete suite; } + +public: + // ----------------------- Success tests ------------------------ + void testgetCMapKnownCMapIsSuccesful() { + TS_ASSERT_THROWS_NOTHING(getCMap("jet")); + } + + void testConstructionColorMapInstanceIsSuccessful() { + auto jet = getCMap("jet"); + TS_ASSERT_EQUALS("jet", jet.pyobj().attr("name")); + } + + // ----------------------- Failure tests ------------------------ + void testgetCMapWithUnknownCMapThrowsException() { + TS_ASSERT_THROWS(getCMap("AnUnknownName"), PythonRuntimeError); + } + + void testConstructionWithNonColorMapObjectThrows() { + TS_ASSERT_THROWS(Colormap cmap(Object{}), std::invalid_argument); + } +}; + +#endif // MPLCPP_COLORMAPTEST_H diff --git a/qt/widgets/mplcpp/test/ColorsTest.h b/qt/widgets/mplcpp/test/ColorsTest.h new file mode 100644 index 0000000000000000000000000000000000000000..4d266b9457bf07c933da83745afb3d6cc6c445c1 --- /dev/null +++ b/qt/widgets/mplcpp/test/ColorsTest.h @@ -0,0 +1,40 @@ +#ifndef MPLCPP_COLORSTEST_H +#define MPLCPP_COLORSTEST_H + +#include <cxxtest/TestSuite.h> + +#include "MantidQtWidgets/MplCpp/Colors.h" + +using MantidQt::Widgets::MplCpp::Normalize; +using MantidQt::Widgets::MplCpp::PowerNorm; +using MantidQt::Widgets::MplCpp::SymLogNorm; + +class ColorsTest : public CxxTest::TestSuite { +public: + static ColorsTest *createSuite() { return new ColorsTest; } + static void destroySuite(ColorsTest *suite) { delete suite; } + +public: + void testNormalize() { + Normalize norm(-1, 1); + TS_ASSERT_EQUALS(-1, norm.pyobj().attr("vmin")); + TS_ASSERT_EQUALS(1, norm.pyobj().attr("vmax")); + } + + void testSymLogNorm() { + SymLogNorm norm(0.001, 2, -1, 1); + // No public api method for access linscale + TS_ASSERT_EQUALS(0.001, norm.pyobj().attr("linthresh")); + TS_ASSERT_EQUALS(-1, norm.pyobj().attr("vmin")); + TS_ASSERT_EQUALS(1, norm.pyobj().attr("vmax")); + } + + void testPowerNorm() { + PowerNorm norm(2, -1, 1); + TS_ASSERT_EQUALS(2, norm.pyobj().attr("gamma")); + TS_ASSERT_EQUALS(-1, norm.pyobj().attr("vmin")); + TS_ASSERT_EQUALS(1, norm.pyobj().attr("vmax")); + } +}; + +#endif // MPLCPP_COLORSTEST_H diff --git a/qt/widgets/mplcpp/test/FigureCanvasQtTest.h b/qt/widgets/mplcpp/test/FigureCanvasQtTest.h new file mode 100644 index 0000000000000000000000000000000000000000..1dcc3796308a844726219d0ea53b42bae1d51021 --- /dev/null +++ b/qt/widgets/mplcpp/test/FigureCanvasQtTest.h @@ -0,0 +1,38 @@ +#ifndef MPLCPP_FIGURECANVASQTTEST_H +#define MPLCPP_FIGURECANVASQTTEST_H + +#include <cxxtest/TestSuite.h> + +#include "MantidQtWidgets/MplCpp/Figure.h" +#include "MantidQtWidgets/MplCpp/FigureCanvasQt.h" + +using MantidQt::Widgets::MplCpp::Figure; +using MantidQt::Widgets::MplCpp::FigureCanvasQt; + +class FigureCanvasQtTest : public CxxTest::TestSuite { +public: + static FigureCanvasQtTest *createSuite() { return new FigureCanvasQtTest; } + static void destroySuite(FigureCanvasQtTest *suite) { delete suite; } + +public: + void testConstructionYieldsExpectedSubplot() { + FigureCanvasQt canvas{111}; + auto geometry = canvas.gca().pyobj().attr("get_geometry")(); + TS_ASSERT_EQUALS(1, geometry[0]); + TS_ASSERT_EQUALS(1, geometry[1]); + TS_ASSERT_EQUALS(1, geometry[2]); + } + + void testConstructionCapturesGivenAxesObject() { + using namespace MantidQt::Widgets::MplCpp; + Figure fig; + fig.addSubPlot(221); + FigureCanvasQt canvas{std::move(fig)}; + auto geometry = canvas.gca().pyobj().attr("get_geometry")(); + TS_ASSERT_EQUALS(2, geometry[0]); + TS_ASSERT_EQUALS(2, geometry[1]); + TS_ASSERT_EQUALS(1, geometry[2]); + } +}; + +#endif // MPLCPP_FIGURECANVASQTTEST_H diff --git a/qt/widgets/mplcpp/test/FigureTest.h b/qt/widgets/mplcpp/test/FigureTest.h new file mode 100644 index 0000000000000000000000000000000000000000..636289799ea6cf8462980c0029f553c441e10f1e --- /dev/null +++ b/qt/widgets/mplcpp/test/FigureTest.h @@ -0,0 +1,37 @@ +#ifndef MPLCPP_FIGURETEST_H +#define MPLCPP_FIGURETEST_H + +#include "MantidQtWidgets/MplCpp/Figure.h" + +#include <cxxtest/TestSuite.h> + +using MantidQt::Widgets::MplCpp::Figure; + +class FigureTest : public CxxTest::TestSuite { +public: + static FigureTest *createSuite() { return new FigureTest; } + static void destroySuite(FigureTest *suite) { delete suite; } + +public: + void testDefaultFigureHasTightLayout() { + Figure fig; + TS_ASSERT_EQUALS(true, fig.pyobj().attr("get_tight_layout")()); + } + + void testConstructFigureWithNoTightLayout() { + Figure fig{false}; + TS_ASSERT_EQUALS(false, fig.pyobj().attr("get_tight_layout")()); + } + + void testAddAxes() { + Figure fig{false}; + TS_ASSERT_THROWS_NOTHING(fig.addAxes(0.1, 0.1, 0.9, 0.9)); + } + + void testSubPlot() { + Figure fig{false}; + TS_ASSERT_THROWS_NOTHING(fig.addSubPlot(111)); + } +}; + +#endif // FigureTEST_H diff --git a/qt/widgets/mplcpp/test/Line2DTest.h b/qt/widgets/mplcpp/test/Line2DTest.h new file mode 100644 index 0000000000000000000000000000000000000000..3d44efeff3b53a2e3c6c0675afee1475a061226e --- /dev/null +++ b/qt/widgets/mplcpp/test/Line2DTest.h @@ -0,0 +1,35 @@ +#ifndef MPLCPP_LINE2DTEST_H +#define MPLCPP_LINE2DTEST_H + +#include "MantidQtWidgets/MplCpp/Line2D.h" +#include <cxxtest/TestSuite.h> + +using namespace MantidQt::Widgets::MplCpp; + +class Line2DTest : public CxxTest::TestSuite { +public: + static Line2DTest *createSuite() { return new Line2DTest; } + static void destroySuite(Line2DTest *suite) { delete suite; } + + // ---------------------- success tests -------------------- + void testConstructionRequiresMplLine2DObject() { + TS_ASSERT_THROWS_NOTHING(Line2D line(pyLine2D(), {}, {})); + } + + // ---------------------- failure tests -------------------- + void testConstructionWithNonLine2DObjectThrowsInvalidArgument() { + Python::Object obj{Python::NewRef(Py_BuildValue("(i)", 1))}; + TS_ASSERT_THROWS(Line2D line(obj, {}, {}), std::invalid_argument); + } + +private: + Python::Object pyLine2D() { + // A Line2D requires x and y data sequences + const Python::Object data{Python::NewRef(Py_BuildValue("(f, f)", 0., 1.))}; + const Python::Object linesModule{ + Python::NewRef(PyImport_ImportModule("matplotlib.lines"))}; + return linesModule.attr("Line2D")(data, data); + } +}; + +#endif // MPLCPP_LINE2DTEST_H diff --git a/qt/widgets/mplcpp/test/MplCppTestInitialization.h b/qt/widgets/mplcpp/test/MplCppTestInitialization.h new file mode 100644 index 0000000000000000000000000000000000000000..98dd2fd8fe4a87760387c3bc16cb995337a1918e --- /dev/null +++ b/qt/widgets/mplcpp/test/MplCppTestInitialization.h @@ -0,0 +1,71 @@ +#ifndef MPLCPPTESTGLOBALINITIALIZATION_H +#define MPLCPPTESTGLOBALINITIALIZATION_H + +#include "cxxtest/GlobalFixture.h" + +#include "MantidPythonInterface/core/NDArray.h" +#include "MantidPythonInterface/core/VersionCompat.h" +#include <QApplication> + +/** + * PythonInterpreter + * + * Uses setUpWorld/tearDownWorld to initialize & finalize + * Python + */ +class PythonInterpreter : CxxTest::GlobalFixture { +public: + bool setUpWorld() override { + Py_Initialize(); + Mantid::PythonInterface::importNumpy(); + return Py_IsInitialized(); + } + + bool tearDown() override { + // Some test methods may leave the Python error handler with an error + // set that confuse other tests when the executable is run as a whole + // Clear the errors after each suite method is run + PyErr_Clear(); + return CxxTest::GlobalFixture::tearDown(); + } + + bool tearDownWorld() override { + Py_Finalize(); + return true; + } +}; + +/** + * QApplication + * + * Uses setUpWorld/tearDownWorld to initialize & finalize + * QApplication object + */ +class QApplicationHolder : CxxTest::GlobalFixture { +public: + bool setUpWorld() override { + int argc(0); + char **argv = {}; + m_app = new QApplication(argc, argv); + return true; + } + + bool tearDownWorld() override { + delete m_app; + return true; + } + +private: + QApplication *m_app; +}; + +//------------------------------------------------------------------------------ +// Static definitions +// +// We rely on cxxtest only including this file once so that the following +// statements do not cause multiple-definition errors. +//------------------------------------------------------------------------------ +static PythonInterpreter PYTHON_INTERPRETER; +static QApplicationHolder MAIN_QAPPLICATION; + +#endif // MPLCPPTESTGLOBALINITIALIZATION_H diff --git a/qt/widgets/mplcpp/test/ScalarMappableTest.h b/qt/widgets/mplcpp/test/ScalarMappableTest.h new file mode 100644 index 0000000000000000000000000000000000000000..8aa8bb59bf16f043e5b0d57ab5749b4344f719fe --- /dev/null +++ b/qt/widgets/mplcpp/test/ScalarMappableTest.h @@ -0,0 +1,47 @@ +#ifndef MPLCPP_SCALARMAPPABLETEST_H +#define MPLCPP_SCALARMAPPABLETEST_H + +#include "MantidQtWidgets/MplCpp/Colormap.h" +#include "MantidQtWidgets/MplCpp/Colors.h" +#include "MantidQtWidgets/MplCpp/ScalarMappable.h" + +#include <QRgb> + +#include <cxxtest/TestSuite.h> + +using MantidQt::Widgets::MplCpp::Normalize; +using MantidQt::Widgets::MplCpp::ScalarMappable; +using MantidQt::Widgets::MplCpp::getCMap; + +class ScalarMappableTest : public CxxTest::TestSuite { +public: + static ScalarMappableTest *createSuite() { return new ScalarMappableTest; } + static void destroySuite(ScalarMappableTest *suite) { delete suite; } + +public: + // ----------------------- Success tests ------------------------ + void testConstructionWithValidCMapAndNormalize() { + TS_ASSERT_THROWS_NOTHING( + ScalarMappable mappable(Normalize(-1, 1), getCMap("jet"))); + } + + void testtoRGBAWithNoAlphaGivesDefault() { + ScalarMappable mappable(Normalize(-1, 1), getCMap("jet")); + auto rgba = mappable.toRGBA(0.0); + TS_ASSERT_EQUALS(124, qRed(rgba)); + TS_ASSERT_EQUALS(255, qGreen(rgba)); + TS_ASSERT_EQUALS(121, qBlue(rgba)); + TS_ASSERT_EQUALS(255, qAlpha(rgba)); + } + + void testtoRGBAWithAlpha() { + ScalarMappable mappable(Normalize(-1, 1), getCMap("jet")); + auto rgba = mappable.toRGBA(0.0, 0.5); + TS_ASSERT_EQUALS(124, qRed(rgba)); + TS_ASSERT_EQUALS(255, qGreen(rgba)); + TS_ASSERT_EQUALS(121, qBlue(rgba)); + TS_ASSERT_EQUALS(127, qAlpha(rgba)); + } +}; + +#endif // MPLCPP_SCALARMAPPABLETEST_H diff --git a/qt/widgets/mplcpp/test/SipTest.h b/qt/widgets/mplcpp/test/SipTest.h new file mode 100644 index 0000000000000000000000000000000000000000..7e72b106d6860d5a533cd050c43dc9be3cc1166f --- /dev/null +++ b/qt/widgets/mplcpp/test/SipTest.h @@ -0,0 +1,42 @@ +#ifndef MPLCPP_SIPTEST_H +#define MPLCPP_SIPTEST_H + +#include <cxxtest/TestSuite.h> + +#include "MantidQtWidgets/MplCpp/BackendQt.h" +#include "MantidQtWidgets/MplCpp/Python/Sip.h" + +#include <QWidget> + +using MantidQt::Widgets::MplCpp::backendModule; +namespace Python = MantidQt::Widgets::MplCpp::Python; + +class SipTest : public CxxTest::TestSuite { +public: + static SipTest *createSuite() { return new SipTest; } + static void destroySuite(SipTest *suite) { delete suite; } + +public: + // ----------------- success tests --------------------- + void testExtractWithSipWrappedTypeSucceeds() { + Python::Object mplBackend{backendModule()}; + Python::Object fig{ + Python::NewRef(PyImport_ImportModule("matplotlib.figure")) + .attr("Figure")()}; + Python::Object pyCanvas{mplBackend.attr("FigureCanvasQT")(fig)}; + QWidget *w{nullptr}; + TS_ASSERT_THROWS_NOTHING(w = Python::extract<QWidget>(pyCanvas)); + TS_ASSERT(w); + } + + // ----------------- failure tests --------------------- + + void testExtractWithNonSipTypeThrowsException() { + const Python::Object nonSipType{ + Python::NewRef(Py_BuildValue("(ii)", 1, 2))}; + struct Foo; + TS_ASSERT_THROWS(Python::extract<Foo>(nonSipType), std::runtime_error); + } +}; + +#endif // MPLCPP_SIPTEST_H