Defining a new Form to implement a data structure¶
The objective is to add a Form concept (e.g. gnomonNewForm
) to the gnomoncore layer, but to do it in such way that the object could be seen from different perspectives, using a Bridge design pattern. Then the idea is to use an implementation of this concept using a Python library.
Define the Form abstraction on the C++ side¶
gnomon
└───src
└───gnomoncore
└───gnomonForm
└───gnomonNewForm
│ CMakeLists.txt
│ gnomonNewForm
│ gnomonNewForm.h
│ gnomonAbstractNewFormData
│ gnomonAbstractNewFormData.h
│ gnomonAbstractNewFormData.cpp
Define the abstraction for the data of the Form¶
The starting point is the definition of an abstract class that will be implemented by one or several plugins (C++ or Python)
Create an empty header file that will contain an abstract class to define the abstraction of the data contained in the Form structure:
gnomonAbstractNewFormData.h
#pragma once
#include <gnomonCoreExport.h>
// gnomonAbstractNewFormData.h ends here
Abstract data class definition¶
Define the abstract class to set the members and methods of the data structure. All the class methods should be defined as pure virtual so that deriving classes will have to implement these functions. Start with a constructor and a destructor, as well as a
clone
method that will be useful for the bridge construct.
// ///////////////////////////////////////////////////////////////////
// gnomonAbstractNewFormData
// ///////////////////////////////////////////////////////////////////
class gnomonAbstractNewFormData
{
public:
gnomonAbstractNewFormData(void) = default;
virtual ~gnomonAbstractNewFormData(void) {};
virtual gnomonAbstractNewFormData* clone(void) const = 0;
};
Define the methods required by the
gnomonAbstractForm
class. ote that at this stage, they are pure virtual methods, and do not override any method. However it will be the case in the bridge class, that will inheritgnomonAbstractForm
.
// ///////////////////////////////////////////////////////////////////
// Metadata
// ///////////////////////////////////////////////////////////////////
public:
virtual QMap<QString,QString> metadata(void) const = 0;
virtual QString dataName(void) const = 0;
Then define the specific API of the class. Depending on the concept, you may have few or many methods to declare. For instance we can imagine our
newForm
to be composed ofelements
with unique ids andfloat
numerical properties, accessible through such an API:
public:
virtual QList<long> elementIds(void) const = 0;
virtual long elementCount(void) const = 0;
virtual QList<QString> elementPropertyNames(void) const = 0;
virtual const QMap<long, float>& elementProperty(const QString& propertyName) const = 0;
virtual QMap<long, float>& elementProperty(const QString& propertyName) = 0;
In case the class API relies on third-party libraries, don’t forger to include the corresponding headers. In this case:
#include <QtCore>
Use forward-declaration for symbols that will be defined elsewhere, to ensure that compilation goes well.
Add the dTK macros for the visibility of the abstraction¶
Include the tools to make the concept visible for the application and add the export macro to the definition of the class…
#include <QtCore>
#include <gnomonCoreExport.h>
#include <dtkCore>
#include "gnomonCore/gnomonCorePlugin.h"
// ///////////////////////////////////////////////////////////////////
// gnomonAbstractNewFormData
// ///////////////////////////////////////////////////////////////////
class GNOMONCORE_EXPORT gnomonAbstractNewFormData
…and the declaration/namespace macros at the end of the file. Be careful to declare the concept starting with a lowercase letter:
newFormData
.
// ///////////////////////////////////////////////////////////////////
// Give the concept the plugin machinery
// ///////////////////////////////////////////////////////////////////
DTK_DECLARE_OBJECT (gnomonAbstractNewFormData *)
DTK_DECLARE_PLUGIN (gnomonAbstractNewFormData, GNOMONCORE_EXPORT)
GNOMON_DECLARE_PLUGIN_FACTORY(gnomonAbstractNewFormData, GNOMONCORE_EXPORT, newFormData)
//
// gnomonAbstractNewFormData.h ends here
Write corresponding .cpp
file¶
Create the corresponding
gnomonAbstractNewFormData.cpp
file that will implement the namespace registration macro that will enable its registration to the manager of the layer. Again, be careful to have a lowercase concept.
#include "gnomonCore.h"
#include "gnomonAbstractNewFormData.h"
#include "gnomonCore.h"
// /////////////////////////////////////////////////////////////////
// Register to gnomonCore layer
// /////////////////////////////////////////////////////////////////
namespace gnomonCore {
GNOMON_DEFINE_CONCEPT(gnomonAbstractNewFormData, newFormData, gnomonCore);
}
//
// gnomonNewFormData.cpp ends here
Create an include file without extension
gnomonAbstractNewFormData
to be able to include the class (cpp guidelines)
#include "gnomonAbstractCellImageData.h"
Define the concrete Form class as a data bridge¶
We will now create an instantiable class that will basically wrap some or all of the abstraction methods to produce an interface for some given use. Note that one abstraction may have several bridges, exposing different aspects of the class depending on the final use.
Write the Form bridge header¶
Create a new empty header file
gnomonNewForm.h
. The class is no longer abstract but will inheriting thegnomonAbstractForm
(abstract) class. This concrete class only has one member which is a pointer on an implementation of the abstract data class:
#pragma once
#include "gnomonAbstractNewFormData.h"
#include "gnomonForm/gnomonAbstractForm.h"
// ///////////////////////////////////////////////////////////////////
// gnomonNewForm
// ///////////////////////////////////////////////////////////////////
class GNOMONCORE_EXPORT gnomonNewForm : public gnomonAbstractForm
{
protected:
gnomonAbstractNewFormData *m_data;
};
//
// gnomonImage.h ends here
The class being no longer abstract, all its methods have to be implemented. Since their implementation will be very short, we implemented them directly in the header file, starting with the constructor and destructor, that simply create/clone and delete the private data member.
public:
explicit gnomonNewForm(void) : m_data(nullptr) {}
explicit gnomonNewForm(gnomonAbstractNewFormData *data) : m_data(data) {}
gnomonNewForm(const gnomonNewForm& other) : m_data(other.m_data->clone()) {}
gnomonAbstractForm *clone(void) { return new gnomonNewForm(*this); };
~gnomonNewForm(void) { if (m_data) { delete m_data; } m_data = nullptr; }
We also add on overload of the
=
operator so that the setting an instance of the bridge class with another one goes well memory-wise (and copies the underlying data).
public:
gnomonNewForm& operator = (const gnomonNewForm& other)
{
if (m_data != other.m_data) {
if (m_data != nullptr) {
delete m_data;
}
if(other.m_data != nullptr) {
m_data = other.m_data->clone();
} else {
m_data = nullptr;
}
}
return *this;
}
We also add getter and setters for the data member, making sure that the previous data object is deleted.
public:
const gnomonAbstractNewFormData *data(void) const { return m_data; }
gnomonAbstractNewFormData *data(void) { return m_data; }
void setData(gnomonAbstractNewFormData* data)
{
if (m_data) {
delete m_data;
}
m_data = data;
}
The
gnomonAbstractForm
API requires aname
method. Here, we declare a (fixed) name for the Form instances, correponding to the class name.
public:
QString name(void) const override { return "gnomonNewForm"; }
Then all the other class methods (including the ones overriding the
gnomonAbstractForm
methods) will simply be calls to the same method of the abstract data class, passing the arguments when necessary.
public:
QMap<QString,QString> metadata(void) const override { return m_data->metadata(); }
QString dataName(void) const override { return m_data->dataName(); }
public:
QList<long> elementIds(void) const { return m_data->elementIds(); };
long elementCount(void) const { return m_data->elementCount(); };
QList<QString> elementPropertyNames(void) const { return m_data->elementPropertyNames(); };
const QMap<long, float>& elementProperty(const QString& propertyName) const { return m_data->elementProperty(propertyName); };
QMap<long, float>& elementProperty(const QString& propertyName) { return m_data->elementProperty(propertyName); };
Declare the
gnomonTimeSeries
corresponding to the Form directly in the header:
#include "gnomonAbstractNewFormData.h"
#include "gnomonForm/gnomonAbstractForm.h"
#include "gnomonForm/gnomonTimeSeries.h"
// ///////////////////////////////////////////////////////////////////
typedef gnomonTimeSeries<gnomonNewForm> gnomonNewFormSeries;
Q_DECLARE_METATYPE(gnomonNewFormSeries *)
Add the
dtk
machinery and macros so that the export runs smoothly.
#include <gnomonCoreExport.h>
#include "gnomonAbstractNewFormData.h"
#include "gnomonForm/gnomonAbstractForm.h"
#include "gnomonForm/gnomonTimeSeries.h"
#include <QtCore>
class GNOMONCORE_EXPORT gnomonNewForm
// ///////////////////////////////////////////////////////////////////
DTK_DECLARE_OBJECT(gnomonCellImage *)
//
// gnomonNewForm.h ends here
Edit the compilation files to include the new abstraction¶
In the
CMakeLists.txt
of thegnomonNewForm/
directory, add the headers and sources using the gnomon-specific macros (defined ingnomon/cmake/gnomonSubdirectoryAddFiles.cmake
)
## #################################################################
## Sources
## #################################################################
ADD_GNOMON_SUBDIRECTORY_HEADERS(
gnomonAbstractNewFormData
gnomonAbstractNewFormData.h
gnomonNewForm
gnomonNewForm.h)
ADD_GNOMON_SUBDIRECTORY_SOURCES(
gnomonAbstractNewFormData.cpp)
######################################################################
### CMakeLists.txt ends here
In the
CMakeLists.txt
of thegnomonForm/
directory, add thegnomonNewForm
subdirectory, and also add the (exported)_HEADERS
and_SOURCES
in the respective sections.
## #################################################################
## Inputs
## #################################################################
...
add_subdirectory(gnomonNewForm)
...
## #################################################################
## Sources
## #################################################################
ADD_GNOMON_HEADERS(
...
${${PROJECT_NAME}_FORM_NEWFORM_HEADERS}
...)
ADD_GNOMON_SOURCES(
...
${${PROJECT_NAME}_FORM_NEWFORM_SOURCES}
...)
######################################################################
### CMakeLists.txt ends here
Include the headers at the top-level of the gnomonCore layer¶
In the file
src/gnomonCore/gnomonCore
, include the newly defined headers, so that they are accessible when we do a#include <gnomonCore>
outside the layer.
...
#include "gnomonForm/gnomonNewForm/gnomonAbstractNewFormData.h"
#include "gnomonForm/gnomonNewForm/gnomonNewForm.h"
...
Wrap the abstraction to make it available in Python¶
In the SWIG input file
wrp/gnomonCore/gnomonCore.i
first include the headers for the Form class and the abstract data class:
...
#include <gnomonCore/gnomonForm/gnomonNewForm/gnomonAbstractNewFormData.h>
#include <gnomonCore/gnomonForm/gnomonNewForm/gnomonNewForm.h>
...
Use the
WRAP_GNOMONCORE_FORM_SERIES
SWIG macro to declare the typemaps that convertgnomonTimeSeries
to / form a Python dictionary of Forms indexed by time:
WRAP_GNOMONCORE_FORM_SERIES(NewForm)
At the bottom of the file, after the typemaps and before the templates, include the headers for SWIG.
%include <gnomonCore/gnomonForm/gnomonNewForm/gnomonAbstractNewFormData.h>
%include <gnomonCore/gnomonForm/gnomonNewForm/gnomonNewForm.h>
Here, we can extend the wrapped class with a
__repr__
method that will allow it to be more nicely diplayed in Python.
%extend gnomonNewForm {
const char* __repr__()
{
static std::string s;
auto&& newForm = $self;
QString str("<gnomoncore.gnomonNewForm");
str += QString(" with %1 element(s)").arg(newForm->elementCount());
str += QString(" at 0x%1>").arg((quintptr)newForm, 12, 16, QChar('0'));
s = str.toStdString();
return s.data();
}
}
Compile gnomon and install it with the new (wrapped) abstraction¶
Run the compilation with the install option
cd build
make -j8 install
In some cases, it might be necessary to clean the existing compiled wrappers:
rm -rf wrp/
rm -rf $CONDA_PREFIX/wrp/gnomon*
If all goes well, you should be able to run in
ipython
:
from gnomon.core import gnomonAbstractNewFormData, newFormData_pluginFactory
Provide a Python plugin that implements the form abstraction¶
Within an already existing plugin package, we will implement a Python class inheriting the abstract data class gnomonAbstractNewFormData
that will override all its pure virtual methods.
The concrete class will typically wrap an existing Python data structure (e.g. MyStructure
) to adapt it to the API specified by the abstract C++ class. The idea is then that the class has a data structure member (e.g. data
) that will be used to fill in the different methods.
Implement the Python class¶
Create a new module
newFormDataMyStructure.py
that defines a class inheriting our Form data abstraction
gnomon-package-pkgname
└───src
└───plugin_name
└───form
│ │ __init__.py
│ │ newFormDataMyStructure.py
│
│ __init__.py
from gnomon.core import gnomonAbstractNewFormData
class newFormDataMyStructure(gnomonAbstractNewFormData):
def __init__(self):
super().__init__()
Add a (hidden)
_data
member (that should be of typeMyStructure
) containing the actual data structure representing the Form. We also add a specificset_data
method that makes a copy of the provided data structure to keep it as the_data
member.
from copy import deepcopy
from my_module import MyStructure
def __init__(self):
super().__init__()
self._data = MyStructure()
def __del__(self):
del self._data
def set_data(self, data: MyStructure):
self._data = deepcopy(data)
To make sure that Forms can be manipulated safely memorywise on the C++ side, we need to implement the
clone
method that duplicates the underlying data structure:
def clone(self):
clone = newFormDataMyStructure()
clone.set_data(self._data)
clone.__disown__()
return clone
To enable the interoperability of the form data class we also define a method to instantiate our Python class from an existing instance of the Form (potentially implemented by a different form data plugin)
def from_gnomonNewForm(self, form):
form_data = form.data()
if isinstance(data, newFormDataMyStructure):
self.set_data(form_data._data)
else:
self._data = MyStructure()
# fill in the structure using the Form API
for eid in data.elementIds():
self._data.add_element(eid)
...
return self
Then, we implement the virtual methods of the abstract class using the methods / attributes of the underlying data structure. First the methods needed by
gnomonAbstractForm
:
def metadata(self):
metadata = {}
metadata['Number of elements'] = str(self._data.nb_elements())
return metadata
def dataName(self):
return "my_module.MyStructure"
…and the rest of the Form API
def elementIds(self):
return [int(eid) for eid in self._data.elements()]
def elementCount(self):
return self._data.nb_elements()
def elementPropertyNames(self):
...
def elementProperty(self, propertyName):
...
Finally, we add the
gnomonDecorator
plugin that implements all the necessary methods to register the plugin to the platform. Since the abstraction (and its plugin factory) is registered to thegnomonCore
namespace, we need to specify it explicitly to the decorator.
import gnomon.core
from gnomon.core import gnomonAbstractNewFormData
from gnomon.utils import corePlugin
@corePlugin(version="0.1.0", coreversion="0.81.1")
class newFormDataMyStructure(gnomonAbstractNewFormData):
Install the newly defined plugin¶
Install the package again to update the entry points
python setup.py develop
We can now check that the registration has been successful by trying to instantiate our class directly through the plugin factory. To do so, you can check it in a python interpreter:
from gnomon.core import newFormData_pluginFactory
from gnomon.utils.gnomonPlugin import load_plugin_group
load_plugin_group("newFormData")
form_data = newFormData_pluginFactory().create("newFormDataMyStructure")
assert(form_data is not None)
Add a test of the Form data plugin¶
In the
test/
folder at the root of the plugin package, add atest_newFormData.py
file containing a test class inheritingunittest.TestCase
The
setUp
method of the class should instantiate:A form data plugin from the
newFormData_pluginFactory
A
gnomonNewForm
form, which will receive the data plugin usingsetData
A data structure
MyStructure
that will be passed to the data plugin
import unittest
import gnomon.core
from gnomon.core import gnomonNewForm, newFormData_pluginFactory
from gnomon.utils import load_plugin_group
from my_module import example_data_structure
load_plugin_group("newFormData")
class TestGnomonNewForm(unittest.TestCase):
"""Tests the gnomonNewForm class.
"""
def setUp(self):
self.data = example_data_structure()
self.form = gnomonNewForm()
self.form_data = newFormData_pluginFactory().create("newFormDataMyStructure")
self.form_data.set_data(self.data)
self.form.setData(self.form_data)
The
tearDown
should leave the objects created in the setup to be destroyed
def tearDown(self):
self.form.this.disown()
self.form_data.this.disown()
Then, add a test method for each overriden method of the Form API, e.g.:
def test_gnomonewForm_elementIds(self):
assert np.all([eid in self.data.elements() for eid in self.form.elementIds])
Add the Form decorators in the gnomon.utils module¶
To facilitate the writing of algorithm plugins, gnomon
provides Python decorators to declare Form types of inputs and outputs, along with a preferred data_plugin
. To make our new Form compatible with this system, we need to include it to the existing decorators.
Add a Python file named
new_data_decorator.py
in thegnomonDecorator
module ofgnomon.utils
gnomon
└───python
└───gnomon.utils
└───gnomonDecorator
│ __init__.py
│ ...
│ new_data_decorator.py
│ ...
The module should declare an input and an output decorator as follows:
import gnomon.core
from gnomon.core import gnomonNewForm
from gnomon.utils.gnomonPlugin import load_plugin_group
from .form_series import buildFormSeries, formDictFromSeries
load_plugin_group("newFormData")
default_plugin = "gnomonNewFormDataSpatialImage"
default_setter = "set_image"
default_attr = "_image"
form_class = gnomonNewForm
form_data_factory = gnomoncore.newFormData_pluginFactory()
from_form_method = "from_gnomonNewForm"
def _gnomonNewFormInput(cls, attr, method, setter_method, data_plugin, data_setter, data_attr):
def func(self, update=True):
update = update or not hasattr(self, "_in_newForm")
if update:
form_dict, data_dict = buildFormSeries(form_dict=getattr(self, attr),
form_class=form_class,
form_data_factory=form_data_factory,
data_plugin=data_plugin,
data_setter=data_setter)
self._in_newForm = form_dict
self._in_newForm_data = data_dict
return self._in_newForm
setattr(cls, method, func)
def setter_func(self, newForm):
self._in_newForm = newForm
setattr(self, attr, {})
if self._in_newForm is not None:
newForm_dict = formDictFromSeries(form=self._in_newForm,
form_data_factory=form_data_factory,
from_form_method=from_form_method,
data_plugin=data_plugin,
data_attr=data_attr)
setattr(self, attr, newForm_dict)
if hasattr(self,"refresh_parameters"):
self.refresh_parameters()
setattr(cls, setter_method, setter_func)
return cls
def gnomonNewFormInput(cls=None, attr=None, method='input', setter_method='setInput', data_plugin=default_plugin, data_setter=default_setter, data_attr=default_attr):
if cls is not None:
return _gnomonNewFormInput(cls, attr, data_plugin=data_plugin, data_setter=data_setter, data_attr=data_attr)
else:
def wrapper(cls):
return _gnomonNewFormInput(cls, attr, method, setter_method, data_plugin=data_plugin, data_setter=data_setter, data_attr=data_attr)
return wrapper
def _gnomonNewFormOutput(cls, attr, method, data_plugin, data_setter):
def func(self, update=True):
update = update or not hasattr(self, "_out_newForm")
if update:
form_dict, data_dict = buildFormSeries(form_dict=getattr(self, attr),
form_class=form_class,
form_data_factory=form_data_factory,
data_plugin=data_plugin,
data_setter=data_setter)
self._out_newForm = form_dict
self._out_newForm_data = data_dict
return self._out_newForm
setattr(cls, method, func)
return cls
def gnomonNewFormOutput(cls=None, attr=None, method='output', data_plugin=default_plugin, data_setter=default_setter):
if cls is not None:
return _gnomonNewFormOutput(cls, attr, data_plugin=data_plugin, data_setter=data_setter)
else:
def wrapper(cls):
return _gnomonNewFormOutput(cls, attr, method, data_plugin=data_plugin, data_setter=data_setter)
return wrapper
Add the new decorators to the
__init__.py
of thegnomonDecorator
module
...
from .new_form_decorator import gnomoNewFormInput, gnomonewFormOutput
...