diff --git a/doc/source/design.rst b/doc/source/design.rst index b14ec9c26a7112e3cb76632a76f78b438f6c518f..b100159577aa67325021063ddcfe5e9b8f50b082 100644 --- a/doc/source/design.rst +++ b/doc/source/design.rst @@ -1,25 +1,26 @@ Design ------ -The application is based on five entities, as written in the introduction: +The application is based on four entities, as written in the introduction: - Multi Criteria Optimizer (MCO) - DataSources -- Key Performance Indicator (KPI) Calculators - Notification Listeners - UI Hooks There are a few core assumptions about each of these entities: -- The MCO design must honor the execution model of Dakota, that is, spawn - a secondary process that performs a computation starting from a given set - of input parameters, and produces a resulting set of output parameters. - In our code, this secondary process is ``force_bdss`` itself, invoked with - the option ``--evaluate``. -- The DataSources are entities that, given the MCO parameters, provide some - numerical result. This result is passed to the KPI calculators. -- The KPI calculators now compute the final KPIs that are then returned to - the invoker MCO. +- The MCO provides numerical values and injects them into a pipeline + made of multiple layers. Each layer is composed of multiple data sources. + The MCO can execute this pipeline directly, or indirectly by invoking + the force_bdss with the option ``--evaluate``. This invocation will produce, + given a vector in the input space, a vector in the output space. +- The DataSources are entities that are arranged in layers. Each DataSource has + inputs and outputs, called slots. These slots may depend on the configuration + options of the specific data source. The pipeline is created by binding + data sources outputs on the layers above with the data sources inputs of a + given layer. Each output can be designated as a KPI and thus be transmitted + back to the MCO for optimization. - The Notification Listener listens to the state of the MCO (Started/New step of the computation/Finished). It can be a remote database which is filled with the MCO results during the computation (e.g. the GUI ``force_wfmanager`` @@ -29,21 +30,17 @@ There are a few core assumptions about each of these entities: bdss, before saving the workflow). Those operations won't be executed by the command line interface of the bdss. - The result can be represented with the following data flow - -1. The MCO produces and, by means of a Communicator, injects... -2. ...DataSourceParameters, that are passed to... -3. one or more DataSources, each performing some computation or data - extraction and produces -4. DataSourceResult, one per DataSource, are then passed (together with the - DataSourceParameters) to... -5. one or more KPICalculators, which perform final data evaluation on the - obtained values, eac producing KPIResult... -6. Whose values are then returned to the MCO via the Communicator. -7. The KPI values are then sent to the notification listeners with the +1. The MCO produces and, potentially by means of a Communicator (if the + execution model is based on invoking ``force_bdss --evaluate``), + injects a given vector of MCO parameter values in the pipeline. +2. These values are passed to one or more DataSources, organised in layers, + each performing some computation or data extraction and produces results. + Layers are executed in order from top to bottom. There is no order among + DataSources on the same layer. +3. Results that have been classified as KPIs are then returned to the MCO + (again, potentially via the Communicator). +4. The KPI values are then sent to the notification listeners with the associated MCO parameters values - -The resulting pipeline is therefore just two layers (DataSources, then -KPICalculators). +5. The cycle repeats until all evaluations have been performed. diff --git a/doc/source/plugin_development.rst b/doc/source/plugin_development.rst index a6527350d874789bb9d25418ab0203e4069eaa32..1ada4602ab81c3faa91935c97e880aff33e5a26c 100644 --- a/doc/source/plugin_development.rst +++ b/doc/source/plugin_development.rst @@ -1,43 +1,231 @@ Plugin Development ------------------ -A single Plugin can provide one or more of the pluggable entities described -elsewhere (MCO/DataSources/NotificationListeners/UIHooks). -Multiple plugins can be installed to provide a broad range of functionalities. +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. An example plugin implementation is available at: https://github.com/force-h2020/force-bdss-plugin-enthought-example -Plugins must return Factories. Each Factory provides factory methods for -one of the above pluggable entities and its associated classes. - -To implement a new plugin, you must - -- define the entity you want to extend (e.g. ``MyOwnDataSource``) as a derived - class of the appropriate class (e.g. ``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 the ``run()`` method is stateless. - - ``slots()``: must return a 2-tuple of tuples. Each tuple contains instances - of the ``Slot`` class. Slots are the input and output entities of the - data source or KPI calculator. Given that this information depends on the - configuration options, ``slots()`` accepts the model and must return the - appropriate values according to the model options. - -- Define the model that this ``DataSource`` needs, by extending - ``BaseDataSourceModel`` and adding, with traits, the appropriate data that - are required by your data source to perform its task. - If a trait change in your model influences the input/output slots, you must - make sure that the event ``changes_slots`` is fired as a consequence of - those changes. 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. -- Define the Factory, by reimplementing BaseDataSourceFactory and reimplementing - its ``get_*`` methods to return the above entities. -- Define a ``Plugin`` by reimplementing ``BaseExtensionPlugin`` and - reimplementing its initialization defaults methods to return your factory. -- add the plugin class in the setup.py entry_point, under the namespace - ``force.bdss.extensions`` +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, or UIHook. +- A Factory class for the entity above: it is responsible for creating the + specific entity, for example, a DataSource +- 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. The Model is also shown visually + in the force_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 +entry_point, 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 function plugin_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. + + +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). + +You can also define a 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 the ``run()`` method is stateless. +- ``slots()``: must return a 2-tuple of tuples. Each tuple contains instances + of the ``Slot`` 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 parameter_factories(self): + +Must return a list of instances (NOT classes) of the parameter factories. + +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.