Skip to content
12 changes: 5 additions & 7 deletions src/aiidalab_qe/app/configuration/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@

from aiidalab_qe.app.parameters import DEFAULT_PARAMETERS
from aiidalab_qe.app.utils import get_entry_items
from aiidalab_qe.app.utils.plugin_manager import (
DEFAULT_PLUGIN_CONFIG_SOURCE,
PluginManager,
is_package_installed,
)
from aiidalab_qe.common.infobox import InAppGuide
from aiidalab_qe.common.panel import (
ConfigurationSettingsModel,
Expand Down Expand Up @@ -106,7 +111,6 @@ def _render(self):
description="Plugin store",
link="plugin_manager.ipynb",
icon="puzzle-piece", # More intuitive icon
button_style="primary", # Keeps it prominent
tooltip="Browse and install additional plugins from the Plugin Store",
)

Expand Down Expand Up @@ -211,12 +215,6 @@ def _update_state(self, _=None):
self.state = self.State.INIT

def _fetch_not_installed_property(self, plugin_config_source=None):
from aiidalab_qe.app.utils.plugin_manager import (
DEFAULT_PLUGIN_CONFIG_SOURCE,
PluginManager,
is_package_installed,
)

self.not_installed_property_children = []

plugin_config_source = plugin_config_source or DEFAULT_PLUGIN_CONFIG_SOURCE
Expand Down
4 changes: 3 additions & 1 deletion src/aiidalab_qe/app/configuration/advanced/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ def set_model_state(self, parameters):
pseudo_family = PseudoFamily.from_string(pseudo_family_string)
library = pseudo_family.library
accuracy = pseudo_family.accuracy
pseudos.functional = pseudo_family.functional
pseudos.library = f"{library} {accuracy}"
pseudos.family = pseudo_family_string
else:
Expand All @@ -176,11 +177,12 @@ def set_model_state(self, parameters):
pseudos.family = None
pseudos.show_upload_warning = True

pseudos.functionals = [pseudos.functional] * len(pseudos.dictionary)

if "pseudos" in parameters["pw"]:
pseudos.dictionary = parameters["pw"]["pseudos"]
pseudos.ecutwfc = parameters["pw"]["parameters"]["SYSTEM"]["ecutwfc"]
pseudos.ecutrho = parameters["pw"]["parameters"]["SYSTEM"]["ecutrho"]
pseudos.functionals = [pseudos.functional] * len(pseudos.dictionary)

convergence = t.cast(
ConvergenceConfigurationSettingsModel,
Expand Down
5 changes: 1 addition & 4 deletions src/aiidalab_qe/app/result/components/viewer/viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,4 @@ def _fetch_plugin_results(self, viewer_model: WorkChainResultsViewerModel):
panel = entry["panel"]
model = entry["model"]()
viewer_model.add_model(identifier, model)
self.panels[identifier] = panel(
identifier=identifier,
model=model,
)
self.panels[identifier] = panel(model=model)
17 changes: 10 additions & 7 deletions src/aiidalab_qe/app/submission/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ def __init__(self, model: SubmissionStepModel, auto_setup=True, **kwargs):
"input_parameters",
)

global_resources_model = GlobalResourceSettingsModel()
global_resources_model = GlobalResourceSettingsModel(
default_codes=DEFAULT["codes"]
)
self.global_resources = GlobalResourceSettingsPanel(
model=global_resources_model
)
Expand Down Expand Up @@ -81,16 +83,16 @@ def _render(self):
super()._render()

