From c3454aa6a064dc88bcd84b15c949c00bf32732a5 Mon Sep 17 00:00:00 2001 From: Edan Bainglass Date: Thu, 31 Oct 2024 17:22:28 +0000 Subject: [PATCH] Decouple code models from the UI --- src/aiidalab_qe/app/submission/__init__.py | 225 ++++++++++-------- .../app/submission/code/__init__.py | 3 +- src/aiidalab_qe/app/submission/code/model.py | 112 +++++++-- src/aiidalab_qe/app/submission/model.py | 94 ++++---- tests/configuration/test_advanced.py | 5 - tests/conftest.py | 109 +++++---- tests/test_app.py | 6 +- tests/test_codes.py | 42 ++-- tests/test_configure.py | 3 - tests/test_plugins_bands.py | 6 - tests/test_plugins_electronic_structure.py | 4 - tests/test_plugins_pdos.py | 4 - tests/test_plugins_xas.py | 3 - tests/test_plugins_xps.py | 3 - tests/test_pseudo.py | 1 - tests/test_result.py | 29 +-- tests/test_submit_qe_workchain.py | 28 +-- 17 files changed, 372 insertions(+), 305 deletions(-) diff --git a/src/aiidalab_qe/app/submission/__init__.py b/src/aiidalab_qe/app/submission/__init__.py index 4c6a20197..ff90d64eb 100644 --- a/src/aiidalab_qe/app/submission/__init__.py +++ b/src/aiidalab_qe/app/submission/__init__.py @@ -12,10 +12,14 @@ from aiidalab_qe.app.utils import get_entry_items from aiidalab_qe.common.setup_codes import QESetupWidget from aiidalab_qe.common.setup_pseudos import PseudosInstallWidget -from aiidalab_qe.common.widgets import LoadingWidget, PwCodeResourceSetupWidget +from aiidalab_qe.common.widgets import ( + LoadingWidget, + PwCodeResourceSetupWidget, + QEAppComputationalResourcesWidget, +) from aiidalab_widgets_base import WizardAppWidgetStep -from .code import CodeModel, PluginCodes +from .code import CodeModel, PluginCodes, PwCodeModel from .model import SubmissionModel DEFAULT: dict = DEFAULT_PARAMETERS # type: ignore @@ -75,32 +79,13 @@ def __init__(self, model: SubmissionModel, qe_auto_setup=True, **kwargs): # "input_parameters", # ) - plugin_codes: PluginCodes = get_entry_items("aiidalab_qe.properties", "code") - plugin_codes |= { - "dft": { - "pw": CodeModel( - description="pw.x:", - default_calc_job_plugin="quantumespresso.pw", - setup_widget_class=PwCodeResourceSetupWidget, - ), - }, - } - for identifier, codes in plugin_codes.items(): - for name, code in codes.items(): - self._model.add_code(identifier, name, code) - code.observe( - self._on_code_activation_change, - "is_active", - ) - code.observe( - self._on_code_selection_change, - "selected", - ) - - self.rendered = False + self.code_widgets: dict[str, QEAppComputationalResourcesWidget] = {} self._install_sssp(qe_auto_setup) self._set_up_qe(qe_auto_setup) + self._set_up_codes() + + self.rendered = False def render(self): if self.rendered: @@ -189,23 +174,10 @@ def render(self): self.rendered = True - # Render and set up default PW code - pw_code = self._model.get_code("dft", "pw") - pw_code.activate() - pw_code_widget = pw_code.get_setup_widget() - pw_code_widget.num_cpus.observe( - self._on_pw_code_resource_change, - "value", - ) - pw_code_widget.num_nodes.observe( - self._on_pw_code_resource_change, - "value", - ) - - # Render any other active codes - self._toggle_code(pw_code) - for _, code in self._model.get_codes(flat=True): - if code is not pw_code and code.is_active: + # Render any active codes + self._model.get_code("dft", "pw").activate() + for _, code in self._model.get_code_models(flat=True): + if code.is_active: self._toggle_code(code) def reset(self): @@ -241,6 +213,8 @@ def _on_installation_change(self, _): def _on_qe_installed(self, _): self._toggle_qe_installation_widget() + if self._model.qe_installed: + self._model.refresh_codes() def _on_sssp_installed(self, _): self._toggle_sssp_installation_widget() @@ -258,57 +232,6 @@ def _on_submission(self, _): self._model.submit() self._update_state() - 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 _toggle_code(self, code: CodeModel): - if not self.rendered: - return - if not code.is_rendered: - loading_message = LoadingWidget(f"Loading {code.name} code") - self.code_widgets_container.children += (loading_message,) - code_widget = code.get_setup_widget() - code_widget.layout.display = "block" if code.is_active else "none" - if not code.is_rendered: - self._render_code_widget(code, code_widget) - - def _render_code_widget(self, code, code_widget): - ipw.dlink( - (code_widget.code_selection.code_select_dropdown, "options"), - (code, "options"), - ) - ipw.dlink( - (code_widget, "value"), - (code, "selected"), - lambda value: value is not None, - ) - code.observe( - lambda change: setattr(code_widget, "parameters", change["new"]), - "parameters", - ) - code_widgets = self.code_widgets_container.children[:-1] # type: ignore - self.code_widgets_container.children = [*code_widgets, code_widget] - self._model.code_widgets[code.name] = code_widget - self._model.set_selected_codes() # TODO check logic - code.is_rendered = True - - def _update_state(self, _=None): - if self.previous_step_state is self.State.FAIL: - self.state = self.State.FAIL - elif self.previous_step_state is not self.State.SUCCESS: - self.state = self.State.INIT - elif self._model.process is not None: - self.state = self.State.SUCCESS - elif self._model.is_blocked: - self.state = self.State.READY - else: - self.state = self.state.CONFIGURED - def _install_sssp(self, qe_auto_setup): self.sssp_installation = PseudosInstallWidget(auto_start=False) ipw.dlink( @@ -344,3 +267,119 @@ def _set_up_qe(self, qe_auto_setup): ) if qe_auto_setup: self.qe_setup.refresh() + + def _set_up_codes(self): + codes: PluginCodes = { + "dft": { + "pw": PwCodeModel( + description="pw.x", + default_calc_job_plugin="quantumespresso.pw", + code_widget_class=PwCodeResourceSetupWidget, + ), + }, + **get_entry_items("aiidalab_qe.properties", "code"), + } + for identifier, code_models in codes.items(): + for name, code_model in code_models.items(): + self._model.add_code(identifier, name, code_model) + code_model.observe( + self._on_code_activation_change, + "is_active", + ) + code_model.observe( + self._on_code_selection_change, + "selected", + ) + + 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 _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] + code_widget.layout.display = "block" if code_model.is_active else "none" + 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.dlink( + (code_model, "selected"), + (code_widget.code_selection.code_select_dropdown, "disabled"), + lambda selected: not selected, + ) + 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, "override"), + (code_widget.parallelization.override, "value"), + ) + ipw.link( + (code_model, "npool"), + (code_widget.parallelization.npool, "value"), + ) + code_model.observe( + self._on_pw_code_resource_change, + ["num_cpus", "num_nodes"], + ) + code_widgets = self.code_widgets_container.children[:-1] # type: ignore + self.code_widgets_container.children = [*code_widgets, code_widget] + code_model.is_rendered = True + + def _update_state(self, _=None): + if self.previous_step_state is self.State.FAIL: + self.state = self.State.FAIL + elif self.previous_step_state is not self.State.SUCCESS: + self.state = self.State.INIT + elif self._model.process is not None: + self.state = self.State.SUCCESS + elif self._model.is_blocked: + self.state = self.State.READY + else: + self.state = self.state.CONFIGURED diff --git a/src/aiidalab_qe/app/submission/code/__init__.py b/src/aiidalab_qe/app/submission/code/__init__.py index 117e94cdf..569a461ac 100644 --- a/src/aiidalab_qe/app/submission/code/__init__.py +++ b/src/aiidalab_qe/app/submission/code/__init__.py @@ -1,7 +1,8 @@ -from .model import CodeModel, CodesDict, PluginCodes +from .model import CodeModel, CodesDict, PluginCodes, PwCodeModel __all__ = [ "CodeModel", "CodesDict", "PluginCodes", + "PwCodeModel", ] diff --git a/src/aiidalab_qe/app/submission/code/model.py b/src/aiidalab_qe/app/submission/code/model.py index eb87141ce..71096a176 100644 --- a/src/aiidalab_qe/app/submission/code/model.py +++ b/src/aiidalab_qe/app/submission/code/model.py @@ -1,35 +1,48 @@ +import ipywidgets as ipw import traitlets as tl +from aiida import orm +from aiida.common import NotExistent from aiidalab_qe.common.widgets import QEAppComputationalResourcesWidget class CodeModel(tl.HasTraits): is_active = tl.Bool(False) - selected = tl.Bool(False) options = tl.List( trait=tl.Tuple(tl.Unicode(), tl.Unicode()), # code option (label, uuid) default_value=[], ) - parameters = tl.Dict() + selected = tl.Unicode(default_value=None, allow_none=True) + num_nodes = tl.Int(1) + num_cpus = tl.Int(1) + ntasks_per_node = tl.Int(1) + cpus_per_task = tl.Int(1) + max_wallclock_seconds = tl.Int(3600 * 12) + allow_hidden_codes = tl.Bool(False) + allow_disabled_computers = tl.Bool(False) def __init__( self, *, - name="pw", + name="", description, default_calc_job_plugin, - setup_widget_class=QEAppComputationalResourcesWidget, + code_widget_class=QEAppComputationalResourcesWidget, ): self.name = name self.description = description self.default_calc_job_plugin = default_calc_job_plugin - self.setup_widget_class = setup_widget_class + self.code_widget_class = code_widget_class self.is_rendered = False - self.is_loaded = False + + ipw.dlink( + (self, "num_cpus"), + (self, "ntasks_per_node"), + ) @property def is_ready(self): - return self.is_loaded and self.is_active and self.selected + return self.is_active and bool(self.selected) def activate(self): self.is_active = True @@ -37,14 +50,85 @@ def activate(self): def deactivate(self): self.is_active = False - def get_setup_widget(self) -> QEAppComputationalResourcesWidget: - if not self.is_loaded: - self._setup_widget = self.setup_widget_class( - description=self.description, - default_calc_job_plugin=self.default_calc_job_plugin, + def update(self, user_email): + if not self.options: + self.options = self._get_codes(user_email) + self.selected = self.options[0][1] if self.options else None + + def get_model_state(self) -> dict: + return { + "code": self.selected, + "nodes": self.num_nodes, + "cpus": self.num_cpus, + "ntasks_per_node": self.ntasks_per_node, + "cpus_per_task": self.cpus_per_task, + "max_wallclock_seconds": self.max_wallclock_seconds, + } + + def set_model_state(self, parameters): + self.selected = self._get_uuid(parameters["code"]) + self.num_nodes = parameters.get("nodes", 1) + self.num_cpus = parameters.get("cpus", 1) + self.ntasks_per_node = parameters.get("ntasks_per_node", 1) + self.cpus_per_task = parameters.get("cpus_per_task", 1) + self.max_wallclock_seconds = parameters.get("max_wallclock_seconds", 3600 * 12) + + def _get_uuid(self, identifier): + if not self.selected: + try: + uuid = orm.load_code(identifier).uuid + except NotExistent: + uuid = None + # If the code was imported from another user, it is not usable + # in the app and thus will not be considered as an option! + self.selected = uuid if uuid in [opt[1] for opt in self.options] else None + return self.selected + + def _get_codes(self, user_email): + user = orm.User.collection.get(email=user_email) + + filters = ( + {"attributes.input_plugin": self.default_calc_job_plugin} + if self.default_calc_job_plugin + else {} + ) + + codes = ( + orm.QueryBuilder() + .append( + orm.Code, + filters=filters, ) - self.is_loaded = True - return self._setup_widget + .all(flat=True) + ) + + return [ + (self._full_code_label(code), code.uuid) + for code in codes + if code.computer.is_user_configured(user) + and (self.allow_hidden_codes or not code.is_hidden) + and (self.allow_disabled_computers or code.computer.is_user_enabled(user)) + ] + + @staticmethod + def _full_code_label(code): + return f"{code.label}@{code.computer.label}" + + +class PwCodeModel(CodeModel): + override = tl.Bool(False) + npool = tl.Int(1) + + def get_model_state(self) -> dict: + parameters = super().get_model_state() + parameters["parallelization"] = {"npool": self.npool} if self.override else {} + return parameters + + def set_model_state(self, parameters): + super().set_model_state(parameters) + if "parallelization" in parameters and "npool" in parameters["parallelization"]: + self.override = True + self.npool = parameters["parallelization"].get("npool", 1) CodesDict = dict[str, CodeModel] diff --git a/src/aiidalab_qe/app/submission/model.py b/src/aiidalab_qe/app/submission/model.py index 2789e6565..decb379d0 100644 --- a/src/aiidalab_qe/app/submission/model.py +++ b/src/aiidalab_qe/app/submission/model.py @@ -7,7 +7,6 @@ import traitlets as tl from aiida import orm -from aiida.common import NotExistent from aiida.engine import ProcessBuilderNamespace from aiida.engine import submit as aiida_submit from aiida.orm.utils.serialize import serialize @@ -55,8 +54,6 @@ class SubmissionModel(tl.HasTraits): internal_submission_blockers = tl.List(tl.Unicode()) external_submission_blockers = tl.List(tl.Unicode()) - code_widgets: dict[str, QEAppComputationalResourcesWidget] = {} - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -72,6 +69,11 @@ def __init__(self, *args, **kwargs): """ + # Used by the code-setup thread to fetch code options + # This is necessary to avoid passing the User object + # between session in separate threads. + self._default_user_email = orm.User.collection.get_default().email + @property def is_blocked(self): return any( @@ -82,7 +84,7 @@ def is_blocked(self): ) def submit(self): - parameters = self._get_submission_parameters() + parameters = self.get_model_state() builder = self._create_builder(parameters) with self.hold_trait_notifications(): @@ -102,14 +104,13 @@ def submit(self): self.process = process def check_resources(self): - pw_code_model = self.get_code("dft", "pw") + pw_code = self.get_code("dft", "pw") - if not self.input_structure or not pw_code_model.selected: + if not self.input_structure or not pw_code.selected: return # No code selected or no structure, so nothing to do - pw_code = pw_code_model.get_setup_widget() - num_cpus = pw_code.num_cpus.value * pw_code.num_nodes.value - on_localhost = orm.load_node(pw_code.value).computer.hostname == "localhost" + num_cpus = pw_code.num_cpus * pw_code.num_nodes + on_localhost = orm.load_node(pw_code.selected).computer.hostname == "localhost" num_sites = len(self.input_structure.sites) volume = self.input_structure.get_cell_volume() @@ -184,15 +185,19 @@ def check_resources(self): ) ) + def refresh_codes(self): + for _, code_model in self.get_code_models(flat=True): + code_model.update(self._default_user_email) # type: ignore + def update_active_codes(self): - for name, code in self.get_codes(flat=True): + for name, code_model in self.get_code_models(flat=True): if name != "pw": - code.deactivate() - properties = self.get_properties() - for identifier, codes in self.get_codes(): + code_model.deactivate() + properties = self._get_properties() + for identifier, code_models in self.get_code_models(): if identifier in properties: - for code in codes.values(): - code.activate() + for code_model in code_models.values(): + code_model.activate() def update_process_label(self): if not self.input_structure: @@ -239,13 +244,10 @@ def update_submission_blocker_message(self): else: self.submission_blocker_messages = "" - def get_properties(self) -> list[str]: - return self.input_parameters.get("workchain", {}).get("properties", []) - def get_model_state(self) -> dict[str, dict[str, dict]]: - return { - "codes": self.get_selected_codes(), - } + parameters: dict = deepcopy(self.input_parameters) # type: ignore + parameters["codes"] = self.get_selected_codes() + return parameters def set_model_state(self, parameters): if "resources" in parameters: @@ -274,7 +276,10 @@ def get_code(self, identifier, name) -> CodeModel | None: if identifier in self.codes and name in self.codes[identifier]: # type: ignore return self.codes[identifier][name] # type: ignore - def get_codes(self, flat=False) -> t.Iterator[tuple[str, CodesDict | CodeModel]]: + def get_code_models( + self, + flat=False, + ) -> t.Iterator[tuple[str, CodesDict | CodeModel]]: if flat: for codes in self.codes.values(): yield from codes.items() @@ -283,27 +288,16 @@ def get_codes(self, flat=False) -> t.Iterator[tuple[str, CodesDict | CodeModel]] def get_selected_codes(self) -> dict[str, dict]: return { - name: code.parameters - for name, code in self.get_codes(flat=True) - if code.is_ready - } # type: ignore + name: code_model.get_model_state() + for name, code_model in self.get_code_models(flat=True) + if code_model.is_ready + } def set_selected_codes(self, code_data=DEFAULT["codes"]): - def get_code_uuid(code): - if code is not None: - try: - return orm.load_code(code).uuid - except NotExistent: - return None - with self.hold_trait_notifications(): - for name, code in self.get_codes(flat=True): + for name, code_model in self.get_code_models(flat=True): if name in code_data: - parameters = code_data[name] - code_uuid = get_code_uuid(parameters["code"]) - if code_uuid in [opt[1] for opt in code.options]: - parameters["code"] = code_uuid - code.parameters = parameters + code_model.set_model_state(code_data[name]) def reset(self): with self.hold_trait_notifications(): @@ -311,6 +305,9 @@ def reset(self): self.input_parameters = {} self.process = None + def _get_properties(self) -> list[str]: + return self.input_parameters.get("workchain", {}).get("properties", []) + def _create_builder(self, parameters) -> ProcessBuilderNamespace: builder = QeAppWorkChain.get_builder_from_protocol( structure=self.input_structure, @@ -331,17 +328,6 @@ def _create_builder(self, parameters) -> ProcessBuilderNamespace: return builder - def _get_submission_parameters(self) -> dict: - submission_parameters = self.get_model_state() - for name, code_widget in self.code_widgets.items(): - if name in submission_parameters["codes"]: - for key, value in code_widget.parameters.items(): - if key != "code": - submission_parameters["codes"][name][key] = value - parameters = deepcopy(self.input_parameters) - parameters.update(submission_parameters) - return parameters # type: ignore - def _check_submission_blockers(self): # Do not submit while any of the background setup processes are running. if self.installing_qe or self.installing_sssp: @@ -357,21 +343,21 @@ def _check_submission_blockers(self): yield ("No pw code selected") # code related to the selected property is not installed - properties = self.get_properties() + properties = self._get_properties() message = "Calculating the {property} property requires code {code} to be set." - for identifier, codes in self.get_codes(): + for identifier, codes in self.get_code_models(): if identifier in properties: for code in codes.values(): if not code.is_ready: yield message.format(property=identifier, code=code.description) # check if the QEAppComputationalResourcesWidget is used - for name, code in self.get_codes(flat=True): + for name, code in self.get_code_models(flat=True): # skip if the code is not displayed, convenient for the plugin developer if not code.is_ready: continue if not issubclass( - code.setup_widget_class, QEAppComputationalResourcesWidget + code.code_widget_class, QEAppComputationalResourcesWidget ): yield ( f"Error: hi, plugin developer, please use the QEAppComputationalResourcesWidget from aiidalab_qe.common.widgets for code {name}." diff --git a/tests/configuration/test_advanced.py b/tests/configuration/test_advanced.py index 79a69342d..1080d1f4f 100644 --- a/tests/configuration/test_advanced.py +++ b/tests/configuration/test_advanced.py @@ -1,5 +1,3 @@ -import pytest - from aiidalab_qe.app.configuration.advanced import AdvancedModel, AdvancedSettings @@ -87,7 +85,6 @@ def test_advanced_kpoints_settings(): assert model.kpoints_distance == 0.5 -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_advanced_molecule_settings(generate_structure_data): """Test kpoints setting of advanced setting widget.""" model = AdvancedModel() @@ -139,7 +136,6 @@ def test_advanced_tot_charge_settings(): assert model.total_charge == 0.0 -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_advanced_kpoints_mesh(generate_structure_data): """Test Mesh Grid HTML widget.""" model = AdvancedModel() @@ -156,7 +152,6 @@ def test_advanced_kpoints_mesh(generate_structure_data): assert model.mesh_grid == "Mesh [5, 5, 5]" -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_advanced_hubbard_settings(generate_structure_data): """Test Hubbard widget.""" from aiidalab_qe.app.configuration.advanced.hubbard import ( diff --git a/tests/conftest.py b/tests/conftest.py index 6b03234e5..88437cc47 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -235,8 +235,8 @@ def _generate_projection_data(): return _generate_projection_data -@pytest.fixture(scope="function") -def sssp(generate_upf_data): +@pytest.fixture(scope="session", autouse=True) +def sssp(generate_upf_data_for_session): """Create SSSP pseudopotentials from scratch.""" from aiida_pseudo.groups.family import SsspFamily @@ -247,7 +247,7 @@ def sssp(generate_upf_data): dirpath = pathlib.Path(d) for element in ELEMENTS: - upf = generate_upf_data(element) + upf = generate_upf_data_for_session(element) filename = dirpath / f"{element}.upf" with open(filename, "w+b") as handle: @@ -267,8 +267,8 @@ def sssp(generate_upf_data): family.set_cutoffs(cutoffs, stringency, unit="Ry") -@pytest.fixture(scope="function") -def pseudodojo(generate_upf_data): +@pytest.fixture(scope="session", autouse=True) +def pseudodojo(generate_upf_data_for_session): """Create pseudodojo pseudopotentials from scratch.""" from aiida_pseudo.data.pseudo import UpfData from aiida_pseudo.groups.family import PseudoDojoFamily @@ -279,10 +279,7 @@ def pseudodojo(generate_upf_data): dirpath = pathlib.Path(d) for element in ELEMENTS: - upf = generate_upf_data(element) - filename = dirpath / f"{element}.upf" - - upf = generate_upf_data(element) + upf = generate_upf_data_for_session(element) filename = dirpath / f"{element}.upf" with open(filename, "w+b") as handle: @@ -327,6 +324,28 @@ def _generate_upf_data(element, filename=None): return _generate_upf_data +@pytest.fixture(scope="session") +def generate_upf_data_for_session(): + """Return a `UpfData` instance for the given element a file for which should exist in `tests/fixtures/pseudos`. + + NOTE: Duplicated fixture for use in the session-scoped pseudos fixtures + """ + + def _generate_upf_data(element, filename=None): + """Return `UpfData` node.""" + from aiida_pseudo.data.pseudo import UpfData + + content = f'\n' + stream = io.BytesIO(content.encode("utf-8")) + + if filename is None: + filename = f"{element}.upf" + + return UpfData(stream, filename=filename) + + return _generate_upf_data + + @pytest.fixture def pw_code(aiida_local_code_factory): """Return a `Code` configured for the pw.x executable.""" @@ -394,36 +413,27 @@ def _smearing_settings_generator(**kwargs): @pytest.fixture def app(pw_code, dos_code, projwfc_code, projwfc_bands_code): - from aiidalab_qe.app.main import App - app = App(qe_auto_setup=False) - app.structure_step.render() - app.configure_step.render() - app.submit_step.render() - app.results_step.render() # 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_step.sssp_installation.installed = True - app.submit_step.qe_setup.installed = True + app.submit_model.installing_qe = False + app.submit_model.installing_sssp = False + app.submit_model.sssp_installed = True + app.submit_model.qe_installed = True # set up codes - app.submit_model.get_code("pdos", "dos").activate() - app.submit_model.get_code("pdos", "projwfc").activate() - app.submit_model.get_code("bands", "projwfc_bands").activate() + pw_code_model = app.submit_model.get_code("dft", "pw") + dos_code_model = app.submit_model.get_code("pdos", "dos") + projwfc_code_model = app.submit_model.get_code("pdos", "projwfc") + projwfc_bands_code_model = app.submit_model.get_code("bands", "projwfc_bands") - app.submit_model.code_widgets["pw"].code_selection.refresh() - app.submit_model.code_widgets["dos"].code_selection.refresh() - app.submit_model.code_widgets["projwfc"].code_selection.refresh() - app.submit_model.code_widgets["projwfc_bands"].code_selection.refresh() + pw_code_model.activate() + dos_code_model.activate() + projwfc_code_model.activate() + projwfc_bands_code_model.activate() - app.submit_model.code_widgets["pw"].value = pw_code.uuid - app.submit_model.code_widgets["dos"].value = dos_code.uuid - app.submit_model.code_widgets["projwfc"].value = projwfc_code.uuid - app.submit_model.code_widgets["projwfc_bands"].value = projwfc_bands_code.uuid - - # TODO overrides app defaults - check! app.submit_model.set_selected_codes( { "pw": {"code": pw_code.label}, @@ -490,10 +500,10 @@ def _submit_app_generator( smearing_model = advanced_model.get_model("smearing") smearing_model.type = smearing smearing_model.degauss = degauss - app.configure_step.confirm() + app.configure_model.confirm() app.submit_model.input_structure = generate_structure_data() - app.submit_model.code_widgets["pw"].num_cpus.value = 2 + app.submit_model.get_code("dft", "pw").num_cpus = 2 return app @@ -501,16 +511,15 @@ def _submit_app_generator( @pytest.fixture -def app_to_submit(app: App): +def app_to_submit(app: App, generate_structure_data): # Step 1: select structure from example - structure = app.structure_step.manager.children[0].children[3] # type: ignore - structure.children[0].value = structure.children[0].options[1][1] - app.structure_step.confirm() + app.structure_model.structure = generate_structure_data() + app.structure_model.confirm() # Step 2: configure calculation # TODO do we need to include bands and pdos here? app.configure_model.get_model("bands").include = True app.configure_model.get_model("pdos").include = True - app.configure_step.confirm() + app.configure_model.confirm() yield app @@ -728,6 +737,7 @@ def _generate_bands_workchain(structure): @pytest.fixture def generate_qeapp_workchain( app: App, + generate_structure_data, generate_workchain, generate_pdos_workchain, generate_bands_workchain, @@ -754,20 +764,17 @@ def _generate_qeapp_workchain( # Step 1: select structure from example if structure is None: - from_example = app.structure_step.manager.children[0].children[3] # type: ignore - # TODO: (unkpcz) using options to set value in test is cranky, instead, use fixture which will make the test more static and robust. - from_example.children[0].value = from_example.children[0].options[1][1] + structure = generate_structure_data() else: structure.store() - aiida_database_wrapper = app.structure_step.manager.children[0].children[2] # type: ignore - aiida_database_wrapper.render() - aiida_database = aiida_database_wrapper.children[0] # type: ignore - aiida_database.search() - aiida_database.results.value = structure - - app.structure_step.confirm() + # aiida_database_wrapper = app.structure_step.manager.children[0].children[2] # type: ignore + # aiida_database_wrapper.render() + # aiida_database = aiida_database_wrapper.children[0] # type: ignore + # aiida_database.search() + # aiida_database.results.value = structure - structure = app.structure_model.structure # type: ignore + app.structure_model.structure = structure + app.structure_model.confirm() # step 2 configure workchain_model = app.configure_model.get_model("workchain") @@ -801,11 +808,11 @@ def _generate_qeapp_workchain( else: magnetization_model.total = tot_magnetization - app.configure_step.confirm() + app.configure_model.confirm() # step 3 setup code and resources - app.submit_model.code_widgets["pw"].num_cpus.value = 4 - parameters = app.submit_model._get_submission_parameters() + app.submit_model.get_code("dft", "pw").num_cpus = 4 + parameters = app.submit_model.get_model_state() builder = app.submit_model._create_builder(parameters) inputs = builder._inputs() diff --git a/tests/test_app.py b/tests/test_app.py index 9fb1cbda8..50636e7c2 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,9 +1,6 @@ -import pytest - from aiidalab_qe.app.main import App -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_reload_and_reset(generate_qeapp_workchain): app = App(qe_auto_setup=False) workchain = generate_qeapp_workchain( @@ -35,7 +32,6 @@ def test_selecting_new_structure_unconfirms_model(generate_structure_data): assert not model.confirmed -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_unsaved_changes(app_to_submit): """Test if the unsaved changes are handled correctly""" from aiidalab_widgets_base import WizardAppWidgetStep @@ -52,7 +48,7 @@ def test_unsaved_changes(app_to_submit): assert len(app.submit_model.external_submission_blockers) == 1 # confirm the changes app._wizard_app_widget.selected_index = 1 - app.configure_step.confirm() + app.configure_model.confirm() app._wizard_app_widget.selected_index = 2 # the blocker should be removed assert len(app.submit_model.external_submission_blockers) == 0 diff --git a/tests/test_codes.py b/tests/test_codes.py index 781469fcb..5f1908878 100644 --- a/tests/test_codes.py +++ b/tests/test_codes.py @@ -1,29 +1,28 @@ -import pytest - from aiidalab_qe.app.main import App from aiidalab_qe.app.submission import SubmitQeAppWorkChainStep from aiidalab_qe.app.submission.model import SubmissionModel -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_code_not_selected(submit_app_generator): """Test if there is an error when the code is not selected.""" app: App = submit_app_generator(properties=["dos"]) model = app.submit_model - model.code_widgets["dos"].value = None + model.get_code("pdos", "dos").selected = None # Check builder construction passes without an error - parameters = model._get_submission_parameters() + parameters = model.get_model_state() model._create_builder(parameters) -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_set_selected_codes(submit_app_generator): """Test set_selected_codes method.""" app: App = submit_app_generator() - parameters = app.submit_model._get_submission_parameters() + parameters = app.submit_model.get_model_state() model = SubmissionModel() - new_submit_step = SubmitQeAppWorkChainStep(model=model, qe_auto_setup=False) - new_submit_step.render() + _ = SubmitQeAppWorkChainStep(model=model, qe_auto_setup=False) + for identifier, code_models in app.submit_model.get_code_models(): + for name, code_model in code_models.items(): + model.get_code(identifier, name).is_active = code_model.is_active + model.qe_installed = True model.set_selected_codes(parameters["codes"]) assert model.get_selected_codes() == app.submit_model.get_selected_codes() @@ -32,15 +31,15 @@ def test_update_codes_display(app: App): """Test update_codes_display method. If the workchain property is not selected, the related code should be hidden. """ + app.submit_step.render() model = app.submit_model model.update_active_codes() - assert model.code_widgets["dos"].layout.display == "none" + assert app.submit_step.code_widgets["dos"].layout.display == "none" model.input_parameters = {"workchain": {"properties": ["pdos"]}} model.update_active_codes() - assert model.code_widgets["dos"].layout.display == "block" + assert app.submit_step.code_widgets["dos"].layout.display == "block" -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_check_submission_blockers(app: App): """Test check_submission_blockers method.""" model = app.submit_model @@ -53,28 +52,29 @@ def test_check_submission_blockers(app: App): assert len(blockers) == 0 # set dos code to None, will introduce another blocker - dos_code_widget = model.code_widgets["dos"] - dos_value = dos_code_widget.value - dos_code_widget.value = None + dos_code = model.get_code("pdos", "dos") + dos_value = dos_code.selected + dos_code.selected = None blockers = list(model._check_submission_blockers()) assert len(blockers) == 1 # set dos code back will remove the blocker - dos_code_widget.value = dos_value + dos_code.selected = dos_value blockers = list(model._check_submission_blockers()) assert len(blockers) == 0 def test_qeapp_computational_resources_widget(app: App): """Test QEAppComputationalResourcesWidget.""" - - pw_code_widget = app.submit_model.code_widgets["pw"] + app.submit_step.render() + pw_code_model = app.submit_model.get_code("dft", "pw") + pw_code_widget = app.submit_step.code_widgets["pw"] assert pw_code_widget.parallelization.npool.layout.display == "none" - pw_code_widget.parallelization.override.value = True - pw_code_widget.parallelization.npool.value = 2 + pw_code_model.override = True + pw_code_model.npool = 2 assert pw_code_widget.parallelization.npool.layout.display == "block" assert pw_code_widget.parameters == { - "code": app.submit_model.code_widgets["pw"].value, # TODO why None? + "code": app.submit_step.code_widgets["pw"].value, # TODO why None? "cpus": 1, "cpus_per_task": 1, "max_wallclock_seconds": 43200, diff --git a/tests/test_configure.py b/tests/test_configure.py index 716709537..12f985d53 100644 --- a/tests/test_configure.py +++ b/tests/test_configure.py @@ -1,5 +1,3 @@ -import pytest - from aiidalab_qe.app.configuration import ConfigureQeAppWorkChainStep from aiidalab_qe.app.configuration.model import ConfigurationModel from aiidalab_qe.setup.pseudos import PSEUDODOJO_VERSION, SSSP_VERSION @@ -30,7 +28,6 @@ def test_get_configuration_parameters(): assert parameters == parameters_ref -@pytest.mark.usefixtures("aiida_profile_clean", "sssp", "pseudodojo") def test_set_configuration_parameters(): model = ConfigurationModel() _ = ConfigureQeAppWorkChainStep(model=model) diff --git a/tests/test_plugins_bands.py b/tests/test_plugins_bands.py index 05b4b4e19..c0d5f79c9 100644 --- a/tests/test_plugins_bands.py +++ b/tests/test_plugins_bands.py @@ -1,7 +1,3 @@ -import pytest - - -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_result(generate_qeapp_workchain): import plotly.graph_objects as go @@ -35,7 +31,6 @@ def test_result(generate_qeapp_workchain): ) -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_structure_1d(generate_qeapp_workchain, generate_structure_data): structure = generate_structure_data("silicon", pbc=(True, False, False)) wkchain = generate_qeapp_workchain(structure=structure) @@ -45,7 +40,6 @@ def test_structure_1d(generate_qeapp_workchain, generate_structure_data): assert wkchain.inputs.bands.bands.bands_kpoints.labels == [(0, "Γ"), (9, "X")] -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_structure_2d(generate_qeapp_workchain, generate_structure_data): structure = generate_structure_data("MoS2", pbc=(True, True, False)) wkchain = generate_qeapp_workchain(structure=structure) diff --git a/tests/test_plugins_electronic_structure.py b/tests/test_plugins_electronic_structure.py index 3e3a54fa8..f25351563 100644 --- a/tests/test_plugins_electronic_structure.py +++ b/tests/test_plugins_electronic_structure.py @@ -1,7 +1,3 @@ -import pytest - - -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_electronic_structure(generate_qeapp_workchain): """Test the electronic structure tab.""" import time diff --git a/tests/test_plugins_pdos.py b/tests/test_plugins_pdos.py index 0d20a0952..3f04847a8 100644 --- a/tests/test_plugins_pdos.py +++ b/tests/test_plugins_pdos.py @@ -1,7 +1,3 @@ -import pytest - - -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_result(generate_qeapp_workchain): import plotly.graph_objects as go diff --git a/tests/test_plugins_xas.py b/tests/test_plugins_xas.py index 17ca7edf3..b2567dd17 100644 --- a/tests/test_plugins_xas.py +++ b/tests/test_plugins_xas.py @@ -1,9 +1,6 @@ -import pytest - from aiidalab_qe.app.main import App -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_settings(submit_app_generator): """Test the settings of the xas app.""" app: App = submit_app_generator(properties=["xas"]) diff --git a/tests/test_plugins_xps.py b/tests/test_plugins_xps.py index 2602f1222..3ffd0cd2d 100644 --- a/tests/test_plugins_xps.py +++ b/tests/test_plugins_xps.py @@ -1,9 +1,6 @@ -import pytest - from aiidalab_qe.app.configuration.model import ConfigurationModel -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_settings(): """Test the settings of the xps app.""" diff --git a/tests/test_pseudo.py b/tests/test_pseudo.py index 420eb37d4..166fcecbf 100644 --- a/tests/test_pseudo.py +++ b/tests/test_pseudo.py @@ -142,7 +142,6 @@ def test_download_and_install_pseudo_from_file(tmp_path): assert len(pseudos_to_install()) == 10 -@pytest.mark.usefixtures("aiida_profile_clean", "sssp", "pseudodojo") def test_pseudos_settings(generate_structure_data, generate_upf_data): from aiidalab_qe.app.configuration.advanced.pseudos import ( PseudoSettings, diff --git a/tests/test_result.py b/tests/test_result.py index f87051315..6f5ec0523 100644 --- a/tests/test_result.py +++ b/tests/test_result.py @@ -1,32 +1,26 @@ -import pytest - from aiidalab_qe.app.main import App -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_result_step(app_to_submit, generate_qeapp_workchain): """Test the result step is properly updated when the process is running.""" - app: App = app_to_submit step = app.results_step app.results_model.process = generate_qeapp_workchain().node.uuid assert step.state == step.State.ACTIVE -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_kill_and_clean_buttons(app_to_submit, generate_qeapp_workchain): """Test the kill and clean_scratch button are properly displayed when the process is in different states.""" - step = app_to_submit.results_step + step.render() model = app_to_submit.results_model model.process = generate_qeapp_workchain().node.uuid assert step.kill_button.layout.display == "block" assert step.clean_scratch_button.layout.display == "none" -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_workchainview(generate_qeapp_workchain): """Test the result tabs are properly updated""" import time @@ -42,7 +36,6 @@ def test_workchainview(generate_qeapp_workchain): assert wcv.result_tabs._titles["1"] == "Final Geometry" # type: ignore -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_summary_report(data_regression, generate_qeapp_workchain): """Test the summary report can be properly generated.""" from aiidalab_qe.app.result.summary_viewer import SummaryView @@ -54,20 +47,18 @@ def test_summary_report(data_regression, generate_qeapp_workchain): data_regression.check(report) -# @pytest.mark.usefixtures("aiida_profile_clean", "sssp") -# def test_summary_report_advanced_settings(data_regression, generate_qeapp_workchain): -# """Test advanced settings are properly reported""" -# from aiidalab_qe.app.result.summary_viewer import SummaryView +def test_summary_report_advanced_settings(data_regression, generate_qeapp_workchain): + """Test advanced settings are properly reported""" + from aiidalab_qe.app.result.summary_viewer import SummaryView -# wkchain = generate_qeapp_workchain( -# spin_type="collinear", electronic_type="metal", initial_magnetic_moments=0.1 -# ) -# viewer = SummaryView(wkchain.node) -# report = viewer.report -# assert report["initial_magnetic_moments"]["Si"] == 0.1 + wkchain = generate_qeapp_workchain( + spin_type="collinear", electronic_type="metal", initial_magnetic_moments=0.1 + ) + viewer = SummaryView(wkchain.node) + report = viewer.report + assert report["initial_magnetic_moments"]["Si"] == 0.1 -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_summary_view(generate_qeapp_workchain): """Test the report html can be properly generated.""" from bs4 import BeautifulSoup diff --git a/tests/test_submit_qe_workchain.py b/tests/test_submit_qe_workchain.py index cfa6afe57..7d183e41b 100644 --- a/tests/test_submit_qe_workchain.py +++ b/tests/test_submit_qe_workchain.py @@ -1,9 +1,6 @@ -import pytest - from aiidalab_qe.app.main import App -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_create_builder_default( data_regression, submit_app_generator, @@ -15,7 +12,7 @@ def test_create_builder_default( app: App = submit_app_generator(properties=["bands", "pdos"]) - parameters = app.submit_model._get_submission_parameters() + parameters = app.submit_model.get_model_state() app.submit_model._create_builder(parameters) # since uuid is specific to each run, we remove it from the output ui_parameters = remove_uuid_fields(parameters) @@ -27,7 +24,6 @@ def test_create_builder_default( # In the future, we will check the builder parameters using regresion test -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_create_process_label(submit_app_generator): """Test the creation of the correct process label.""" app: App = submit_app_generator(properties=["bands", "pdos"]) @@ -53,7 +49,6 @@ def test_create_process_label(submit_app_generator): ) -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_create_builder_insulator( submit_app_generator, ): @@ -65,7 +60,7 @@ def test_create_builder_insulator( app: App = submit_app_generator( electronic_type="insulator", properties=["bands", "pdos"] ) - parameters = app.submit_model._get_submission_parameters() + parameters = app.submit_model.get_model_state() builder = app.submit_model._create_builder(parameters) # check and validate the builder @@ -78,7 +73,6 @@ def test_create_builder_insulator( assert "smearing" not in got["bands"]["bands"]["scf"]["pw"]["parameters"]["SYSTEM"] -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_create_builder_advanced_settings( submit_app_generator, ): @@ -102,7 +96,7 @@ def test_create_builder_advanced_settings( electron_maxstep=100, properties=["bands", "pdos"], ) - parameters = app.submit_model._get_submission_parameters() + parameters = app.submit_model.get_model_state() builder = app.submit_model._create_builder(parameters) # check and validate the builder @@ -129,7 +123,6 @@ def test_create_builder_advanced_settings( ) -@pytest.mark.usefixtures("aiida_profile_clean", "sssp") def test_warning_messages( generate_structure_data, submit_app_generator, @@ -150,32 +143,31 @@ def test_warning_messages( } app: App = submit_app_generator(properties=["bands", "pdos"]) - submit_step = app.submit_step submit_model = app.submit_model - pw_code = submit_model.get_code("dft", "pw").get_setup_widget() - pw_code.num_cpus.value = 1 + pw_code = submit_model.get_code("dft", "pw") + pw_code.num_cpus = 1 submit_model.check_resources() # no warning: - assert submit_step.submission_warning_messages.value == "" + assert submit_model.submission_warning_messages == "" # now we increase the resources, so we should have the Warning-3 - pw_code.num_cpus.value = len(os.sched_getaffinity(0)) + pw_code.num_cpus = len(os.sched_getaffinity(0)) submit_model.check_resources() for suggestion in ["avoid_overloading", "go_remote"]: - assert suggestions[suggestion] in submit_step.submission_warning_messages.value + assert suggestions[suggestion] in submit_model.submission_warning_messages # now we use a large structure, so we should have the Warning-1 (and 2 if not on localhost) structure = generate_structure_data("H2O-larger") submit_model.input_structure = structure - pw_code.num_cpus.value = 1 + pw_code.num_cpus = 1 submit_model.check_resources() num_sites = len(structure.sites) volume = structure.get_cell_volume() estimated_CPUs = submit_model._estimate_min_cpus(num_sites, volume) assert estimated_CPUs == 2 for suggestion in ["more_resources", "change_configuration"]: - assert suggestions[suggestion] in submit_step.submission_warning_messages.value + assert suggestions[suggestion] in submit_model.submission_warning_messages def builder_to_readable_dict(builder):