From 021ffe89aad061a231fa57100bfd538816e6bddd Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Fri, 14 Feb 2025 09:54:44 +0000 Subject: [PATCH] Lift app setup above wizard initialization --- qe.ipynb | 9 -- src/aiidalab_qe/app/main.py | 65 ++----------- src/aiidalab_qe/app/parameters/__init__.py | 2 +- .../app/result/components/summary/summary.py | 1 + src/aiidalab_qe/app/submission/__init__.py | 80 +--------------- src/aiidalab_qe/app/submission/model.py | 29 +----- src/aiidalab_qe/app/wizard_app.py | 3 +- src/aiidalab_qe/app/wrapper.py | 95 ++++++++++++++++++- src/aiidalab_qe/common/widgets.py | 10 +- tests/conftest.py | 10 +- tests/test_app.py | 2 +- tests/test_codes.py | 2 +- 12 files changed, 116 insertions(+), 192 deletions(-) diff --git a/qe.ipynb b/qe.ipynb index 4f93c7488..fa915e5e5 100644 --- a/qe.ipynb +++ b/qe.ipynb @@ -60,15 +60,6 @@ "\n", "app = QeApp(process=pk)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "app.load()" - ] } ], "metadata": { diff --git a/src/aiidalab_qe/app/main.py b/src/aiidalab_qe/app/main.py index e482b73c1..e8507f5d9 100644 --- a/src/aiidalab_qe/app/main.py +++ b/src/aiidalab_qe/app/main.py @@ -5,15 +5,10 @@ from pathlib import Path -import ipywidgets as ipw from IPython.display import display from aiidalab_qe.app.static import styles -from aiidalab_qe.app.wizard_app import WizardApp from aiidalab_qe.app.wrapper import AppWrapperContoller, AppWrapperModel, AppWrapperView -from aiidalab_widgets_base.bug_report import ( - install_create_github_issue_exception_handler, -) from aiidalab_widgets_base.utils.loaders import load_css DEFAULT_BUG_REPORT_URL = "https://github.com/aiidalab/aiidalab-qe/issues/new" @@ -29,64 +24,18 @@ def __init__( ): """Initialize the AiiDAlab QE application with the necessary setup.""" - self.process = process - self.qe_auto_setup = qe_auto_setup - self.log_widget = None - self._load_styles() # Initialize MVC components - self.model = AppWrapperModel() - self.view = AppWrapperView() - display(self.view) - - if show_log: - self.log_widget = ipw.Output( - layout=ipw.Layout( - border="solid 1px lightgray", - margin="2px", - padding="5px", - ), - ) - reset_button = ipw.Button( - description="Clear log", - button_style="primary", - icon="trash", - layout=ipw.Layout(width="fit-content"), - ) - reset_button.on_click(lambda _: self.log_widget.clear_output()) - display( - ipw.VBox( - children=[ - reset_button, - self.log_widget, - ], - ) - ) + model = AppWrapperModel() + model.process = process + model.show_log = show_log + model.qe_auto_setup = qe_auto_setup + view = AppWrapperView(show_log, bug_report_url) + _ = AppWrapperContoller(model, view) - # Set up bug report handling (if a URL is provided) - if bug_report_url: - install_create_github_issue_exception_handler( - self.log_widget if show_log else self.view.output, - url=bug_report_url, - labels=("bug", "automated-report"), - ) - - # setup UI controls - self.controller = AppWrapperContoller(self.model, self.view) - self.controller.enable_controls() + display(view) def _load_styles(self): """Load CSS styles from the static directory.""" load_css(css_path=Path(styles.__file__).parent) - - def load(self): - """Initialize the WizardApp and integrate the app into the main view.""" - self.app = WizardApp( - qe_auto_setup=self.qe_auto_setup, - log_widget=self.log_widget, - ) - self.view.main.children = [self.app] - # load a previous calculation if it is provided - if self.process: - self.app.process = self.process diff --git a/src/aiidalab_qe/app/parameters/__init__.py b/src/aiidalab_qe/app/parameters/__init__.py index 0ff9acf3e..6cb23861f 100644 --- a/src/aiidalab_qe/app/parameters/__init__.py +++ b/src/aiidalab_qe/app/parameters/__init__.py @@ -48,7 +48,7 @@ def recursive_merge(d1, d2): DEFAULT_PARAMETERS = yaml.safe_load(resources.read_text(parameters, "qeapp.yaml")) -custom_config_file = Path.home() / ".aiidalab" / "quantumespresso" / "config.yml" +custom_config_file = Path.home() / ".aiidalab" / "quantum-espresso" / "config.yml" if custom_config_file.exists(): custom_config = yaml.safe_load(custom_config_file.read_text()) DEFAULT_PARAMETERS = recursive_merge(DEFAULT_PARAMETERS, custom_config) diff --git a/src/aiidalab_qe/app/result/components/summary/summary.py b/src/aiidalab_qe/app/result/components/summary/summary.py index b182d9ae5..6aefcd139 100644 --- a/src/aiidalab_qe/app/result/components/summary/summary.py +++ b/src/aiidalab_qe/app/result/components/summary/summary.py @@ -71,6 +71,7 @@ def _render_summary(self): (self._model, "failed_calculation_report"), (self.failed_calculation_report, "value"), ) + self._model.generate_failure_report() self.children = [ container, diff --git a/src/aiidalab_qe/app/submission/__init__.py b/src/aiidalab_qe/app/submission/__init__.py index c3777adfd..e1ab7072d 100644 --- a/src/aiidalab_qe/app/submission/__init__.py +++ b/src/aiidalab_qe/app/submission/__init__.py @@ -17,8 +17,6 @@ PluginResourceSettingsPanel, ResourceSettingsPanel, ) -from aiidalab_qe.common.setup_codes import QESetupWidget -from aiidalab_qe.common.setup_pseudos import PseudosInstallWidget from aiidalab_qe.common.widgets import LinkButton, QeDependentWizardStep from .global_settings import GlobalResourceSettingsModel, GlobalResourceSettingsPanel @@ -30,7 +28,7 @@ class SubmitQeAppWorkChainStep(QeDependentWizardStep[SubmissionStepModel]): missing_information_warning = "Missing input structure and/or configuration parameters. Please set them first." - def __init__(self, model: SubmissionStepModel, qe_auto_setup=True, **kwargs): + def __init__(self, model: SubmissionStepModel, **kwargs): super().__init__(model=model, **kwargs) self._model.observe( self._on_submission, @@ -47,22 +45,6 @@ def __init__(self, model: SubmissionStepModel, qe_auto_setup=True, **kwargs): "external_submission_blockers", ], ) - self._model.observe( - self._on_installation_change, - ["installing_sssp", "sssp_installed"], - ) - self._model.observe( - self._on_sssp_installed, - "sssp_installed", - ) - self._model.observe( - self._on_installation_change, - ["installing_qe", "qe_installed"], - ) - self._model.observe( - self._on_qe_installed, - "qe_installed", - ) global_resources_model = GlobalResourceSettingsModel() self.global_resources = GlobalResourceSettingsPanel( @@ -87,9 +69,6 @@ def __init__(self, model: SubmissionStepModel, qe_auto_setup=True, **kwargs): } self._fetch_plugin_resource_settings() - self._install_sssp(qe_auto_setup) - self._set_up_qe(qe_auto_setup) - def _render(self): self.process_label = ipw.Text( description="Label:", @@ -179,8 +158,6 @@ def _render(self): layout=ipw.Layout(grid_gap="5px"), ), self.tabs, - self.sssp_installation, - self.qe_setup, self.submission_blocker_messages, self.submission_warning_messages, ipw.HTML(""" @@ -235,64 +212,9 @@ def _on_submission_blockers_change(self, _): self._model.update_submission_blocker_message() self._update_state() - def _on_installation_change(self, _): - self._model.update_submission_blockers() - - def _on_qe_installed(self, _): - self._toggle_qe_installation_widget() - if self._model.qe_installed: - self._model.update() - - def _on_sssp_installed(self, _): - self._toggle_sssp_installation_widget() - def _on_submission(self, _): self._update_state() - def _install_sssp(self, qe_auto_setup): - self.sssp_installation = PseudosInstallWidget(auto_start=False) - ipw.dlink( - (self.sssp_installation, "busy"), - (self._model, "installing_sssp"), - ) - ipw.dlink( - (self.sssp_installation, "installed"), - (self._model, "installing_sssp"), - lambda installed: not installed, - ) - ipw.dlink( - (self.sssp_installation, "installed"), - (self._model, "sssp_installed"), - ) - if qe_auto_setup: - self.sssp_installation.refresh() - - def _set_up_qe(self, qe_auto_setup): - self.qe_setup = QESetupWidget(auto_start=False) - ipw.dlink( - (self.qe_setup, "busy"), - (self._model, "installing_qe"), - ) - ipw.dlink( - (self.qe_setup, "installed"), - (self._model, "installing_qe"), - lambda installed: not installed, - ) - ipw.dlink( - (self.qe_setup, "installed"), - (self._model, "qe_installed"), - ) - if qe_auto_setup: - self.qe_setup.refresh() - - def _toggle_sssp_installation_widget(self): - sssp_installation_display = "none" if self._model.sssp_installed else "block" - self.sssp_installation.layout.display = sssp_installation_display - - def _toggle_qe_installation_widget(self): - qe_installation_display = "none" if self._model.qe_installed else "block" - self.qe_setup.layout.display = qe_installation_display - def _refresh_resources(self, _=None): for _, model in self._model.get_models(): model.refresh_codes() diff --git a/src/aiidalab_qe/app/submission/model.py b/src/aiidalab_qe/app/submission/model.py index 31ff49c03..0dfe87a75 100644 --- a/src/aiidalab_qe/app/submission/model.py +++ b/src/aiidalab_qe/app/submission/model.py @@ -37,11 +37,6 @@ class SubmissionStepModel( submission_blocker_messages = tl.Unicode("") submission_warning_messages = tl.Unicode("") - installing_qe = tl.Bool(False) - installing_sssp = tl.Bool(False) - qe_installed = tl.Bool(allow_none=True) - sssp_installed = tl.Bool(allow_none=True) - plugin_overrides = tl.List(tl.Unicode()) confirmation_exceptions = [ @@ -50,10 +45,6 @@ class SubmissionStepModel( "external_submission_blockers", "submission_blocker_messages", "submission_warning_messages", - "installing_qe", - "installing_sssp", - "qe_installed", - "sssp_installed", ] def __init__(self, *args, **kwargs): @@ -153,16 +144,15 @@ def update_plugin_overrides(self): ] def update_submission_blockers(self): - submission_blockers = list(self._check_submission_blockers()) + submission_blockers = [] for _, model in self.get_models(): submission_blockers += model.submission_blockers self.internal_submission_blockers = submission_blockers def update_submission_warnings(self): - submission_warning_messages = self._check_submission_warnings() - for _, model in self.get_models(): - submission_warning_messages += model.submission_warning_messages - self.submission_warning_messages = submission_warning_messages + self.submission_warning_messages = "".join( + model.submission_warning_messages for _, model in self.get_models() + ) # type: ignore def update_submission_blocker_message(self): blockers = self.internal_submission_blockers + self.external_submission_blockers @@ -292,14 +282,3 @@ def _create_builder(self, parameters) -> ProcessBuilderNamespace: builder.relax.base.pw.parallelization = orm.Dict(dict=parallelization) return builder - - def _check_submission_blockers(self): - if self.installing_qe or self.installing_sssp: - yield "Background setup processes must finish." - - if not self.sssp_installed: - yield "The SSSP library is not installed." - - def _check_submission_warnings(self): - """Check for any warnings that should be displayed to the user.""" - return "" diff --git a/src/aiidalab_qe/app/wizard_app.py b/src/aiidalab_qe/app/wizard_app.py index 93944ec18..baa479031 100644 --- a/src/aiidalab_qe/app/wizard_app.py +++ b/src/aiidalab_qe/app/wizard_app.py @@ -23,7 +23,7 @@ class WizardApp(ipw.VBox): # The PK or UUID of the work chain node. process = tl.Union([tl.Unicode(), tl.Int()], allow_none=True) - def __init__(self, qe_auto_setup=True, **kwargs): + def __init__(self, **kwargs): # Initialize the models self.structure_model = StructureStepModel() self.configure_model = ConfigurationStepModel() @@ -44,7 +44,6 @@ def __init__(self, qe_auto_setup=True, **kwargs): self.submit_step = SubmitQeAppWorkChainStep( model=self.submit_model, auto_advance=True, - qe_auto_setup=qe_auto_setup, ) self.results_step = ViewQeAppWorkChainStatusAndResultsStep( model=self.results_model, diff --git a/src/aiidalab_qe/app/wrapper.py b/src/aiidalab_qe/app/wrapper.py index 871fb6008..694f21786 100644 --- a/src/aiidalab_qe/app/wrapper.py +++ b/src/aiidalab_qe/app/wrapper.py @@ -4,8 +4,14 @@ import traitlets as tl from IPython.display import display +from aiidalab_qe.app.wizard_app import WizardApp from aiidalab_qe.common.guide_manager import guide_manager +from aiidalab_qe.common.setup_codes import QESetupWidget +from aiidalab_qe.common.setup_pseudos import PseudosInstallWidget from aiidalab_qe.common.widgets import LinkButton, LoadingWidget +from aiidalab_widgets_base.bug_report import ( + install_create_github_issue_exception_handler, +) def without_triggering(toggle: str): @@ -48,6 +54,8 @@ def __init__( self._view = view self._set_event_handlers() + self._setup() + def enable_controls(self) -> None: """Enable the control buttons at the top of the app.""" for control in self._view.controls.children: @@ -90,6 +98,40 @@ def _on_guide_selection_change(self, _): guide = self._view.guide_selection.value self._model.update_active_guide(category, guide) + def _on_installation_change(self, _): + if all([self._model.qe_installed, self._model.sssp_installed]): + self._load_app() + + def _setup(self): + self._install_sssp() + self._set_up_qe() + + def _install_sssp(self): + self.sssp_installation = PseudosInstallWidget(auto_start=False) + self._view.main.children += (self.sssp_installation,) + ipw.dlink( + (self.sssp_installation, "installed"), + (self._model, "sssp_installed"), + ) + if self._model.qe_auto_setup: + self.sssp_installation.refresh() + + def _set_up_qe(self): + self.qe_setup = QESetupWidget(auto_start=False) + self._view.main.children += (self.qe_setup,) + ipw.dlink( + (self.qe_setup, "installed"), + (self._model, "qe_installed"), + ) + if self._model.qe_auto_setup: + self.qe_setup.refresh() + + def _load_app(self): + self._view.main.children = [LoadingWidget("Loading the app")] + self.app = WizardApp(log_widget=self._view.log_widget) + self._view.main.children = [self.app] + self.app.process = self._model.process + def _set_event_handlers(self) -> None: """Set up event handlers.""" self._model.observe( @@ -103,6 +145,14 @@ def _set_event_handlers(self) -> None: "selected_guide", ], ) + self._model.observe( + self._on_installation_change, + "sssp_installed", + ) + self._model.observe( + self._on_installation_change, + "qe_installed", + ) self._view.guide_toggle.observe( self._on_guide_toggle, @@ -134,11 +184,19 @@ def _set_event_handlers(self) -> None: class AppWrapperModel(tl.HasTraits): """An MVC model for `AppWrapper`.""" + process = tl.Union([tl.Unicode(), tl.Int()], allow_none=True) + guide_category_options = tl.List(["none", *guide_manager.get_guide_categories()]) selected_guide_category = tl.Unicode("none") guide_options = tl.List(tl.Unicode()) selected_guide = tl.Unicode(None, allow_none=True) + qe_installed = tl.Bool(allow_none=True) + sssp_installed = tl.Bool(allow_none=True) + + qe_auto_setup = tl.Bool(True) + show_log = tl.Bool(False) + def update_active_guide(self, category, guide): """Sets the current active guide.""" active_guide = f"{category}/{guide}" if category != "none" else category @@ -148,7 +206,7 @@ def update_active_guide(self, category, guide): class AppWrapperView(ipw.VBox): """An MVC view for `AppWrapper`.""" - def __init__(self) -> None: + def __init__(self, show_log: bool = False, bug_report_url: str = "") -> None: """`AppWrapperView` constructor.""" ################# LAZY LOADING ################# @@ -264,7 +322,7 @@ def __init__(self) -> None: ) header.add_class("app-header") - self.main = ipw.VBox(children=[LoadingWidget("Loading the app")]) + self.main = ipw.VBox() current_year = datetime.now().year footer = ipw.HTML(f""" @@ -274,6 +332,38 @@ def __init__(self) -> None: """) + self.log_container = ipw.VBox() + + if show_log: + self.log_widget = ipw.Output( + layout=ipw.Layout( + border="solid 1px lightgray", + margin="2px", + padding="5px", + ), + ) + reset_button = ipw.Button( + description="Clear log", + button_style="primary", + icon="trash", + layout=ipw.Layout(width="fit-content"), + ) + reset_button.on_click(lambda _: self.log_widget.clear_output()) + self.log_container.children = [ + reset_button, + self.log_widget, + ] + else: + self.log_widget = None + + # Set up bug report handling (if a URL is provided) + if bug_report_url: + install_create_github_issue_exception_handler( + self.log_widget if show_log else self.output, + url=bug_report_url, + labels=("bug", "automated-report"), + ) + super().__init__( layout={}, children=[ @@ -281,5 +371,6 @@ def __init__(self) -> None: header, self.main, footer, + self.log_container, ], ) diff --git a/src/aiidalab_qe/common/widgets.py b/src/aiidalab_qe/common/widgets.py index 970fc2dd0..0ef261e8c 100644 --- a/src/aiidalab_qe/common/widgets.py +++ b/src/aiidalab_qe/common/widgets.py @@ -269,7 +269,7 @@ def _observe_value(self, change): class CalcJobOutputFollower(traitlets.HasTraits): calcjob_uuid = traitlets.Unicode(allow_none=True) filename = traitlets.Unicode(allow_none=True) - output = traitlets.List(trait=traitlets.Unicode) + output = traitlets.List(trait=traitlets.Unicode()) lineno = traitlets.Int() def __init__(self, **kwargs): @@ -454,8 +454,8 @@ class AddingTagsEditor(ipw.VBox): """Editor for adding tags to atoms.""" structure = traitlets.Instance(ase.Atoms, allow_none=True) - selection = traitlets.List(traitlets.Int, allow_none=True) - input_selection = traitlets.List(traitlets.Int, allow_none=True) + selection = traitlets.List(traitlets.Int(), allow_none=True) + input_selection = traitlets.List(traitlets.Int(), allow_none=True) structure_node = traitlets.Instance(orm_Data, allow_none=True, read_only=True) def __init__(self, title="", **kwargs): @@ -704,7 +704,7 @@ def __init__(self, **kwargs): """ self.code_selection = ComputationalResourcesWidget( include_setup_widget=False, - fetch_codes=True, # TODO resolve testing issues when set to `False` + fetch_codes=False, # TODO resolve testing issues when set to `False` **kwargs, ) self.code_selection.layout.width = "80%" @@ -1507,7 +1507,7 @@ def _report(self, filename: str, stdout: str, stderr: str): class ShakeNBreakEditor(ipw.VBox): structure = traitlets.Instance(ase.Atoms, allow_none=True) - selection = traitlets.List(traitlets.Int) + selection = traitlets.List(traitlets.Int()) structure_node = traitlets.Instance(orm_Data, allow_none=True, read_only=True) def __init__(self, title="Editor ShakeNbreak"): diff --git a/tests/conftest.py b/tests/conftest.py index 1ed2aadd8..e7566724a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -418,15 +418,7 @@ def _smearing_settings_generator(**kwargs): @pytest.fixture def app(pw_code, dos_code, projwfc_code, projwfc_bands_code): - app = WizardApp(qe_auto_setup=False) - - # Since we use `qe_auto_setup=False`, which will skip the pseudo library - # installation, we need to mock set the installation status to `True` to - # avoid the blocker message pop up in the submission step. - app.submit_model.installing_qe = False - app.submit_model.installing_sssp = False - app.submit_model.sssp_installed = True - app.submit_model.qe_installed = True + app = WizardApp() # set up codes global_model = app.submit_model.get_model("global") diff --git a/tests/test_app.py b/tests/test_app.py index 92ad482ac..91695e7dc 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -2,7 +2,7 @@ def test_reload_and_reset(generate_qeapp_workchain): - app = WizardApp(qe_auto_setup=False) + app = WizardApp() workchain = generate_qeapp_workchain( relax_type="positions", spin_type="collinear", diff --git a/tests/test_codes.py b/tests/test_codes.py index 09ee9f349..0554e42a8 100644 --- a/tests/test_codes.py +++ b/tests/test_codes.py @@ -18,7 +18,7 @@ def test_set_selected_codes(submit_app_generator): app: WizardApp = submit_app_generator() parameters = app.submit_model.get_model_state() model = SubmissionStepModel() - _ = SubmitQeAppWorkChainStep(model=model, qe_auto_setup=False) + _ = SubmitQeAppWorkChainStep(model=model) for identifier, code_model in app.submit_model.get_model("global").get_models(): model.get_model("global").get_model(identifier).is_active = code_model.is_active model.qe_installed = True