self.process_label = ipw.Text(
description="Label:",
layout=ipw.Layout(width="auto", indent="0px"),
description="Label",
layout=ipw.Layout(width="auto"),
)
ipw.link(
(self._model, "process_label"),
(self.process_label, "value"),
)
self.process_description = ipw.Textarea(
description="Description",
layout=ipw.Layout(width="auto", indent="0px"),
layout=ipw.Layout(width="auto"),
)
ipw.link(
(self._model, "process_description"),
Expand Down Expand Up @@ -205,7 +207,6 @@ def _on_qe_installed(self, _):
self._toggle_qe_installation_widget()
if self._model.qe_installed:
self._model.update()
self._refresh_resources()

def _set_up_qe(self, auto_setup):
self.qe_setup = QESetupWidget(auto_start=False)
Expand Down Expand Up @@ -272,7 +273,9 @@ def _fetch_plugin_resource_settings(self):
if key not in resources:
raise ValueError(f"Entry {identifier} is missing the '{key}' key")

model: PluginResourceSettingsModel = resources["model"]()
model: PluginResourceSettingsModel = resources["model"](
default_codes=DEFAULT["codes"]
)
model.observe(
self._on_plugin_overrides_change,
"override",
Expand All @@ -292,4 +295,4 @@ def _fetch_plugin_resource_settings(self):

codes[identifier] = dict(model.get_models())

self.global_resources.set_up_codes(codes)
self.global_resources.build_global_codes(codes)
13 changes: 8 additions & 5 deletions src/aiidalab_qe/app/submission/global_settings/model.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import numpy as np
import traitlets as tl

from aiida import orm
Expand Down Expand Up @@ -208,6 +209,9 @@ def _get_properties(self) -> list[str]:
return self.input_parameters.get("workchain", {}).get("properties", [])

def _check_blockers(self):
if not self.input_parameters:
return

warning_in_blockers = (
"""
<b>Please note</b> that, if this blocker is shown for an already finished workflow,
Expand Down Expand Up @@ -237,13 +241,13 @@ def _check_blockers(self):
if not code_model.is_ready:
blocker = True
yield message.format(
property=name,
property=identifier,
code=code_model.description,
)

# in the test, we need to consider one more blocker,
# even if empty:
if blocker:
if blocker and warning_in_blockers:
yield (warning_in_blockers)

# Check if the QEAppComputationalResourcesWidget is used
Expand All @@ -252,7 +256,8 @@ def _check_blockers(self):
if not code_model.is_ready:
continue
if not issubclass(
code_model.code_widget_class, QEAppComputationalResourcesWidget
code_model.code_widget_class,
QEAppComputationalResourcesWidget,
):
yield (
f"Error: hi, plugin developer, please use the QEAppComputationalResourcesWidget from aiidalab_qe.common.widgets for code {identifier}."
Expand Down Expand Up @@ -306,8 +311,6 @@ def _estimate_min_cpus(
`int`
The estimated minimum number of CPUs required.
"""
import numpy as np

return int(
np.ceil(
num_cpus0
Expand Down
27 changes: 15 additions & 12 deletions src/aiidalab_qe/app/submission/global_settings/setting.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,10 @@ def render(self):
if code_model.is_active:
self._toggle_code(code_model)

def set_up_codes(self, codes: PluginCodes):
def build_global_codes(self, codes: PluginCodes):
for identifier, code_models in codes.items():
for _, code_model in code_models.items():
self.register_code_trait_callbacks(code_model)
base_code_model = self._model.add_global_model(identifier, code_model)
if base_code_model is not None:
base_code_model.observe(
Expand All @@ -73,13 +74,26 @@ def set_up_codes(self, codes: PluginCodes):
self._on_code_selection_change,
"selected",
)
self.register_code_trait_callbacks(base_code_model)
if base_code_model.default_calc_job_plugin == "quantumespresso.pw":
base_code_model.observe(
self._on_pw_code_resource_change,
[
"num_cpus",
"num_nodes",
"ntasks_per_node",
"cpus_per_task",
"max_wallclock_seconds",
],
)
self._model.update_global_codes()

def reset(self):
self._model.set_selected_codes()

def _on_input_parameters_change(self, _):
self._model.update_active_codes()
self._model.update_blockers()

def _on_input_structure_change(self, _):
self._model.check_resources()
Expand All @@ -105,17 +119,6 @@ def _render_code_widget(
code_widget: QEAppComputationalResourcesWidget,
):
super()._render_code_widget(code_model, code_widget)
if code_model.default_calc_job_plugin == "quantumespresso.pw":
code_model.observe(
self._on_pw_code_resource_change,
[
"num_cpus",
"num_nodes",
"ntasks_per_node",
"cpus_per_task",
"max_wallclock_seconds",
],
)

def toggle_widget(_=None, model=code_model, widget=code_widget):
widget = self.code_widgets[model.name]
Expand Down
5 changes: 2 additions & 3 deletions src/aiidalab_qe/app/submission/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,8 @@ def _get_properties(self) -> list[str]:
def _create_builder(self, parameters) -> ProcessBuilderNamespace:
builder = QeAppWorkChain.get_builder_from_protocol(
structure=self.input_structure,
parameters=shallow_copy_nested_dict(
parameters
), # use shallow copy to avoid workflow mute these parameters.
# Use shallow copy to avoid parameter mutation by the workflow
parameters=shallow_copy_nested_dict(parameters),
)

codes = parameters["codes"]["global"]["codes"]
Expand Down
4 changes: 2 additions & 2 deletions src/aiidalab_qe/app/utils/plugin_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,8 +396,8 @@ def _build_ui(self) -> None:

# Build plugin description details
details = f"""
<b>Author:</b> {plugin_data.get('author', 'N/A')}<br>
<b>Description:</b> {plugin_data.get('description', 'No description available')}<br>
<b>Author:</b> {plugin_data.get("author", "N/A")}<br>
<b>Description:</b> {plugin_data.get("description", "No description available")}<br>
<b>Status:</b> {badge_html}<br>
{version_message}
"""
Expand Down
17 changes: 15 additions & 2 deletions src/aiidalab_qe/common/code/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ class CodeModel(Model):
allow_hidden_codes = tl.Bool(False)
allow_disabled_computers = tl.Bool(False)
override = tl.Bool(False)
warning = tl.Unicode(allow_none=True)

_WARNING_TEMPLATE = "<span style='color: red;'>{warning}</span>"

def __init__(
self,
Expand Down Expand Up @@ -59,10 +62,20 @@ def activate(self):
def deactivate(self):
self.is_active = False

def update(self, user_email="", refresh=False):
def update(self, user_email="", default_code=None, refresh=False):
if not self.options or refresh:
self.options = self._get_codes(user_email)
self.selected = self.first_option
if default_code:
try:
selected = orm.load_code(default_code).uuid
except NotExistent:
selected = None
self.warning = self._WARNING_TEMPLATE.format(
warning=f"Code '{default_code}' not found"
)
else:
selected = self.first_option
self.selected = selected

def get_model_state(self) -> dict:
return {
Expand Down
63 changes: 41 additions & 22 deletions src/aiidalab_qe/common/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,18 +213,29 @@ class ResourceSettingsModel(SettingsModel, HasModels[CodeModel]):
warning_messages = tl.Unicode("")

def __init__(self, *args, **kwargs):
self.default_codes: dict[str, dict] = kwargs.pop("default_codes", {})

super().__init__(*args, **kwargs)

# Used by the code-setup thread to fetch code options
self.DEFAULT_USER_EMAIL = orm.User.collection.get_default().email

def add_model(self, identifier, model):
super().add_model(identifier, model)
model.update(self.DEFAULT_USER_EMAIL)
code_key = model.default_calc_job_plugin.split(".")[-1]
model.update(
self.DEFAULT_USER_EMAIL,
default_code=self.default_codes.get(code_key, {}).get("code"),
)

def refresh_codes(self):
for _, code_model in self.get_models():
code_model.update(self.DEFAULT_USER_EMAIL, refresh=True)
code_key = code_model.default_calc_job_plugin.split(".")[-1]
code_model.update(
self.DEFAULT_USER_EMAIL,
default_code=self.default_codes.get(code_key, {}).get("code"),
refresh=True,
)

def get_model_state(self):
return {
Expand All @@ -245,9 +256,9 @@ def get_selected_codes(self) -> dict[str, dict]:
if code_model.is_ready
}

def set_selected_codes(self, code_data=DEFAULT["codes"]):
def set_selected_codes(self, code_data=None):
for identifier, code_model in self.get_models():
if identifier in code_data:
if identifier in (code_data or self.default_codes):
code_model.set_model_state(code_data[identifier])

def _check_blockers(self):
Expand All @@ -264,6 +275,28 @@ def __init__(self, model, **kwargs):
super().__init__(model, **kwargs)
self.code_widgets = {}

def register_code_trait_callbacks(self, code_model: CodeModel):
"""Registers event handlers on code model traits."""
if code_model.default_calc_job_plugin == "quantumespresso.pw":
code_model.observe(
self._on_code_resource_change,
[
"parallelization_override",
"npool",
],
)
code_model.observe(
self._on_code_resource_change,
[
"selected",
"num_cpus",
"num_nodes",
"ntasks_per_node",
"cpus_per_task",
"max_wallclock_seconds",
],
)

def _on_code_resource_change(self, _):
pass

Expand Down Expand Up @@ -301,6 +334,10 @@ def _render_code_widget(
(code_model, "options"),
(code_widget.code_selection.code_select_dropdown, "options"),
)
ipw.link(
(code_model, "warning"),
(code_widget.code_selection.output, "value"),
)
ipw.link(
(code_model, "selected"),
(code_widget, "value"),
Expand Down Expand Up @@ -334,24 +371,6 @@ def _render_code_widget(
(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,
[
"selected",
"num_cpus",
"num_nodes",
"ntasks_per_node",
"cpus_per_task",
"max_wallclock_seconds",
],
)
code_widget.code_selection.code_select_dropdown.observe(
self._on_code_options_change,
"options",
Expand Down
Loading