diff --git a/src/aiidalab_qe/app/submission/__init__.py b/src/aiidalab_qe/app/submission/__init__.py index 07f509cd6..344535f3a 100644 --- a/src/aiidalab_qe/app/submission/__init__.py +++ b/src/aiidalab_qe/app/submission/__init__.py @@ -11,7 +11,7 @@ from aiidalab_qe.app.parameters import DEFAULT_PARAMETERS from aiidalab_qe.app.utils import get_entry_items from aiidalab_qe.common.code import PluginCodes, PwCodeModel -from aiidalab_qe.common.panel import ResourceSettingsModel, ResourceSettingsPanel +from aiidalab_qe.common.panel import PluginResourceSettingsModel, ResourceSettingsPanel from aiidalab_qe.common.setup_codes import QESetupWidget from aiidalab_qe.common.setup_pseudos import PseudosInstallWidget from aiidalab_widgets_base import WizardAppWidgetStep @@ -337,7 +337,7 @@ def _fetch_plugin_resource_settings(self): raise ValueError(f"Entry {identifier} is missing the '{key}' key") panel = resources["panel"] - model: ResourceSettingsModel = resources["model"]() + model: PluginResourceSettingsModel = resources["model"]() model.observe( self._on_plugin_overrides_change, "override", diff --git a/src/aiidalab_qe/app/submission/global_settings/model.py b/src/aiidalab_qe/app/submission/global_settings/model.py index 5a1303a76..8b2710aa8 100644 --- a/src/aiidalab_qe/app/submission/global_settings/model.py +++ b/src/aiidalab_qe/app/submission/global_settings/model.py @@ -48,8 +48,6 @@ def __init__(self, *args, **kwargs): self.plugin_mapping: dict[str, list[str]] = {} - self.override = True - def update_global_codes(self): self.global_codes = self.get_model_state()["codes"] diff --git a/src/aiidalab_qe/app/submission/global_settings/setting.py b/src/aiidalab_qe/app/submission/global_settings/setting.py index 24276d987..29eaaea76 100644 --- a/src/aiidalab_qe/app/submission/global_settings/setting.py +++ b/src/aiidalab_qe/app/submission/global_settings/setting.py @@ -77,11 +77,10 @@ def set_up_codes(self, codes: PluginCodes): self._on_code_selection_change, "selected", ) + self._model.update_global_codes() def reset(self): - with self.hold_trait_notifications(): - self._model.reset() - self._model.set_selected_codes() + self._model.set_selected_codes() def _on_input_parameters_change(self, _): self._model.update_active_codes() diff --git a/src/aiidalab_qe/app/submission/model.py b/src/aiidalab_qe/app/submission/model.py index a61f46b0c..098b1f0c4 100644 --- a/src/aiidalab_qe/app/submission/model.py +++ b/src/aiidalab_qe/app/submission/model.py @@ -11,7 +11,7 @@ from aiidalab_qe.app.parameters import DEFAULT_PARAMETERS from aiidalab_qe.common.mixins import Confirmable, HasInputStructure, HasModels from aiidalab_qe.common.mvc import Model -from aiidalab_qe.common.panel import ResourceSettingsModel +from aiidalab_qe.common.panel import PluginResourceSettingsModel, ResourceSettingsModel from aiidalab_qe.workflows import QeAppWorkChain DEFAULT: dict = DEFAULT_PARAMETERS # type: ignore @@ -131,7 +131,9 @@ def update_plugin_overrides(self): self.plugin_overrides = [ identifier for identifier, model in self.get_models() - if identifier != "global" and model.include and model.override + if isinstance(model, PluginResourceSettingsModel) + and model.include + and model.override ] def update_submission_blockers(self): diff --git a/src/aiidalab_qe/common/mixins.py b/src/aiidalab_qe/common/mixins.py index b41c33d1c..c08bc73ad 100644 --- a/src/aiidalab_qe/common/mixins.py +++ b/src/aiidalab_qe/common/mixins.py @@ -51,7 +51,7 @@ def get_models(self) -> t.Iterable[tuple[str, T]]: return self._models.items() def _link_model(self, model: T): - raise NotImplementedError() + pass class HasProcess(tl.HasTraits): diff --git a/src/aiidalab_qe/common/panel.py b/src/aiidalab_qe/common/panel.py index a5828c17c..5224efc27 100644 --- a/src/aiidalab_qe/common/panel.py +++ b/src/aiidalab_qe/common/panel.py @@ -205,11 +205,9 @@ def _reset(self): class ResourceSettingsModel(SettingsModel, HasModels[CodeModel]): - """Base model for plugin code setting models.""" + """Base model for resource setting models.""" - dependencies = [ - "global.global_codes", - ] + dependencies = [] global_codes = tl.Dict( key_trait=tl.Unicode(), @@ -218,7 +216,6 @@ class ResourceSettingsModel(SettingsModel, HasModels[CodeModel]): submission_blockers = tl.List(tl.Unicode()) submission_warning_messages = tl.Unicode("") - override = tl.Bool(False) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -230,29 +227,11 @@ def add_model(self, identifier, model): super().add_model(identifier, model) model.update(self.DEFAULT_USER_EMAIL) - def update(self): - """Updates the code models from the global resources. - - Skips synchronization with global resources if the user has chosen to override - the resources for the plugin codes. - """ - if self.override: - return - for _, code_model in self.get_models(): - default_calc_job_plugin = code_model.default_calc_job_plugin - if default_calc_job_plugin in self.global_codes: - code_resources: dict = self.global_codes[default_calc_job_plugin] # type: ignore - options = code_resources.get("options", []) - if options != code_model.options: - code_model.update(self.DEFAULT_USER_EMAIL, refresh=True) - code_model.set_model_state(code_resources) - def update_submission_blockers(self): self.submission_blockers = list(self._check_submission_blockers()) def get_model_state(self): return { - "override": self.override, "codes": { identifier: code_model.get_model_state() for identifier, code_model in self.get_models() @@ -260,7 +239,6 @@ def get_model_state(self): } def set_model_state(self, parameters: dict): - self.override = parameters.get("override", self.identifier == "global") self.set_selected_codes(parameters.get("codes", {})) def get_selected_codes(self) -> dict[str, dict]: @@ -275,13 +253,143 @@ def set_selected_codes(self, code_data=DEFAULT["codes"]): if identifier in code_data: code_model.set_model_state(code_data[identifier]) - def reset(self): - """If not overridden, updates the model w.r.t the global resources.""" - self.update() - def _check_submission_blockers(self): return [] + +RSM = t.TypeVar("RSM", bound=ResourceSettingsModel) + + +class ResourceSettingsPanel(SettingsPanel[RSM], t.Generic[RSM]): + """Base class for resource setting panels.""" + + def __init__(self, model, **kwargs): + super().__init__(model, **kwargs) + + self.code_widgets = {} + + def _on_code_resource_change(self, _): + pass + + def _toggle_code(self, code_model: CodeModel): + if not self.rendered: + return + if not code_model.is_rendered: + loading_message = LoadingWidget(f"Loading {code_model.name} code") + self.code_widgets_container.children += (loading_message,) + if code_model.name not in self.code_widgets: + code_widget = code_model.code_widget_class( + description=code_model.description, + default_calc_job_plugin=code_model.default_calc_job_plugin, + ) + self.code_widgets[code_model.name] = code_widget + else: + code_widget = self.code_widgets[code_model.name] + if not code_model.is_rendered: + self._render_code_widget(code_model, code_widget) + + def _render_code_widget( + self, + code_model: CodeModel, + code_widget: QEAppComputationalResourcesWidget, + ): + ipw.dlink( + (code_model, "options"), + (code_widget.code_selection.code_select_dropdown, "options"), + ) + ipw.link( + (code_model, "selected"), + (code_widget.code_selection.code_select_dropdown, "value"), + ) + ipw.link( + (code_model, "num_cpus"), + (code_widget.num_cpus, "value"), + ) + ipw.link( + (code_model, "num_nodes"), + (code_widget.num_nodes, "value"), + ) + ipw.link( + (code_model, "ntasks_per_node"), + (code_widget.resource_detail.ntasks_per_node, "value"), + ) + ipw.link( + (code_model, "cpus_per_task"), + (code_widget.resource_detail.cpus_per_task, "value"), + ) + ipw.link( + (code_model, "max_wallclock_seconds"), + (code_widget.resource_detail.max_wallclock_seconds, "value"), + ) + if isinstance(code_widget, PwCodeResourceSetupWidget): + ipw.link( + (code_model, "parallelization_override"), + (code_widget.parallelization.override, "value"), + ) + ipw.link( + (code_model, "npool"), + (code_widget.parallelization.npool, "value"), + ) + code_model.observe( + self._on_code_resource_change, + [ + "parallelization_override", + "npool", + ], + ) + code_model.observe( + self._on_code_resource_change, + [ + "options", + "selected", + "num_cpus", + "num_nodes", + "ntasks_per_node", + "cpus_per_task", + "max_wallclock_seconds", + ], + ) + code_widgets = self.code_widgets_container.children[:-1] # type: ignore + self.code_widgets_container.children = [*code_widgets, code_widget] + code_model.is_rendered = True + + +class PluginResourceSettingsModel(ResourceSettingsModel): + """Base model for plugin resource setting models.""" + + dependencies = [ + "global.global_codes", + ] + + override = tl.Bool(False) + + def update(self): + """Updates the code models from the global resources. + + Skips synchronization with global resources if the user has chosen to override + the resources for the plugin codes. + """ + if self.override: + return + for _, code_model in self.get_models(): + default_calc_job_plugin = code_model.default_calc_job_plugin + if default_calc_job_plugin in self.global_codes: + code_resources: dict = self.global_codes[default_calc_job_plugin] # type: ignore + options = code_resources.get("options", []) + if options != code_model.options: + code_model.update(self.DEFAULT_USER_EMAIL, refresh=True) + code_model.set_model_state(code_resources) + + def get_model_state(self): + return { + "override": self.override, + **super().get_model_state(), + } + + def set_model_state(self, parameters: dict): + self.override = parameters.get("override", False) + super().set_model_state(parameters) + def _link_model(self, model: CodeModel): tl.link( (self, "override"), @@ -289,11 +397,11 @@ def _link_model(self, model: CodeModel): ) -RSM = t.TypeVar("RSM", bound=ResourceSettingsModel) +PRSM = t.TypeVar("PRSM", bound=PluginResourceSettingsModel) -class ResourceSettingsPanel(SettingsPanel[RSM], t.Generic[RSM]): - """Base class for plugin code setting panels.""" +class PluginResourceSettingsPanel(ResourceSettingsPanel[PRSM], t.Generic[PRSM]): + """Base class for plugin resource setting panels.""" def __init__(self, model, **kwargs): super().__init__(model, **kwargs) @@ -307,8 +415,6 @@ def __init__(self, model, **kwargs): "override", ) - self.code_widgets = {} - def render(self): if self.rendered: return @@ -348,77 +454,35 @@ def render(self): def _on_global_codes_change(self, _): self._model.update() - def _on_code_resource_change(self, _): - pass - def _on_override_change(self, _): - self._model.reset() - - def _toggle_code(self, code_model: CodeModel): - if not self.rendered: - return - if not code_model.is_rendered: - loading_message = LoadingWidget(f"Loading {code_model.name} code") - self.code_widgets_container.children += (loading_message,) - if code_model.name not in self.code_widgets: - code_widget = code_model.code_widget_class( - description=code_model.description, - default_calc_job_plugin=code_model.default_calc_job_plugin, - ) - self.code_widgets[code_model.name] = code_widget - else: - code_widget = self.code_widgets[code_model.name] - if not code_model.is_rendered: - self._render_code_widget(code_model, code_widget) + self._model.update() def _render_code_widget( self, code_model: CodeModel, code_widget: QEAppComputationalResourcesWidget, ): - ipw.dlink( - (code_model, "options"), - (code_widget.code_selection.code_select_dropdown, "options"), - ) - ipw.link( - (code_model, "selected"), - (code_widget.code_selection.code_select_dropdown, "value"), - ) + super()._render_code_widget(code_model, code_widget) + self._link_override_to_widget_disable(code_model, code_widget) + + def _link_override_to_widget_disable(self, code_model, code_widget): + """Links the override attribute of the code model to the disable attribute + of subwidgets of the code widget.""" ipw.dlink( (code_model, "override"), (code_widget.code_selection.code_select_dropdown, "disabled"), lambda override: not override, ) - ipw.link( - (code_model, "num_cpus"), - (code_widget.num_cpus, "value"), - ) ipw.dlink( (code_model, "override"), (code_widget.num_cpus, "disabled"), lambda override: not override, ) - ipw.link( - (code_model, "num_nodes"), - (code_widget.num_nodes, "value"), - ) ipw.dlink( (code_model, "override"), (code_widget.num_nodes, "disabled"), lambda override: not override, ) - ipw.link( - (code_model, "ntasks_per_node"), - (code_widget.resource_detail.ntasks_per_node, "value"), - ) - ipw.link( - (code_model, "cpus_per_task"), - (code_widget.resource_detail.cpus_per_task, "value"), - ) - ipw.link( - (code_model, "max_wallclock_seconds"), - (code_widget.resource_detail.max_wallclock_seconds, "value"), - ) ipw.dlink( (code_model, "override"), (code_widget.code_selection.btn_setup_new_code, "disabled"), @@ -430,46 +494,16 @@ def _render_code_widget( lambda override: not override, ) if isinstance(code_widget, PwCodeResourceSetupWidget): - ipw.link( - (code_model, "parallelization_override"), - (code_widget.parallelization.override, "value"), - ) ipw.dlink( (code_model, "override"), (code_widget.parallelization.override, "disabled"), lambda override: not override, ) - ipw.link( - (code_model, "npool"), - (code_widget.parallelization.npool, "value"), - ) ipw.dlink( (code_model, "override"), (code_widget.parallelization.npool, "disabled"), lambda override: not override, ) - code_model.observe( - self._on_code_resource_change, - [ - "parallelization_override", - "npool", - ], - ) - code_model.observe( - self._on_code_resource_change, - [ - "options", - "selected", - "num_cpus", - "num_nodes", - "ntasks_per_node", - "cpus_per_task", - "max_wallclock_seconds", - ], - ) - code_widgets = self.code_widgets_container.children[:-1] # type: ignore - self.code_widgets_container.children = [*code_widgets, code_widget] - code_model.is_rendered = True class ResultsModel(Model, HasProcess): diff --git a/src/aiidalab_qe/plugins/bands/resources.py b/src/aiidalab_qe/plugins/bands/resources.py index 02b4c3760..2905d72d8 100644 --- a/src/aiidalab_qe/plugins/bands/resources.py +++ b/src/aiidalab_qe/plugins/bands/resources.py @@ -1,10 +1,13 @@ """Panel for Bands plugin.""" from aiidalab_qe.common.code.model import CodeModel, PwCodeModel -from aiidalab_qe.common.panel import ResourceSettingsModel, ResourceSettingsPanel +from aiidalab_qe.common.panel import ( + PluginResourceSettingsModel, + PluginResourceSettingsPanel, +) -class BandsResourceSettingsModel(ResourceSettingsModel): +class BandsResourceSettingsModel(PluginResourceSettingsModel): """Model for the band structure plugin.""" identifier = "bands" @@ -27,5 +30,7 @@ def __init__(self, **kwargs): ) -class BandsResourceSettingsPanel(ResourceSettingsPanel[BandsResourceSettingsModel]): +class BandsResourceSettingsPanel( + PluginResourceSettingsPanel[BandsResourceSettingsModel], +): title = "Band Structure" diff --git a/src/aiidalab_qe/plugins/pdos/resources.py b/src/aiidalab_qe/plugins/pdos/resources.py index 11bbc6a2b..0d17798f7 100644 --- a/src/aiidalab_qe/plugins/pdos/resources.py +++ b/src/aiidalab_qe/plugins/pdos/resources.py @@ -1,10 +1,13 @@ """Panel for PDOS plugin.""" from aiidalab_qe.common.code.model import CodeModel, PwCodeModel -from aiidalab_qe.common.panel import ResourceSettingsModel, ResourceSettingsPanel +from aiidalab_qe.common.panel import ( + PluginResourceSettingsModel, + PluginResourceSettingsPanel, +) -class PdosResourceSettingsModel(ResourceSettingsModel): +class PdosResourceSettingsModel(PluginResourceSettingsModel): """Model for the pdos code setting plugin.""" identifier = "pdos" @@ -32,5 +35,7 @@ def __init__(self, **kwargs): ) -class PdosResourceSettingsPanel(ResourceSettingsPanel[PdosResourceSettingsModel]): +class PdosResourceSettingsPanel( + PluginResourceSettingsPanel[PdosResourceSettingsModel], +): title = "PDOS" diff --git a/src/aiidalab_qe/plugins/xas/resources.py b/src/aiidalab_qe/plugins/xas/resources.py index 1ead99223..7e9f77a9d 100644 --- a/src/aiidalab_qe/plugins/xas/resources.py +++ b/src/aiidalab_qe/plugins/xas/resources.py @@ -1,10 +1,13 @@ """Panel for XAS plugin.""" from aiidalab_qe.common.code.model import CodeModel, PwCodeModel -from aiidalab_qe.common.panel import ResourceSettingsModel, ResourceSettingsPanel +from aiidalab_qe.common.panel import ( + PluginResourceSettingsModel, + PluginResourceSettingsPanel, +) -class XasResourceSettingsModel(ResourceSettingsModel): +class XasResourceSettingsModel(PluginResourceSettingsModel): """Model for the XAS plugin.""" identifier = "xas" @@ -27,5 +30,7 @@ def __init__(self, **kwargs): ) -class XasResourceSettingsPanel(ResourceSettingsPanel[XasResourceSettingsModel]): +class XasResourceSettingsPanel( + PluginResourceSettingsPanel[XasResourceSettingsModel], +): title = "XAS Structure" diff --git a/tests/test_submit_qe_workchain/test_create_builder_default.yml b/tests/test_submit_qe_workchain/test_create_builder_default.yml index a05b8483a..9652a7e6d 100644 --- a/tests/test_submit_qe_workchain/test_create_builder_default.yml +++ b/tests/test_submit_qe_workchain/test_create_builder_default.yml @@ -66,7 +66,6 @@ codes: max_wallclock_seconds: 43200 nodes: 1 ntasks_per_node: 1 - override: true pdos: codes: dos: