Plugin Development¶
Force BDSS is extensible through plugins. A plugin can be (and generally is) provided as a separate python package that makes available some new classes. Force BDSS will find these classes from the plugin at startup.
A single Plugin can provide one or more of the following entities:
MCO
, DataSources
, NotificationListeners
, UIHooks
. It can optionally
provide DataViews
to be used by the force_wfmanager
GUI.
An example plugin implementation is available at:
https://github.com/force-h2020/force-bdss-plugin-enthought-example
To implement a new plugin, you must define at least four classes:
The
Plugin
class itself.One of the entities you want to implement: a
DataSource
,NotificationListener
,MCO
, orUIHook
.A
Factory
class for the entity above: it is responsible for creating the specific entity, for example, aDataSource
A
Model
class which contains configuration options for the entity. For example, it can contain login and password information so that its data source knows how to connect to a service. TheModel
is also shown visually in theforce_wfmanager
UI, so some visual aspects need to be configured as well.
The plugin is made available by having it defined in the setup.py
file
entry_points
section, under the namespace force.bdss.extensions
. For example:
entry_points={
"force.bdss.extensions": [
"enthought_example = "
"enthought_example.example_plugin:ExamplePlugin",
]
}
The plugin¶
The plugin class must be
Inheriting from
force_bdss.api.BaseExtensionPlugin
Implement a
id
class member, that must be set to the result of calling the functionplugin_id()
. For example:id = plugin_id("enthought", "example", 0)
Implement a method
get_factory_classes()
returning a list of all the classes (NOT the instances) of the entities you want to export.Implement the methods
get_name()
,get_version()
andget_description()
to return appropriate values. Theget_version()
method in particular should return the same value as in the id (in this case zero). It is advised to extract this value in a global, module level constant.
The Factory¶
The factory must inherit from the appropriate factory for the given type.
For example, to create a DataSource, the factory must inherit from
BaseDataSourceFactory
. It then needs these methods to be redefined
get_identifier()
: must returns a unique string, e.g. a uuid or a memorable string that must be unique across your plugins, present and future.get_name()
: a memorable, user presentable name for the data source.get_description()
: a user presentable description.get_model_class()
: Must return the Model class.get_data_source_class()
: Must return the data source class.
The Model class¶
The model class must inherit from the appropriate Base model class, depending
on the entity, for example BaseDataSourceModel
in case of a data source.
This class then must be treated as a Traits class, where you can use traits
to define the type of data it holds. Pay particular attention to those data
that can modify the slots. For those, add a changes_slots=True
metadata
tag to the trait. This will notify the UI that the new slots need to be
recomputed and presented to the user. Failing to do so will have unexpected
consequences. Example:
class MyModel(BaseDataSourceModel):
normal_option = String()
option_changing_slots = String(changes_slots=True)
Typically, options that change slots are those options that modify the behavior of the computational engine, thus requiring more or less input (input slots) or producing more or less output (output slots).
Many BaseModel
subclasses also include a verify
method, which is
called before an MCO run starts to ensure that the execution will be successful.
This verification step can also be triggered in the WfManager UI even before an MCO run is
submitted. For BaseDataSourceModel
subclasses it is automatically performed whenever the
slots objects are updated, however developers can also include the verify=True
metadata
on any additional trait that requires verification. Including this in example above:
class MyModel(BaseDataSourceModel):
normal_option = String(verify=True)
option_changing_slots = String(changes_slots=True)
You can also define a UI view with traitsui (import traitsui.api
). This is
recommended as the default view arranges the options in random order. To do
so, have a default_traits_view()
method:
def default_traits_view():
return View(
Item("normal_option"),
Item("option_changing_slots")
)
The DataSource class¶
This is the “business end” of the data source, and where things are done.
The class must be derived from BaseDataSource
), and reimplement
the appropriate methods:
run()
: where the actual computation takes place, given the configuration options specified in the model (which is received as an argument). It is strongly advised that therun()
method is stateless.slots()
: must return a 2-tuple of tuples. Each tuple contains instances of theSlot
class. Slots are the input and output entities of the data source. Given that this information depends on the configuration options,slots()
accepts the model and must return the appropriate values according to the model options.
The MCO class¶
Like the data source, the MCO needs a model (derived from BaseMCOModel
),
a factory (derived from BaseMCOFactory
) and a MCO class (derived from
BaseMCO
). Additional entities must be also provided:
MCOCommunicator
: this class is responsible for handling communication between the MCO and the spawned process when the MCO is using a “subprocess” model, that is, the MCO invokes the force_bdss in evaluation mode to compute a single point.parameters
: We assume that different MCOs can support different parameter types for the generated variables. Currently, only the “range” type is commonly handled.
The factory then must be added to the plugin get_factory_classes()
list.
The factory must define the following methods:
def get_identifier(self):
def get_name(self):
def get_description(self):
def get_model_class(self):
as in data source factory. The following:
def get_optimizer_class(self):
def get_communicator_class(self):
Must return classes of the MCO and the MCOCommunicator. Finally:
def get_parameter_factory_classes(self):
Must return a list of classes of the parameter factories.
Optimizer Engines¶
The force_bdss.api
package offers the BaseOptimizerEngine
and
SpaceSampler
abstract classes, both of which are designed as utility objects for backend developers.
The BaseOptimizerEngine
class provides a schema that can easily be reimplemented to
act as an interface between the BDSS and an external optimization library. Although it is not strictly
required to run an MCO, it is expected that a developer would import the object into a BaseMCO.run
implementation, whilst providing any relevant pre and post processing of information for the specific used
case that the MCO is solving. The base class must simply define the following method:
def optimize(self):
Which is expected to act as a generator, yielding values corresponding to optimised input parameters
and their corresponding KPIs. A concrete implementation of this base class, the WeightedOptimizerEngine
,
is provided that uses the SciPy
library as a backend.
The SpaceSampler
abstract class also acts as a utility class in order to sample
vectors of values from a given distribution. Implementations of this class could be used to either provide
trial parameter sets to feed into an optimiser as initial points, or importance weights to apply to each KPI.
The base class must define the following methods:
def _get_sample_point(self):
def generate_space_sample(self, *args, **kwargs):
Two concrete implementations of this class are provided: UniformSpaceSampler
, which performs a grid
search and DirichletSpaceSampler
, which samples random points from the Dirichlet distribution.
MCO Communicator¶
The MCO Communicator must reimplement BaseMCOCommunicator and two methods:
receive_from_mco()
and send_to_mco()
. These two methods can use files,
stdin/stdout or any other trick to send and receive data between the MCO and
the BDSS running as a subprocess of the MCO to evaluate a single point.
Parameter factories¶
MCO parameter types also require a model and a factory per each type. Right now, the only typo encountered is Range, but others may be provided in the future, by MCOs that support them.
The parameter factory must inherit from BaseMCOParameterFactory
and
reimplement:
def get_identifier(self):
def get_name(self):
def get_description(self):
as in the case of data source. Then:
def get_model_class(self):
must return a model class for the given parameter, inheriting from
BaseMCOParameter
. This model contains the data the user can set, and is
relevant to the given parameter. For example, in the case of a Range, it might
specify the min and max value, as well as the starting value.
Notification Listeners¶
Notification listeners are used to notify the state of the MCO to external listeners, including the data that is obtained by the MCO as it performs the evaluation. Communication to databases (for writing) and CSV/HDF5 writers are notification listeners.
The notification listener requires a model (inherit from
BaseNotificationListenerModel
), a factory (from
BaseNotificationListenerFactory
) and a notification listener
(from BaseNotificationListener
). The factory requires, in addition to:
def get_identifier(self):
def get_name(self):
def get_description(self):
def get_model_class(self):
the method:
get_listener_class()
return the notification listener object class.
The NotificationListener class must reimplement the following methods, that are invoked in specific lifetime events of the BDSS:
def initialize(self):
Called once, when the BDSS is initialized. For example, to setup the
connection to a database, or open a file.
def finalize(self):
Called once, when the BDSS is finalized. For example, to close the
connection to a database, or close a file.
def deliver(self, event):
Called every time the MCO generates an event. The event will be passed
as an argument. Depending on the argument, the listener implements
appropriate action. The available events are in the api module.
UI Hooks¶
UI Hooks are callbacks that are triggered at some events during the lifetime
of the UI. It has no model. The factory must inherit from
BaseUIHooksFactory
, and must reimplement get_ui_hooks_manager_class()
to return a class inheriting from BaseUIHooksManager
. This class has
specific methods to be reimplemented to perform operations before and after
some UI operations.
Any BaseDriverEvents
that are required to be delivered to a UI can be indicated
using the UIEventMixin
class. The MCOStartEvent
, MCOProgressEvent
and
MCOFinishEvent
are all examples of such objects.
Envisage Service Offers¶
A plugin can also define one or more custom visualization classes for the
GUI application force-wfmanager
, typically to either display data or
provide a tailor-made UI for a specific user. In which case, the plugin class
must inherit from force_bdss.core_plugins.service_offer_plugin.ServiceOfferExtensionPlugin
, which is a child class of BaseExtensionPlugin
. Any UI subclasses
can then be made discoverable by force-wfmanager
using the envisage
ServiceOffer
protocol through the get_service_offer_factories
method:
def get_service_offer_factories(self):
"""A method returning a list user-made objects to be provided by this
plugin as envisage ServiceOffer objects. Each item in the outer list is
a tuple containing an Interface trait to be used as the ServiceOffer
protocol and an inner list of subclass factories to be instantiated
from said protocol.
Returns
-------
service_offer_factories: list of tuples
List of objects to load, where each tuple takes the form
(Interface, [HasTraits1, HasTraits2..]), defining a Traits
Interface subclass and a list of HasTraits subclasses to be
instantiated as an envisage ServiceOffer.
"""
Make sure to import the module containing the data view class from inside
get_service_offer_factories
: this ensures that running BDSS without a GUI
application doesn’t import the graphical stack.
Custom UI classes¶
There are currently two types of custom UI object that may be contributed by a
plugin: IBasePlot
and IContributedUI
. These interfaces represent requirements
for any UI feature that can be used to display MCO data or a present a simplified
workflow builder respectively.
Also, multiple types of plugin contributed UI objects can be imported in the same call. For instance:
def get_service_offer_factories(self):
from force_wfmanager.ui import IBasePlot, IContributedUI
from .example_custom_uis import PlotUI, ExperimentUI, AnalysisUI
return [
(IBasePlot, [PlotUI]),
(IContributedUI, [ExperimentUI, AnalysisUI])
]