From ab98967b146330ceeda0c37265d5a1305d78b611 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Wed, 6 Nov 2024 14:50:28 +0000 Subject: [PATCH] Update docs --- docs/source/development/architecture.rst | 262 +++++++++++++++++++++-- 1 file changed, 245 insertions(+), 17 deletions(-) diff --git a/docs/source/development/architecture.rst b/docs/source/development/architecture.rst index 5627b6c78..71e7938bd 100644 --- a/docs/source/development/architecture.rst +++ b/docs/source/development/architecture.rst @@ -12,21 +12,243 @@ Each step may contain several sections (panels), as shown in the figure below. .. image:: ../_static/images/plugin_step.png -Parameter transfer between steps ---------------------------------- +MVC design +========== -The data is passed to the next step by linking it to the corresponding ``trait`` of the step widget. -For example, the ``confirmed_structure`` of step 1 links to the ``input_structure`` trait of step 2. +The app is designed following a Model-View-Controller (MVC) pattern, with data managed solely by the model network (the source of truth). + +A simple model may look like the following: + +.. code:: python + + class SimpleModel(tl.HasTraits): + + # Define traits + trait = tl.Unicode("I am a trait") # `tl` is alias for `traitlets` + ... + + def __init__(self, *args, **kwargs): # Optional, only if required + super().__init__(*args, **kwargs) + + # Optional attributes can be defined here + + def reset(self): + # Reset the model to its defaults + +The corresponding View-Controller may look like this: + +.. code:: python + + class SimpleWidget(ipw.VBox): + def __init__(self, model=SimpleModel, **kwargs): + from aiidalab_qe.common.widgets import LoadingWidget + + super().__init__( + children=[LoadingWidget("Loading message")], # loading spinner icon + **kwargs, + ) + + self._model = model + + # Contoller observations of model changes + self._model.observe( + self._on_trait_change, + "trait", + ) + ... + + # View + def render(self): + if self.rendered: + return + + # Define and link widgets + widget = ipw.Dropdown(description="Trait") + ipw.dlink( + (self._model, "options"), + (widget, "value"), + ) + ipw.link( + (self._model, "trait"), + (widget, "value"), + ) + ipw.dlink( + (self._model, "override"), + (widget, "disabled"), + lambda override: not override, + ) + ... + + self.children = [ + widget, + ... + ] + + self.rendered = True + + # Controller methods + # EXAMPLE + def _on_trait_change(self, change): + # Update widgets based on model changes + ... + +MVC in the configuration step +----------------------------- + +Some models require additional functionality, particularly in the configuration step (step 2), where many MVC components come together to form the +UI for specifying both the workflow and its input. +Each of these is defined as a ``SettingsPanel`` with a corresponding ``SettingsModel``. +Some of these are defined in the app, such as the basic and advanced settings panels. +For dedicated calculations (e.g. bands, pdos, xps), they are defined externally as :ref:`plugins `. + +Below are snippets of the ``AdvancedModel`` and ``AdvancedSettings`` classes: + +.. code:: python + + class AdvancedModel( + SettingsModel, + HasModels[AdvancedSubModel], + HasInputStructure, + ): + dependencies = [ + "input_structure", + "workchain.protocol", + ... + ] + + protocol = tl.Unicode() + + kpoints_distance = tl.Float(0.0) + ... + + self.include = True # includes this model in the configuration step + + # Optional attributes can be defined here + self.dftd3_version = { + ... + } + + def update(self, specific=""): + # Update the model, optionally limited to a specific scope + ... + + def get_model_state(self) -> dict: + # Return the model state as a dictionary + ... + + def set_model_state(self, parameters: dict): + # Set the model state from a parameters dictionary + # Used when loading a previous calculation + ... + + def reset(self): + # Reset the model to its defaults + ... + +Model updates recompute trait defaults w.r.t dependent traits (e.g. input structure, protocol) and update the model's traits to these defaults. +The defaults are stored in a ``_defaults`` dictionary that is used in ``reset`` to return the model to its *current* defaults. +The dependencies of a model are defined in the ``dependencies`` list, which is used by in configuration step during plugin discovery to connect the model network. +The inter-connection of dependency traits forms an **Observer** pattern across the app, with each dependent model receiving notifications of state changes in dependencies. +When a calculation is submitted, the configuration step will collect the parameters from all *included* models (``include == True``) and pass them to the submit step. + +.. note:: The basic and advanced models are included by default. Inclusion of installed plugin models is controlled by the user in step 2.1. + +.. warning:: The ``default_value`` of ``List`` or ``Dict`` traits is not stored by ``traitlets``. + Instead, explicitly define the defaults to at least ``[]`` or ``{}``, respectively. + +.. code:: python + + class AdvancedSettings(SettingsPanel[AdvancedModel]): + title = "Advanced Settings" + identifier = "advanced" + + def __init__(self, model: AdvancedModel, **kwargs): + super().__init__( + model=model, + layout={"justify_content": "space-between", **kwargs.get("layout", {})}, + **kwargs, + ) + + self._model.observe( + self._on_input_structure_change, + "input_structure", + ) + ... + + # Optionally connect sub-MVC components + smearing_model = SmearingModel() + self.smearing = SmearingSettings(model=smearing_model) + model.add_model("smearing", smearing_model) + ... + + def render(self): + if self.rendered: + return + + # Define and link widgets + ... + + self.children = [ + ... + ] + + self.rendered = True + +.. note:: The generic type ``AdvancedModel`` in ``SettingsPanel[AdvancedModel]`` is used to specify the model type, which is used to infer the type of the model in the class and provide type hinting in modern IDEs. + +Mixins +------ + +The ``HasModels`` mixin class inherited by the ``AdvancedModel`` is used to manage sub-models. +It provides functionality to add, register, and get a sub-model. +It is presently used by the configuration step to register basic, advanced, and plugin models. +It is also used by the advanced panel to register the sub-sections of the advanced settings (e.g. magnetization, hubbard, etc.). + +Other mixins, such as ``HasInputStructure``, provide are trait oriented, providing both the trait and methods to work with it. +The ``Confirmable`` mixin, for example, provides a ``confirmed`` trait, a ``confirm`` method, and an means of un-confirming on any state change in the inheriting model. + +.. code:: python + + class Confirmable(tl.HasTraits): + confirmed = tl.Bool(False) + + def confirm(self): + self.confirmed = True + + @tl.observe(tl.All) + def _on_any_change(self, change): + if change and change["name"] != "confirmed": + self._unconfirm() + + def _unconfirm(self): + self.confirmed = False + +App status +========== + +To keep track of the status of the app at any given step, a ``state`` trait of a step is linked with the ``previous_step_state`` trait of its following step. A ``SUCCESS`` state is used to auto-proceed to a following step. +The ``state`` trait is updated on most events. .. code:: python ipw.dlink( - (self.structure_selection_step, "confirmed_structure"), - (self.configure_qe_app_work_chain_step, "input_structure"), - ) + (self.structure_step, "state"), + (self.configure_step, "previous_step_state"), + ) + +Data management across steps +============================ + +Data is passed to the next step by use of ``App``-level controller observations of step (un)confirmation following the **Mediator** pattern, as follows: + +- Step 1 confirmed -> triggers setting of ``input_structure`` in step 2 +- Step 2 confirmed -> triggers setting of ``input_structure`` and ``input_parameters`` in step 3 +- Step 3 submitted -> triggers setting of ``process.uuid`` in step 4 -Each step widget can contains several panels. In the configuration step, the parameters from the panels are generated and stored as a dictionary, which is linked to the ``input_parameters`` trait of the next submit step. -The dictionary has the following structure: +.. note:: In the observers of step 1 and 2, ``_update_blockers()`` is also triggered to identify any current blockers to submission. + These blockers are linked with the submission model, which is used to show any submission warnings in step 3. + +In confirming step 2, the configuration step collects the parameters from all included models and returns them as a dictionary of the following format: .. code:: python @@ -55,17 +277,23 @@ The dictionary has the following structure: "plugin_2": {...}, } -Plugin -========= -QuantumESPRESSO app supports running multiple properties (bands, pdos, etc.) calculations in one app. -The individual properties to be developed and seamlessly integrated into the QuantumESPRESSO app as plugins. This integration is made possible due to several key aspects: +.. _develop:plugins: + +Plugins +======= -- the configuration for a property calculation has its settings unrelated to other properties. -- the sub-workchain of the properties can be run independently. -- the analysis of the results of the properties is independent. +The Quantum ESPRESSO app supports computing multiple properties (bands, pdos, etc.). +For this, plugins are to be developed and seamlessly integrated into the app. +The integration is made possible by several key aspects: -Each plugin responsible for one property calculation. +- The configuration settings for a property calculation must be decoupled from any other plugin - no cross-dependency +- The sub-workchain of a property can be run independently +- The analysis of the results of a property is independent + +Each plugin is responsible for the calculation of a single property. For instance, we could create a PDOS plugin, including its settings, workchain, and result analysis. + +.. TODO modify below The GUI of the PDOS plugin is only loaded when the user selects to run it. Here is an example, where two new setting panels are shown when the user selects to run the properties.