From 70a0894b46ab85b8bc343e4c18388bea527848be Mon Sep 17 00:00:00 2001 From: Jusong Yu Date: Wed, 3 Jul 2024 10:28:44 +0200 Subject: [PATCH] EXPERIMENT on solara --- LICENSE | 12 +- Procfile | 7 + README.md | 34 - aiidalab_sssp/__init__.py | 0 aiidalab_sssp/inspect/__init__.py | 329 ----- aiidalab_sssp/inspect/plot_utils.py | 84 -- aiidalab_sssp/inspect/subwidgets/__init__.py | 0 aiidalab_sssp/inspect/subwidgets/bands.py | 286 ---- .../inspect/subwidgets/convergence.py | 247 ---- aiidalab_sssp/inspect/subwidgets/delta.py | 321 ----- .../inspect/subwidgets/periodic_table.py | 225 --- aiidalab_sssp/inspect/subwidgets/plot.py | 113 -- aiidalab_sssp/inspect/subwidgets/select.py | 196 --- aiidalab_sssp/inspect/subwidgets/summary.py | 215 --- aiidalab_sssp/inspect/subwidgets/utils.py | 3 - aiidalab_sssp/parameters/__init__.py | 7 - aiidalab_sssp/parameters/ssspapp.yaml | 20 - aiidalab_sssp/process.py | 153 --- aiidalab_sssp/setup_codes.py | 375 ----- aiidalab_sssp/static/__init__.py | 0 aiidalab_sssp/static/style.css | 42 - aiidalab_sssp/static/welcome.jinja | 32 - aiidalab_sssp/steps.py | 1210 ----------------- aiidalab_sssp/version.py | 6 - inspect.ipynb | 176 --- miscellaneous/logo-sssp.png | Bin 24514 -> 0 bytes mypy.ini | 3 + pyproject.toml | 4 +- src/aiidalab_sssp/__init__.py | 4 + src/aiidalab_sssp/components/__init__.py | 2 + src/aiidalab_sssp/components/article.py | 21 + src/aiidalab_sssp/components/header.py | 6 + src/aiidalab_sssp/components/layout.py | 6 + .../components/periodic_table/__init__.py | 140 ++ .../components/periodic_table/utils.py | 166 +++ .../components/periodic_table/widget.css | 68 + .../components/periodic_table/widget.js | 256 ++++ src/aiidalab_sssp/components/viz.py | 13 + .../content/articles/equis-in-vidi.md | 85 ++ .../content/articles/substiterat-vati.md | 70 + src/aiidalab_sssp/data.py | 139 ++ src/aiidalab_sssp/pages/__init__.py | 59 + src/aiidalab_sssp/pages/explore/__init__.py | 84 ++ src/aiidalab_sssp/pages/howto/__init__.py | 27 + .../pages/verification/__init__.py | 79 ++ sssp-docs.ipynb | 176 --- start.py | 27 - verification.ipynb | 151 -- viewers.py | 99 -- 49 files changed, 1244 insertions(+), 4534 deletions(-) create mode 100644 Procfile delete mode 100644 aiidalab_sssp/__init__.py delete mode 100644 aiidalab_sssp/inspect/__init__.py delete mode 100644 aiidalab_sssp/inspect/plot_utils.py delete mode 100644 aiidalab_sssp/inspect/subwidgets/__init__.py delete mode 100644 aiidalab_sssp/inspect/subwidgets/bands.py delete mode 100644 aiidalab_sssp/inspect/subwidgets/convergence.py delete mode 100644 aiidalab_sssp/inspect/subwidgets/delta.py delete mode 100644 aiidalab_sssp/inspect/subwidgets/periodic_table.py delete mode 100644 aiidalab_sssp/inspect/subwidgets/plot.py delete mode 100644 aiidalab_sssp/inspect/subwidgets/select.py delete mode 100644 aiidalab_sssp/inspect/subwidgets/summary.py delete mode 100644 aiidalab_sssp/inspect/subwidgets/utils.py delete mode 100644 aiidalab_sssp/parameters/__init__.py delete mode 100644 aiidalab_sssp/parameters/ssspapp.yaml delete mode 100644 aiidalab_sssp/process.py delete mode 100644 aiidalab_sssp/setup_codes.py delete mode 100644 aiidalab_sssp/static/__init__.py delete mode 100644 aiidalab_sssp/static/style.css delete mode 100644 aiidalab_sssp/static/welcome.jinja delete mode 100644 aiidalab_sssp/steps.py delete mode 100644 aiidalab_sssp/version.py delete mode 100644 inspect.ipynb delete mode 100644 miscellaneous/logo-sssp.png create mode 100644 mypy.ini create mode 100644 src/aiidalab_sssp/__init__.py create mode 100644 src/aiidalab_sssp/components/__init__.py create mode 100644 src/aiidalab_sssp/components/article.py create mode 100644 src/aiidalab_sssp/components/header.py create mode 100644 src/aiidalab_sssp/components/layout.py create mode 100644 src/aiidalab_sssp/components/periodic_table/__init__.py create mode 100644 src/aiidalab_sssp/components/periodic_table/utils.py create mode 100644 src/aiidalab_sssp/components/periodic_table/widget.css create mode 100644 src/aiidalab_sssp/components/periodic_table/widget.js create mode 100644 src/aiidalab_sssp/components/viz.py create mode 100644 src/aiidalab_sssp/content/articles/equis-in-vidi.md create mode 100644 src/aiidalab_sssp/content/articles/substiterat-vati.md create mode 100644 src/aiidalab_sssp/data.py create mode 100644 src/aiidalab_sssp/pages/__init__.py create mode 100644 src/aiidalab_sssp/pages/explore/__init__.py create mode 100644 src/aiidalab_sssp/pages/howto/__init__.py create mode 100644 src/aiidalab_sssp/pages/verification/__init__.py delete mode 100644 sssp-docs.ipynb delete mode 100644 start.py delete mode 100644 verification.ipynb delete mode 100644 viewers.py diff --git a/LICENSE b/LICENSE index 436bd1d..2398e28 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -MIT License +The MIT License (MIT) -Copyright (c) 2021 The AiiDA lab Team. +Copyright (c) 2022 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,13 +9,13 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..c6010f7 --- /dev/null +++ b/Procfile @@ -0,0 +1,7 @@ +# heroku by default sets WEB_CONCURRENCY=2 +# see: https://devcenter.heroku.com/changelog-items/618 +# which uvicorn picks up, unless we explicitly set --workers --1 +# see https://www.uvicorn.org/deployment/ +# we do not support multiple workers yet +# we also need to bind to 0.0.0.0 otherwise heroku cannot route to our server +web: solara run aiidalab_sssp.pages --port=$PORT --no-open --host=0.0.0.0 --workers 1 diff --git a/README.md b/README.md index d5f7560..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,34 +0,0 @@ -# Application sssp workflow - -The aiidalab plugin to running sssp workflow - -## Installation - -This jupyter-based app is intended to run in -[AiiDA lab](https://www.materialscloud.org/aiidalab) -## Usage - -Here may go a few sreenshots / animated gifs illustrating how to use the app. - -## For maintainers - -To create a new release, clone the repository, install development dependencies with `pip install '.[dev]'`, and then execute `bumpver update`. -This will: - - 1. Create a tagged release with bumped version and push it to the repository. - 2. Trigger a GitHub actions workflow that creates a GitHub release. - -Additional notes: - - - Use the `--dry` option to preview the release change. - - The release tag (e.g. a/b/rc) is determined from the last release. - Use the `--tag` option to switch the release tag. - -## License - -MIT - - -## Contact - -📧 email: jusong.yu@epfl.ch diff --git a/aiidalab_sssp/__init__.py b/aiidalab_sssp/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/aiidalab_sssp/inspect/__init__.py b/aiidalab_sssp/inspect/__init__.py deleted file mode 100644 index dca0dcd..0000000 --- a/aiidalab_sssp/inspect/__init__.py +++ /dev/null @@ -1,329 +0,0 @@ -# -*- coding: utf-8 -*- -import json -import os -import random -from pathlib import Path - -import matplotlib.pyplot as plt -from aiida import orm -from aiida.common import AttributeDict -from monty.json import jsanitize - -DB_FOLDER = Path.home().joinpath(".cache", "SSSP") -SSSP_DB = Path.joinpath(DB_FOLDER, "sssp_db") -SSSP_LOCAL_DB = Path.joinpath(DB_FOLDER, "sssp_local_db") - -_px = 1 / plt.rcParams["figure.dpi"] # unit pixel for plot - - -def extract_element(pseudos): - try: - # Since the element are same for all, use first one - label0 = list(pseudos.keys())[0] - element = label0.split(".")[0] - except Exception: - element = None - - return element - - -def get_conf_list(element): - """get configuration list from element""" - from aiida_sssp_workflow.utils import ( - OXIDE_CONFIGURATIONS, - RARE_EARTH_ELEMENTS, - UNARIE_CONFIGURATIONS, - ) - - if element == "O": - return UNARIE_CONFIGURATIONS + ["TYPICAL"] - - if element in RARE_EARTH_ELEMENTS: - return OXIDE_CONFIGURATIONS + ["RE"] - - return OXIDE_CONFIGURATIONS + UNARIE_CONFIGURATIONS + ["TYPICAL"] - - -def parse_label(label): - """parse label to dict of pseudo info""" - element, type, z, tool, family, *version = label.split(".") - version = ".".join(version) - - if type == "nc": - full_type = "NC" - if type == "us": - full_type = "Ultrasoft" - if type == "paw": - full_type = "PAW" - - return { - "element": element, - "type": type, - "z": z, - "tool": tool, - "family": family, - "version": version, - "representive_label": f"{z}|{full_type}|{family}|{tool}|{version}", - "concise_label": f"{z}|{type}|{family}|{version}", - "full_label": f"{element}|{z}|{full_type}|{family}|{tool}|{version}", - } - - -def lighten_color(color, amount=0.5): - """ - Lightens the given color by multiplying (1-luminosity) by the given amount. - Input can be matplotlib color string, hex string, or RGB tuple. - REF from https://stackoverflow.com/questions/37765197/darken-or-lighten-a-color-in-matplotlib - - Examples: - >> lighten_color('g', 0.3) - >> lighten_color('#F034A3', 0.6) - >> lighten_color((.3,.55,.1), 0.5) - """ - import colorsys - - import matplotlib.colors as mc - - try: - c = mc.cnames[color] - except Exception: - c = color - c = colorsys.rgb_to_hls(*mc.to_rgb(c)) - return colorsys.hls_to_rgb(c[0], 1 - amount * (1 - c[1]), c[2]) - - -def cmap(pseudo_info: dict) -> str: - """Return RGB string of color for given pseudo info - Hardcoded at the momment. - """ - if pseudo_info["family"] == "sg15" and pseudo_info["version"] == "v0": - return "#000000" - - if pseudo_info["family"] == "gbrv": - return "#4682B4" - - if ( - pseudo_info["family"] == "psl" - and pseudo_info["type"] == "us" - and pseudo_info["version"] == "v1.0.0-high" - ): - return "#F50E02" - - if ( - pseudo_info["family"] == "psl" - and pseudo_info["type"] == "us" - and pseudo_info["version"] == "v1.0.0-low" - ): - return lighten_color("#F50E02") - - if ( - pseudo_info["family"] == "psl" - and pseudo_info["type"] == "paw" - and pseudo_info["version"] == "v1.0.0-high" - ): - return "#008B00" - - if ( - pseudo_info["family"] == "psl" - and pseudo_info["type"] == "paw" - and pseudo_info["version"] == "v1.0.0-low" - ): - return lighten_color("#008B00") - - if ( - pseudo_info["family"] == "psl" - and pseudo_info["type"] == "paw" - and "v0." in pseudo_info["version"] - ): - return "#FF00FF" - - if ( - pseudo_info["family"] == "psl" - and pseudo_info["type"] == "us" - and "v0." in pseudo_info["version"] - ): - return lighten_color("#FF00FF") - - if pseudo_info["family"] == "dojo" and pseudo_info["version"] == "v4-str": - return "#F9A501" - - if pseudo_info["family"] == "dojo" and pseudo_info["version"] == "v4-std": - return lighten_color("#F9A501") - - if pseudo_info["family"] == "jth" and pseudo_info["version"] == "v1.1-str": - return "#00C5ED" - - if pseudo_info["family"] == "jth" and pseudo_info["version"] == "v1.1-std": - return lighten_color("#00C5ED") - - # TODO: more mapping - # if a unknow type generate random color based on ascii sum - ascn = sum([ord(c) for c in pseudo_info["representive_label"]]) - random.seed(ascn) - return "#%06x" % random.randint(0, 0xFFFFFF) - - -def dump_to_sssp_local_db(node): - """dump node to the local db - - element summary - - bands - - band structure. - """ - _node = node - label = _node.extras.get("label").split()[ - -1 - ] # do not contain the extra machine info - element = label.split(".")[0] - - json_fn = f"{element}.json" - Path(os.path.join(SSSP_LOCAL_DB, "bands", element)).mkdir( - parents=True, exist_ok=True - ) - Path(os.path.join(SSSP_LOCAL_DB, "band_structure", element)).mkdir( - parents=True, exist_ok=True - ) - - assert f"{label}.upf" == _node.inputs.pseudo.filename - - res_json = os.path.join(SSSP_LOCAL_DB, json_fn) - if os.path.exists(res_json): - with open(res_json, "r") as fh: - curated_result = json.load(fh) - else: - curated_result = {} # all results of pseudos of the elements - - psp_result = { - "_metadata": [get_metadata(_node)], - } # the results of one verification - psp_result["accuracy"] = {} - psp_result["convergence"] = {} - - for called_wf in _node.called: - if called_wf.process_label == "parse_pseudo_info": - psp_result["pseudo_info"] = { - **called_wf.outputs.result.get_dict(), - } - # delta - if called_wf.process_label == "DeltaMeasureWorkChain": - psp_result["accuracy"]["delta"] = _flatten_output( - _node.outputs.accuracy.delta - ) - psp_result["accuracy"]["delta"]["_metadata"] = get_metadata(called_wf) - # bands - if called_wf.process_label == "BandsMeasureWorkChain": - psp_result["accuracy"]["bands"] = { - "bands": f"bands/{element}/{label}.json", - "band_structure": f"band_structure/{element}/{label}.json", - } - psp_result["accuracy"]["bands"]["_metadata"] = get_metadata(called_wf) - - with open( - os.path.join(SSSP_LOCAL_DB, "bands", element, f"{label}.json"), "w" - ) as fh: - bands = called_wf.outputs.bands - json.dump( - export_bands_data(bands.band_structure, bands.band_parameters), fh - ) - with open( - os.path.join(SSSP_LOCAL_DB, "band_structure", element, f"{label}.json"), - "w", - ) as fh: - bands = called_wf.outputs.band_structure - json.dump( - export_bands_structure(bands.band_structure, bands.band_parameters), - fh, - ) - - # convergence - for k, v in process_prop_label_mapping.items(): - if called_wf.process_label == v: - try: - output_res = _flatten_output(_node.outputs.convergence[k]) - except KeyError: - # run but not finished therefore no output node - output_res = {"message": "error"} - psp_result["convergence"][k] = output_res - psp_result["convergence"][k]["_metadata"] = get_metadata(called_wf) - - curated_result[f"{label}"] = psp_result - - with open(os.path.join(SSSP_LOCAL_DB, json_fn), "w") as fh: - json.dump(dict(curated_result), fh, indent=2, sort_keys=True, default=str) - - return psp_result - - -def get_metadata(node): - return { - "uuid": node.uuid, - "ctime": node.ctime, - "_aiida_hash": node.get_hash(), - } - - -def export_bands_structure(band_structure, band_parameters): - data = json.loads(band_structure._exportcontent("json", comments=False)[0]) - data["fermi_level"] = band_parameters["fermi_energy"] - data["number_of_electrons"] = band_parameters["number_of_electrons"] - data["number_of_bands"] = band_parameters["number_of_bands"] - - return jsanitize(data) - - -def export_bands_data(band_structure: orm.BandsData, band_parameters: orm.Dict): - bands_arr = band_structure.get_bands() - kpoints_arr, weights_arr = band_structure.get_kpoints(also_weights=True) - - data = { - "fermi_level": band_parameters["fermi_energy"], - "number_of_electrons": band_parameters["number_of_electrons"], - "number_of_bands": band_parameters["number_of_bands"], - "bands": bands_arr.tolist(), - "kpoints": kpoints_arr.tolist(), # TODO: using override JSON encoder - "weights": weights_arr.tolist(), - } - - return data - - -def _flatten_output(attr_dict, skip: list = lambda: []): - """ - flaten output dict node - node_collection is a list to accumulate the nodes that not unfolded - - :param skip: is a list of keys (format with parent_key.key) of Dict name that - will not collected into the json file. - - For output nodes not being expanded, write down the uuid and datatype for future query. - """ - # do_not_unfold = ["band_parameters", "scf_parameters", "seekpath_parameters"] - - for key, value in attr_dict.items(): - if key in skip: - continue - - if isinstance(value, AttributeDict): - # keep on unfold if it is a namespace - _flatten_output(value, skip) - elif isinstance(value, orm.Dict): - attr_dict[key] = value.get_dict() - elif isinstance(value, orm.Int): - attr_dict[key] = value.value - else: - # node type not handled attach uuid - attr_dict[key] = { - "uuid": value.uuid, - "datatype": type(value), - } - - # print(archive_uuids) - return attr_dict - - -process_prop_label_mapping = { - "cohesive_energy": "ConvergenceCohesiveEnergyWorkChain", - "phonon_frequencies": "ConvergencePhononFrequenciesWorkChain", - "pressure": "ConvergencePressureWorkChain", - "bands": "ConvergenceBandsWorkChain", - "delta": "ConvergenceDeltaWorkChain", -} diff --git a/aiidalab_sssp/inspect/plot_utils.py b/aiidalab_sssp/inspect/plot_utils.py deleted file mode 100644 index ca1c4bd..0000000 --- a/aiidalab_sssp/inspect/plot_utils.py +++ /dev/null @@ -1,84 +0,0 @@ -import matplotlib.pyplot as plt - -from aiidalab_sssp.inspect import cmap - -CONFIGURATIONS = [ - "BCC", - "FCC", - "SC", - "Diamond", - "XO", - "X2O", - "XO3", - "X2O", - "X2O3", - "X2O5", -] - - -def convergence(pseudos: dict, wf_name, measure_name, ylabel, threshold=None): - - px = 1 / plt.rcParams["figure.dpi"] - fig, (ax1, ax2) = plt.subplots( - 1, 2, gridspec_kw={"width_ratios": [2, 1]}, figsize=(960 * px, 360 * px) - ) - - for label, output in pseudos.items(): - # Calculate the avg delta measure value - lst = [] - for configuration in CONFIGURATIONS: - try: - res = output["delta_measure"]["output_parameters"][f"{configuration}"] - lst.append(res["nu"]) - except Exception: - pass - - avg_delta = sum(lst) / len(lst) - - try: - res = output[wf_name] - x_wfc = res["output_parameters_wfc_test"]["ecutwfc"] - y_wfc = res["output_parameters_wfc_test"][measure_name] - - x_rho = res["output_parameters_rho_test"]["ecutrho"] - y_rho = res["output_parameters_rho_test"][measure_name] - - wfc_cutoff = res["final_output_parameters"]["wfc_cutoff"] - - _, pp_family, pp_z, pp_type, pp_version = label.split("/")[0:5] - out_label = f"{pp_z}/{pp_type}(ν={avg_delta:.2f})({pp_family}-{pp_version})" - - ax1.plot(x_wfc, y_wfc, marker="o", color=cmap(label), label=out_label) - ax2.plot( - x_rho, - y_rho, - marker="o", - color=cmap(label), - label=f"cutoff wfc = {wfc_cutoff} Ry", - ) - except Exception: - raise - - ax1.set_ylabel(ylabel) - ax1.set_xlabel("Wavefuntion cutoff (Ry)") - ax1.set_title( - "Fixed rho cutoff at 200 * dual (dual=4 for NC and dual=8 for non-NC)" - ) - - # ax2.legend(loc='upper left', bbox_to_anchor=(1, 1.0)) - ax1.legend() - - ax2.set_xlabel("Charge density cudoff (Ry)") - ax2.set_title("Convergence test at fixed wavefunction cutoff") - ax2.legend() - - if threshold: - ax1.axhline(y=threshold, color="r", linestyle="--") - ax2.axhline(y=threshold, color="r", linestyle="--") - - ax1.set_ylim(-0.5 * threshold, 10 * threshold) - ax2.set_ylim(-0.5 * threshold, 10 * threshold) - - plt.tight_layout() - - return fig diff --git a/aiidalab_sssp/inspect/subwidgets/__init__.py b/aiidalab_sssp/inspect/subwidgets/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/aiidalab_sssp/inspect/subwidgets/bands.py b/aiidalab_sssp/inspect/subwidgets/bands.py deleted file mode 100644 index 020103e..0000000 --- a/aiidalab_sssp/inspect/subwidgets/bands.py +++ /dev/null @@ -1,286 +0,0 @@ -import itertools -import json -import os -from pathlib import Path - -import ipywidgets as ipw -import matplotlib.pyplot as plt -import numpy as np -import traitlets -from aiida_sssp_workflow.calculations.calculate_bands_distance import get_bands_distance -from aiida_sssp_workflow.utils import MAGNETIC_ELEMENTS, NONMETAL_ELEMENTS -from IPython.display import clear_output, display -from widget_bandsplot import BandsPlotWidget - -from aiidalab_sssp.inspect import SSSP_DB, _px, extract_element, parse_label - -# from aiidalab_sssp.inspect.band_util import get_bands_distance - -_DEGAUSS = 0.045 -_RY_TO_EV = 13.6056980659 -_FERMI_SHIFT = 10.0 # eV in protocol FIXME also change title of plot Tab widget - -_SMEARING_WIDTH = _DEGAUSS * _RY_TO_EV - - -def _bandview(json_path): - """ - return bands data can directly use for bands plot widget - - :param json_path: the path to json file - """ - try: - with open(json_path, "r") as fh: - data = json.load(fh) - except Exception: - # the bands file not exist - data = None - - return data - - -class BandStructureWidget(ipw.VBox): - """ - widget for band structure representation. When pseudos set the dropdown enabled - for choosing pseudos for compare in one frame. - Two dropdown for select two pseudos, but one is acceptable. If two pseudos are same - raise warning using StatusHTML ask to select new one. - """ - - pseudos = traitlets.Dict(allow_none=True) - - def __init__(self): - self.band_structure = ipw.Output() - self.pseudo1_select = ipw.Dropdown() - self.pseudo2_select = ipw.Dropdown() - - self.pseudo1_select.observe(self._on_pseudo_select) - self.pseudo2_select.observe(self._on_pseudo_select) - - super().__init__( - children=[ - ipw.HTML("

Band Structure

"), - ipw.HBox(children=[self.pseudo1_select, self.pseudo2_select]), - self.band_structure, - ], - ) - - @traitlets.observe("pseudos") - def _on_pseeudos_change(self, change): - if change["new"]: - self.layout.visibility = "visible" - self.pseudo1_select.options = ["None"] + list(self.pseudos.keys()) - self.pseudo2_select.options = ["None"] + list(self.pseudos.keys()) - # The first bands default select the first pseudo - self.pseudo1_select.value = list(self.pseudos.keys())[0] - else: - self.layout.visibility = "hidden" - - def _on_pseudo_select(self, _): - pseudo1_label = self.pseudo1_select.value - pseudo1 = self.pseudos.get(pseudo1_label, None) - pseudo2_label = self.pseudo2_select.value - pseudo2 = self.pseudos.get(pseudo2_label, None) - - bands = [] - if pseudo1: - try: - path = pseudo1["accuracy"]["bands"]["band_structure"] - json_path = Path.joinpath(SSSP_DB, path) - except Exception: - return - - bandsdata_a = self.bands_align_to_fermi(_bandview(json_path)) - bands.append(bandsdata_a) - - if pseudo2: - try: - path = pseudo2["accuracy"]["bands"]["band_structure"] - json_path = Path.joinpath(SSSP_DB, path) - except Exception: - return - - bandsdata_b = self.bands_align_to_fermi(_bandview(json_path)) - bands.append(bandsdata_b) - - _band_structure_preview = BandsPlotWidget( - bands=bands, - energy_range={"ymin": -10.0, "ymax": 15.0}, - fermi_energy=0.0, # since we have aligned to fermi level - ) - - with self.band_structure: - clear_output(wait=True) - display(_band_structure_preview) - - def bands_align_to_fermi(self, bandsdata): - """ - align the band structure to fermi level - """ - fermi_energy = bandsdata["fermi_level"] - - for path in bandsdata["paths"]: - values = [[y - fermi_energy for y in ys] for ys in path["values"]] - path["values"] = values - - # After align to fermi level, we need to update the fermi level to 0.0 - bandsdata["fermi_level"] = 0.0 - - return bandsdata - - -class BandChessboard(ipw.VBox): - """Band distance compare in chess board""" - - pseudos = traitlets.Dict(allow_none=True) - - def __init__(self): - self.chessboard = ipw.Output() - - # for caching - self.__cache_bands = {} - - super().__init__( - children=[ - ipw.HTML("

Accuracy: Bands distance chessboard

"), - self.chessboard, - ], - ) - - @traitlets.observe("pseudos") - def _on_pseudos_change(self, change): - if change["new"]: - self.layout.visibility = "visible" - self._render() - else: - self.layout.visibility = "hidden" - - def _render(self): - """render bands chessboard side by side eta_v and eta_10""" - _MAX_NUM = 8 - if len(self.pseudos) > _MAX_NUM: - # Since not well render to notebook, only take 8 entities - pseudos = {k: self.pseudos[k] for k in list(self.pseudos)[:_MAX_NUM]} - # !!! FIXME: MUST raise warning here to ask user to toggle for more to show - else: - pseudos = {k: self.pseudos[k] for k in list(self.pseudos)[:]} # all pseudos - - output = self.chessboard - labels, arr_v, arr_c = self._bands_distance(pseudos) - fig, (ax_v, ax_c) = plt.subplots( - 1, - 2, - gridspec_kw={"wspace": 0.02, "hspace": 0}, - figsize=(1020 * _px, 680 * _px), - ) - fig.canvas.header_visible = False - self._render_plot(ax_v, ax_c, arr_v=arr_v, arr_c=arr_c, labels=labels) - - with output: - clear_output(wait=True) - display(fig.canvas) - - @staticmethod - def _render_plot(ax_v, ax_c, arr_v, arr_c, labels): - # label to concise label - labels = [parse_label(i)["concise_label"] for i in labels] - - for idx, (ax, arr, title) in enumerate( - [(ax_v, arr_v, r"$\eta_v$"), (ax_c, arr_c, r"$\eta_{10}$")] - ): - ax.imshow(arr, vmin=0, vmax=50, cmap="viridis") - - # Show all ticks and label them with the respective list entries - # We want to show all ticks... - ax.set_xticks(np.arange(len(labels))) - ax.set_yticks(np.arange(len(labels))) - # ... and label them with the respective list entries - ax.set_xticklabels(labels) - ax.set_yticklabels(labels) - - # specific for up side eta_v fig - if idx == 1: - ax.yaxis.set_visible(False) - - # Rotate the tick labels and set their alignment. - plt.setp( - ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor" - ) - plt.setp( - ax.get_yticklabels(), rotation=45, ha="right", rotation_mode="anchor" - ) - - # Loop over data dimensions and create text annotations. - for i in range(len(labels)): - for j in range(len(labels)): - ax.text( - j, - i, - np.around(arr[i, j], decimals=2), - ha="center", - va="center", - color="w", - ) - - ax.set_title(title) - - def _bands_distance(self, pseudos): - """compute bands distance""" - labels = list(pseudos.keys()) - element = extract_element(pseudos) - do_smearing = element not in NONMETAL_ELEMENTS - - fermi_shift = _FERMI_SHIFT - - arr_v = np.empty((len(labels), len(labels))) - arr_v[:] = np.nan - arr_c = np.empty((len(labels), len(labels))) - arr_c[:] = np.nan - for (idx1, label1), (idx2, label2) in itertools.combinations( - enumerate(labels), 2 - ): - # load cache if exist. - # the pseudos passed in the function is in order, keys are sorted when created - # Therefore, can always be '(label1)(label2)' - cache_key = f"({label1})({label2})" - if cache_key in self.__cache_bands: - distance = self.__cache_bands.get(cache_key) - else: - try: - bandsdata1 = _bandview( - os.path.join( - SSSP_DB, pseudos[label1]["accuracy"]["bands"]["bands"] - ) - ) - bandsdata2 = _bandview( - os.path.join( - SSSP_DB, pseudos[label2]["accuracy"]["bands"]["bands"] - ) - ) - except KeyError: - continue - - spin = element is not None and element in MAGNETIC_ELEMENTS - - distance = get_bands_distance( - bandsdata_a=bandsdata1, - bandsdata_b=bandsdata2, - smearing=_SMEARING_WIDTH, - fermi_shift=fermi_shift, - do_smearing=do_smearing, - spin=spin, - ) - self.__cache_bands[cache_key] = distance - - eta_v = distance["eta_v"] - max_diff_v = distance["max_diff_v"] - eta_c = distance["eta_c"] - max_diff_c = distance["max_diff_c"] - - arr_v[idx1, idx2] = eta_v - arr_v[idx2, idx1] = max_diff_v - - arr_c[idx1, idx2] = eta_c - arr_c[idx2, idx1] = max_diff_c - - return labels, arr_v, arr_c diff --git a/aiidalab_sssp/inspect/subwidgets/convergence.py b/aiidalab_sssp/inspect/subwidgets/convergence.py deleted file mode 100644 index 27ebeda..0000000 --- a/aiidalab_sssp/inspect/subwidgets/convergence.py +++ /dev/null @@ -1,247 +0,0 @@ -import ipywidgets as ipw -import traitlets -from aiida_sssp_workflow.utils import get_protocol -from IPython.display import clear_output, display -from matplotlib import pyplot as plt - -from aiidalab_sssp.inspect import _px, cmap, extract_element, parse_label -from aiidalab_sssp.inspect.subwidgets.summary import SummaryWidget - - -def get_threshold(property_name) -> dict: - """Get threshold for plot from protocol - - return a dict of upper bound of criteria - """ - protocol = get_protocol("criteria") - threshold = {} - for key, value in protocol.items(): - threshold[key] = max(value[property_name]["bounds"]) - - return threshold - - -property_map = { - "Cohesive energy (Absolute Error, meV/atom)": { - "name": "cohesive_energy", - "measure": "absolute_diff", - "ylabel": "Absolute error per atom (meV/atom)", - "threshold": get_threshold("cohesive_energy"), - }, - "Cohesive energy (Raw Energy, meV/atom)": { - "name": "cohesive_energy", - "measure": "cohesive_energy_per_atom", - "ylabel": "Cohesive energy per atom (meV/atom)", - }, - "Phonon frequencies (Relative Error, %)": { - "name": "phonon_frequencies", - "measure": "relative_diff", - "ylabel": "Relative error (%)", - "threshold": get_threshold("phonon_frequencies"), - }, - "Phonon frequencies (Max freq error, cm-1)": { - "name": "phonon_frequencies", - "measure": "absolute_max_diff", - "ylabel": "Max frequencies error, cm-1", - }, - "Phonon frequencies (Max frequencies, cm-1)": { - "name": "phonon_frequencies", - "measure": "omega_max", - "ylabel": "Max frequencies, cm-1", - }, - "Pressure (Relative Error, %)": { - "name": "pressure", - "measure": "relative_diff", - "ylabel": "Relative error (%)", - "threshold": get_threshold("pressure"), - }, - "Bands distance (Avg. error meV)": { - "name": "bands", - "measure": "eta_c", - "ylabel": r"$Avg. Error of \eta_c (meV)$", - "threshold": get_threshold("bands"), - }, - "Bands distance (Max. error meV)": { - "name": "bands", - "measure": "max_diff_c", - "ylabel": r"$Max Error of \eta_v (meV)$", - }, - "Delta (Relative Error, %)": { - "name": "delta", - "measure": "relative_diff", - "ylabel": "Relative error (%)", - "threshold": get_threshold("delta"), - }, - "Delta (Raw value, meV/cell)": { - "name": "delta", - "measure": "delta", - "ylabel": r"$\Delta$ value (meV/cell)", - }, -} - - -class ConvergenceWidget(ipw.VBox): - - pseudos = traitlets.Dict(allow_none=True) - - def __init__(self): - - # using raido button widget so user only choose one proper to check - # at one time. It can be more, but pollute the UX and not useful. - self.property_select = ipw.RadioButtons( - options=list(property_map.keys()), - value=list(property_map.keys())[0], - ) - self.property_select.observe(self._on_property_select_change, names="value") - self.summary = SummaryWidget() - self.summary.observe( - self._on_summary_criteria_change, names="selected_criteria" - ) - ipw.dlink((self, "pseudos"), (self.summary, "pseudos")) - - self.out = ipw.Output() # out figure - self.convergence = ipw.VBox( - children=[ - self.property_select, - self.out, - ], - ) - - self.summary_accordion = ipw.Accordion( - children=[self.summary], selected_index=None - ) - self.summary_accordion.set_title( - 0, "Toggle to show the summary of verification results." - ) - self.convergence_accordion = ipw.Accordion( - children=[self.convergence], selected_index=None - ) - self.convergence_accordion.set_title( - 0, "Toggle to show the detailed convergence verification results." - ) - self.convergence_accordion.observe( - self._on_convergence_accordion_change, names="selected_index" - ) - self.accordions = ipw.VBox( - children=[ - self.summary_accordion, - self.convergence_accordion, - ] - ) - self.accordions.layout.visibility = "hidden" - - self.help_message = ipw.HTML("

Convergence results

") - self.help_message.layout.visibility = "hidden" - - super().__init__( - children=[ - self.help_message, - self.accordions, - ] - ) - - def _on_summary_criteria_change(self, change): - """When select new criteria on summary widget""" - if change["new"]: - self._render() - - @traitlets.observe("pseudos") - def _on_pseudos_change(self, change): - """only update plot when accordion open""" - if change["new"]: - self.accordions.layout.visibility = "visible" - self.help_message.layout.visibility = "visible" - self.convergence_accordion.selected_index = None - else: - self.accordions.layout.visibility = "hidden" - self.help_message.layout.visibility = "hidden" - - def _on_convergence_accordion_change(self, change): - # only render when accordion open up - if change["new"] == 0: - self._render() - - def _on_property_select_change(self, change): - if change["new"]: - self._render() - - def _render(self): - """render the plot""" - property_selected = self.property_select.value - - fig, (ax_wfc, ax_rho) = plt.subplots( - 2, - 1, - gridspec_kw={"wspace": 0.00, "hspace": 0.40}, - figsize=(1024 * _px, 600 * _px), - ) - fig.canvas.header_visible = False - self._render_plot(ax_wfc, ax_rho, property_selected) - - with self.out: - clear_output(wait=True) - display(fig.canvas) - - def _render_plot(self, ax_wfc, ax_rho, property): - """Actual render of plot""" - wfname = property_map[property]["name"] - measure = property_map[property]["measure"] - element = extract_element(self.pseudos) - - for label, pseudo_out in self.pseudos.items(): - # TODO: Calculate the one delta measure and attach to label value - try: - res = pseudo_out["convergence"][wfname] - x_wfc = res["output_parameters_wfc_test"]["ecutwfc"] - y_wfc = res["output_parameters_wfc_test"][measure] - - x_rho = res["output_parameters_rho_test"]["ecutrho"] - y_rho = res["output_parameters_rho_test"][measure] - - wavefunction_cutoff = res["output_parameters"]["wavefunction_cutoff"] - except KeyError: - # usually the convergence test on the property is not finished okay - continue # TODO give more detailed messages - - # plot - pseudo_info = parse_label(label) - - # TODO label include delta info - ax_wfc.plot( - x_wfc, - y_wfc, - marker="^", - markersize=2, - color=cmap(pseudo_info), - label=pseudo_info["representive_label"], - ) - ax_rho.plot( - x_rho, - y_rho, - marker="^", - markersize=2, - color=cmap(pseudo_info), - label=f"{wavefunction_cutoff} Ry", - ) - - # ax_wfc.set_ylabel(property_map[property]['ylabel']) - ax_wfc.set_xlabel("Wavefuntion cutoff (Ry)") - ax_wfc.set_title( - f"Convergence verification on element {element} (dual=4 for NC and dual=8 for non-NC)" - ) - ax_wfc.legend(loc="upper right", prop={"size": 6}) - - ax_rho.set_ylabel(property_map[property]["ylabel"]) - ax_rho.yaxis.set_label_coords(-0.05, 0.9) - ax_rho.set_xlabel("Charge density cudoff (Ry)") - ax_rho.set_title("Convergence test at fixed wavefunction cutoff") - ax_rho.legend(loc="upper right", prop={"size": 6}) - - _criteria = str.lower(self.summary.selected_criteria) - threshold = property_map[property].get("threshold", {}).get(_criteria, None) - if threshold: - ax_wfc.axhline(y=threshold, color="r", linestyle="--") - ax_rho.axhline(y=threshold, color="r", linestyle="--") - - # ax_wfc.set_ylim(-0.5 * threshold, 10 * threshold) - # ax_rho.set_ylim(-0.5 * threshold, 10 * threshold) diff --git a/aiidalab_sssp/inspect/subwidgets/delta.py b/aiidalab_sssp/inspect/subwidgets/delta.py deleted file mode 100644 index 80be406..0000000 --- a/aiidalab_sssp/inspect/subwidgets/delta.py +++ /dev/null @@ -1,321 +0,0 @@ -"""Moudle contains widgets for accuracy delat results inspect. -The widget EosComparisonWidget for compare Eos fit line of a given pseudos in the given configuration. -The widget AccuracyMeritWidget showing Nicola's Nu measure of all pseudos in all configurations""" -import ipywidgets as ipw -import matplotlib.pyplot as plt -import numpy as np -import traitlets -from aiida_sssp_workflow.calculations.calculate_delta import rel_errors_vec_length -from IPython.display import clear_output, display - -from aiidalab_sssp.inspect import _px, cmap, extract_element, parse_label -from aiidalab_sssp.inspect.subwidgets.utils import CONFIGURATIONS - - -def birch_murnaghan(V, E0, V0, B0, B01): - """ - Return the energy for given volume (V - it can be a vector) according to - the Birch Murnaghan function with parameters E0,V0,B0,B01. - """ - r = (V0 / V) ** (2.0 / 3.0) - return E0 + 9.0 / 16.0 * B0 * V0 * ( - (r - 1.0) ** 3 * B01 + (r - 1.0) ** 2 * (6.0 - 4.0 * r) - ) - - -class AccuracyMeritWidget(ipw.VBox): - """Widget for showing accuracy merit of a given pseudo over all configuration. - The merit can be either nu or delta. - """ - - pseudos = traitlets.Dict(allow_none=True) - merit_type = traitlets.Unicode(default_value="nu") - - def __init__(self): - self.out_plot = ipw.Output() - - super().__init__( - children=[ - self.out_plot, - ], - ) - - @traitlets.observe("merit_type") - def _on_merit_type_change(self, change): - if change["new"]: - self.update_plot() - - @traitlets.observe("pseudos") - def _on_pseudos_change(self, change): - """Update the plot when pseudos are changed.""" - if change["new"]: - self.layout.display = "block" - self.update_plot() - else: - self.layout.display = "none" - - def update_plot(self): - """Update the plot with the current pseudos and measure type.""" - with self.out_plot: - clear_output() - fig = self._render_plot(self.pseudos, self.merit_type) - fig.canvas.header_visible = False - display(fig.canvas) - - @staticmethod - def _render_plot(pseudos: dict, measure_type): - """Render the plot for the given pseudos and measure type.""" - fig, ax = plt.subplots(1, 1, figsize=(1024 * _px, 360 * _px)) - # conf_list store configuration list of every pseudo - conf_list = {} - for label, data in pseudos.items(): - _data = data["accuracy"]["delta"] - conf_list[label] = [ - i for i in CONFIGURATIONS if i in _data.keys() and i != "TYPICAL" - ] - - # element - element = extract_element(pseudos) - - if measure_type == "delta": - ylabel = "Δ -factor" - elif measure_type == "nu": - ylabel = "ν -factor" - - xticklabels = [] - - for i, (label, output) in enumerate(pseudos.items()): - # update xticklabel to include all configurations in output - if len(xticklabels) < len(conf_list[label]): - xticklabels = conf_list[label] - - width = 0.6 / len(pseudos) - - y_delta = [] - for configuration in conf_list[label]: - res = output["accuracy"]["delta"][configuration]["output_parameters"] - if measure_type == "delta": - y_delta.append(res["delta/natoms"]) - if measure_type == "nu": - v0w, b0w, b1w = res["birch_murnaghan_results"] - v0f, b0f, b1f = res["reference_wien2k_V0_B0_B1"] - nu = rel_errors_vec_length(v0w, b0w, b1w, v0f, b0f, b1f) - y_delta.append(nu) - - x = np.arange(len(conf_list[label])) - pseudo_info = parse_label(label) - - ax.bar( - x + width * i, - y_delta, - width, - color=cmap(pseudo_info), - edgecolor="black", - linewidth=1, - label=pseudo_info["representive_label"], - ) - ax.set_title(f"X={element}") - - if max(y_delta) < 8.0: - y_max = 10 / 8.0 * max(y_delta) - else: - y_max = 10.0 - - ax.legend(loc="upper left", prop={"size": 10}) - ax.axhline(y=1.0, linestyle="--", color="gray") - ax.set_ylabel(ylabel) - ax.set_ylim([0, y_max]) - ax.set_xticks(range(len(xticklabels))) - ax.set_xticklabels(xticklabels) - - return fig - - -class EosComparisonWidget(ipw.VBox): - """This widget is used to compare the equation of state of two different - pseudopotentials. - Two subplots are shown for two different pseudopotentials. The drowdown menu - allows user to select the pseudopotential and configuration. - """ - - pseudos = traitlets.Dict(allow_none=True) - - def __init__(self): - self.select_pseudo_ref = ipw.Dropdown() - self.select_pseudo_comp = ipw.Dropdown() - - self._observer_on_for_pseudos_dropdown() - - self.select_configuration = ipw.Dropdown() - self.select_configuration.observe(self._on_configuration_change, names="value") - - self.eos_preview = ipw.Output() # empty plot with a instruction ask for select - - super().__init__( - children=[ - ipw.HBox( - children=[ - self.select_pseudo_ref, - self.select_pseudo_comp, - ] - ), - self.select_configuration, - self.eos_preview, - ], - ) - - def _observer_on_for_pseudos_dropdown(self): - self.select_pseudo_ref.observe(self._on_pseudo_select, names="value") - self.select_pseudo_comp.observe(self._on_pseudo_select, names="value") - - def _unobserve_on_for_pseudos_dropdown(self): - self.select_pseudo_ref.unobserve(self._on_pseudo_select, names="value") - self.select_pseudo_comp.unobserve(self._on_pseudo_select, names="value") - - @traitlets.observe("pseudos") - def _on_pseudos_change(self, change): - if change["new"] is not None and len(change["new"]) > 0: - self.layout.display = "block" - with self.hold_trait_notifications(): - pseudo_list = list(self.pseudos.keys()) - - # remove the observer before update the dropdown menu - self._unobserve_on_for_pseudos_dropdown() - - # update the dropdown menu - self.select_pseudo_ref.options = pseudo_list - self.select_pseudo_comp.options = pseudo_list - - # update the configuration dropdown menu and select the first and second pseudo, respectively - self.select_pseudo_ref.value = pseudo_list[0] - self.select_pseudo_comp.value = pseudo_list[0] - self._update_configuration(pseudo_list[0], pseudo_list[0]) - - # add the observer back - self._observer_on_for_pseudos_dropdown() - - self.update_plot() - else: - self.layout.display = "none" - - def _on_pseudo_select(self, _): - """Update configuration dropdown options""" - label_ref = self.select_pseudo_ref.value - label_comp = self.select_pseudo_comp.value - - if label_ref is None or label_comp is None: - return - - self._update_configuration(label_ref, label_comp) - self.update_plot() - - def _update_configuration(self, ref, comp): - """Update configuration dropdown options""" - _data_ref = self.pseudos[ref]["accuracy"]["delta"] - _data_comp = self.pseudos[comp]["accuracy"]["delta"] - - # The configuration list is the intersection of two pseudos - self.select_configuration.options = [ - i - for i in CONFIGURATIONS - if i in _data_ref.keys() and i in _data_comp.keys() - ] - - self.update_plot() - - def update_plot(self): - """Trigger plot update""" - label_ref = self.select_pseudo_ref.value - label_comp = self.select_pseudo_comp.value - configuration = self.select_configuration.value - - data_ref = self.pseudos[label_ref]["accuracy"]["delta"].get(configuration, None) - data_comp = self.pseudos[label_comp]["accuracy"]["delta"].get( - configuration, None - ) - - with self.eos_preview: - clear_output(wait=True) - fig = self._render_plot( - data_ref, data_comp, configuration, titles=(label_ref, label_comp) - ) - fig.canvas.header_visible = False - display(fig.canvas) - - def _on_configuration_change(self, change): - """Update eos preview""" - if change["new"] is not None: - self.update_plot() - - @staticmethod - def _render_plot(data_ref, data_comp, configuration, titles=("EOS", "EOS")): - """render preview of EOS comparison result.""" - fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(1024 * _px, 440 * _px)) - - # plot to ax1 - plot_eos(ax1, data_ref, configuration, title=titles[0]) - # plot to ax2 - plot_eos(ax2, data_comp, configuration, title=titles[1]) - - return fig - - -def plot_eos(ax, data, configuration, title="EOS"): - """plot EOS result on ax""" - volumes = data["eos"]["output_volume_energy"]["volumes"] - energies = data["eos"]["output_volume_energy"]["energies"] - - dense_volume_max = max(volumes) - dense_volume_min = min(volumes) - - dense_volumes = np.linspace(dense_volume_min, dense_volume_max, 100) - - E0 = data["eos"]["output_birch_murnaghan_fit"]["energy0"] - ref_V0, ref_B0, ref_B01 = data["output_parameters"]["reference_wien2k_V0_B0_B1"] - V0, B0, B01 = data["output_parameters"]["birch_murnaghan_results"] - - ae_eos_fit_energy = birch_murnaghan( - V=dense_volumes, - E0=E0, # in future update E0 from referece json, where ACWF has E0 stored. - V0=ref_V0, - B0=ref_B0, - B01=ref_B01, - ) - psp_eos_fit_energy = birch_murnaghan( - V=dense_volumes, - E0=E0, - V0=V0, - B0=B0, - B01=B01, - ) - - # Plot EOS: this will be done anyway - ax.tick_params(axis="y", labelsize=6, rotation=45) - - ax.plot(volumes, energies, "ob", label="RAW equation of state") - ax.plot(dense_volumes, ae_eos_fit_energy, "-b", label="AE reference") - ax.axvline(V0, linestyle="--", color="gray") - - ax.plot(dense_volumes, psp_eos_fit_energy, "-r", label=f"{configuration} fit") - ax.fill_between( - dense_volumes, - ae_eos_fit_energy, - psp_eos_fit_energy, - alpha=0.5, - color="red", - ) - - center_x = (max(volumes) + min(volumes)) / 2 - center_y = (max(energies) + min(energies)) / 2 - - # write text of nu value in close middle - nu = rel_errors_vec_length(ref_V0, ref_B0, ref_B01, V0, B0, B01) - nu = round(nu, 3) - delta = round(data["output_parameters"]["delta/natoms"], 3) - ax.text(center_x, center_y, f"$\\nu$={nu}\n$\\Delta$={delta} meV/atom") - - ax.legend(loc="upper center") - ax.set_xlabel("Cell volume per formula unit ($\\AA^3$)", fontsize=8) - ax.set_ylabel("$E - TS$ per formula unit (eV)", fontsize=8) - ax.get_yaxis().set_ticks([]) - ax.set_title(title, fontsize=8) diff --git a/aiidalab_sssp/inspect/subwidgets/periodic_table.py b/aiidalab_sssp/inspect/subwidgets/periodic_table.py deleted file mode 100644 index 32ec663..0000000 --- a/aiidalab_sssp/inspect/subwidgets/periodic_table.py +++ /dev/null @@ -1,225 +0,0 @@ -import json -import os -import shutil -import tarfile -from urllib import request - -import ipywidgets as ipw -import traitlets -from widget_periodictable import PTableWidget - -from aiidalab_sssp.inspect import SSSP_DB - -__all__ = ("PeriodicTable",) - - -_DB_URL = "https://github.com/unkcpz/sssp-verify-scripts/raw/main/sssp_db.tar.gz" -_DB_FOLDER = "sssp_db" - - -def _load_pseudos(element, db=SSSP_DB) -> dict: - """Open result json file of element return as dict""" - if element: - json_fn = os.path.join(db, f"{element}.json") - with open(json_fn, "r") as fh: - pseudos = json.load(fh) - - return {key: pseudos[key] for key in sorted(pseudos.keys(), key=str.lower)} - - return dict() - - -class PeriodicTable(ipw.VBox): - """Wrapper-widget for PTableWidget, select the element and update the dict of pseudos""" - - # (output) dict of pseudos for selected element - pseudos = traitlets.Dict(allow_none=True) - - def __init__(self, cache_folder, **kwargs): - self._disabled = kwargs.get("disabled", False) - self._cache_folder = cache_folder - - self.ptable = PTableWidget(states=1, selected_colors=["green"], **kwargs) - self._last_selected = None - self.ptable.observe(self._on_element_select) - self._element = None # selected element, for record the last selected element - - self.elements = set() # elements that have json file in the db folder - - # if cache empty run update: first time - self.db_version = None - if os.path.exists(os.path.join(cache_folder, _DB_FOLDER)): - self._update_db(download=False) - else: - self._update_db(download=True) - - disable_elements = [ - e for e in self.ptable.allElements if e not in self.elements - ] - self.ptable.disabled_elements = disable_elements - db_update = ipw.Button( - description="Update Database.", - ) - db_update.on_click(self._update_db) - - self.json_upload = ipw.FileUpload( - accept=".json", multiple=False, description="Upload json file" - ) - self.json_upload.observe(self._on_json_upload, names="value") - - super().__init__( - children=( - self.ptable, - ipw.HBox( - children=[ - db_update, - ipw.HTML(f"The SSSP Database version: {self.db_version}"), - ] - ), - self.json_upload, - ), - layout=kwargs.get("layout", {}), - ) - - def _on_json_upload(self, change): - if change["name"] == "value" and change["type"] == "change": - if change["new"]: - # get the first file - file_name = list(change["new"].keys())[0] - content = change["new"][file_name]["content"] - pseudos = json.loads(content.decode("utf-8")) - - # check if the pseudos are valid by check the element name from the key of pseudos - for key in pseudos: - if self._element is not None and self._element not in key: - raise ValueError( - f"The element name in the json file is not {self._element}, please check the json file." - ) - - # if self.pseudos is None or len(self.pseudos) == 0: - # self.pseudos = pseudos - # else: - # self.pseudos = self.pseudos.update(pseudos) - self.update_pseudos(element=self._element, pseudos=pseudos) - - # reset the upload widget to empty so that the same file can be uploaded again - # self.json_upload.value = {} - self.json_upload._counter = 0 - - def _on_element_select(self, event): - if event["name"] == "selected_elements" and event["type"] == "change": - if tuple(event["new"].keys()) == ("Du",): - self._last_selected = event["old"] - elif tuple(event["old"].keys()) == ("Du",): - if len(event["new"]) != 1: - # Reset to only one element only if there is more than one selected, - # to avoid infinite loops - newly_selected = set(event["new"]).difference(self._last_selected) - # If this is empty it's ok, unselect all - # If there is more than one, that's weird... to avoid problems, anyway, I pick one of the two - if newly_selected: - self._element = list(newly_selected)[0] - self.ptable.selected_elements = {self._element: 0} - self.update_pseudos(self._element) - else: - self.reset() - # To have the correct 'last' value for next calls - self._last_selected = self.ptable.selected_elements - else: - # first time set: len(event['new']) -> 1 - self._element = list(event["new"])[0] - self.update_pseudos(self._element) - - def update_pseudos(self, element=None, pseudos=None): - pseudos = {} if pseudos is None else pseudos - if element is not None: - pseudos.update(_load_pseudos(element)) - - self.pseudos = pseudos - - def _update_db(self, _=None, download=True): - """update cached db fetch from remote. and update ptable""" - # download from remote - if download: - self._download(self._cache_folder) - - self.elements = self._get_enabled_elements(self._cache_folder) - disable_elements = [ - e for e in self.ptable.allElements if e not in self.elements - ] - self.ptable.disabled_elements = disable_elements - self.db_version = self._get_db_version(self._cache_folder) - - self.reset() - - @staticmethod - def _get_enabled_elements(cache_folder): - elements = set() - for fn in os.listdir(os.path.join(cache_folder, _DB_FOLDER)): - if "band" not in fn: - elements.add(fn.split(".")[0]) - - return elements - - @staticmethod - def _get_db_version(cache_folder): - with open(os.path.join(cache_folder, _DB_FOLDER, "version.txt"), "r") as fh: - lines = fh.read() - db_version = lines.split("\n")[0].split("=")[1].strip() - - return db_version - - @staticmethod - def _download(cache_folder): - """ - The original sssp_db folder is deleted and re-downloaded from - source and extracted. - - :params cache_folder: folder where cache stored - """ - # Purge whole db folder filst - db_dir = f"{cache_folder}/sssp_db" - if os.path.exists(db_dir) and os.path.isdir(db_dir): - shutil.rmtree(db_dir) - - # download DB tar file from source - tar_file = f"{cache_folder}/sssp_db.tar.gz" - request.urlretrieve(_DB_URL, tar_file) - - # decompress to the db folder - tar = tarfile.open(tar_file) - os.chdir(cache_folder) - tar.extractall() - tar.close() - - os.remove(tar_file) - - @property - def value(self) -> dict: - """Return value for wrapped PTableWidget""" - - return self.ptable.selected_elements.copy() - - @property - def disabled(self) -> None: - """Disable widget""" - return self._disabled - - @disabled.setter - def disabled(self, value: bool) -> None: - """Disable widget""" - if not isinstance(value, bool): - raise TypeError("disabled must be a boolean") - - def reset(self): - """Reset widget""" - self.ptable.selected_elements = {} - self.selected_element = None - - def freeze(self): - """Disable widget""" - self.disabled = True - - def unfreeze(self): - """Activate widget (in its current state)""" - self.disabled = False diff --git a/aiidalab_sssp/inspect/subwidgets/plot.py b/aiidalab_sssp/inspect/subwidgets/plot.py deleted file mode 100644 index 429d598..0000000 --- a/aiidalab_sssp/inspect/subwidgets/plot.py +++ /dev/null @@ -1,113 +0,0 @@ -import ipywidgets as ipw -import traitlets -from IPython.display import clear_output, display - -from aiidalab_sssp.inspect.plot_utils import convergence - - -class _PlotConvergenBaseWidget(ipw.VBox): - - selected_pseudos = traitlets.Dict(allow_none=True) - - _WF = "Not implement" - _MEASURE = "Not implement" - _YLABEL = "Not implement" - _THRESHOLD = None - - def __init__(self): - # output widget - self.output = ipw.Output() - - super().__init__( - children=[ - self.output, - ], - ) - - @traitlets.observe("selected_pseudos") - def _on_pseudos_change(self, change): - - if change["new"]: - with self.output: - clear_output(wait=True) - fig = convergence( - change["new"], - wf_name=self._WF, - measure_name=self._MEASURE, - ylabel=self._YLABEL, - threshold=self._THRESHOLD, - ) - fig.canvas.header_visible = False - display(fig.canvas) - - -class PlotCohesiveEnergyConvergeWidget(_PlotConvergenBaseWidget): - - _WF = "convergence_cohesive_energy" - _MEASURE = "cohesive_energy_per_atom" - _YLABEL = "Cohesive Energy per atom (meV/atom)" - _THRESHOLD = None - - -class PlotCohesiveEnergyConvergeDiffWidget(_PlotConvergenBaseWidget): - - _WF = "convergence_cohesive_energy" - _MEASURE = "absolute_diff" - _YLABEL = "Cohesive Energy per atom (absolute error, meV/atom)" - _THRESHOLD = 2.0 - - -class PlotPhononFrequenciesConvergeAbsWidget(_PlotConvergenBaseWidget): - - _WF = "convergence_phonon_frequencies" - _MEASURE = "absolute_diff" - _YLABEL = "Phonon frequencies ω (absolute error, cm-1)" - _THRESHOLD = None - - -class PlotPhononFrequenciesConvergeRelWidget(_PlotConvergenBaseWidget): - - _WF = "convergence_phonon_frequencies" - _MEASURE = "relative_diff" - _YLABEL = "Phonon frequencies ω (relative error, %)" - _THRESHOLD = 2.0 - - -class PlotPressureConvergeWidget(_PlotConvergenBaseWidget): - - _WF = "convergence_pressure" - _MEASURE = "pressure" - _YLABEL = "Pressure (GPa)" - _THRESHOLD = None - - -class PlotPressureConvergeRelWidget(_PlotConvergenBaseWidget): - - _WF = "convergence_pressure" - _MEASURE = "relative_diff" - _YLABEL = "Pressure (relative error, %)" - _THRESHOLD = 1.0 - - -class PlotDeltaConvergeWidget(_PlotConvergenBaseWidget): - - _WF = "convergence_delta" - _MEASURE = "delta" - _YLABEL = "Δ -factor (meV)" - _THRESHOLD = None - - -class PlotDeltaConvergeRelWidget(_PlotConvergenBaseWidget): - - _WF = "convergence_delta" - _MEASURE = "relative_diff" - _YLABEL = "Delta (relative error, %)" - _THRESHOLD = 2.0 - - -class PlotBandsConvergeWidget(_PlotConvergenBaseWidget): - - _WF = "convergence_bands" - _MEASURE = "eta_c" - _YLABEL = "η up above fermi energe 5 eV (meV)" - _THRESHOLD = 20.0 diff --git a/aiidalab_sssp/inspect/subwidgets/select.py b/aiidalab_sssp/inspect/subwidgets/select.py deleted file mode 100644 index 6324e76..0000000 --- a/aiidalab_sssp/inspect/subwidgets/select.py +++ /dev/null @@ -1,196 +0,0 @@ -import ipywidgets as ipw -import traitlets - -from aiidalab_sssp.inspect import parse_label - -BASE_DOWNLOAD_URL = ( - "https://raw.githubusercontent.com/unkcpz/sssp-verify-scripts/main/libraries-pbe" -) - - -class SelectMultipleCheckbox(ipw.VBox): - """Widget with a search field and lots of checkboxes of pseudopotentials""" - - options = traitlets.List() # options for all labels - selected_labels = traitlets.List() # labels selected - - def __init__(self, tick_all=True, **kwargs): - self.tick_all = tick_all - self.checkbox_dict = {} - self._update_checkbox_group() - - super().__init__(**kwargs) - - def dw_btn(self, label): - """From label generate a redirect button to upf source""" - if "dojo.v4-std" in label: - lib_folder = "NC-DOJOv4-standard" - elif "dojo.v4-str" in label: - lib_folder = "NC-DOJOv4-stringent" - elif "sg15.v0" in label: - lib_folder = "NC-SG15-ONCVPSP4" - elif "psl.v0." in label and "paw" in label: - lib_folder = "PAW-PSL0.x" - elif "psl.v0." in label and "us" in label: - lib_folder = "US-PSL0.x" - elif "psl.v1.0.0-high" in label and "paw" in label: - lib_folder = "PAW-PSL1.0.0-high" - elif "psl.v1.0.0-low" in label and "paw" in label: - lib_folder = "PAW-PSL1.0.0-low" - elif "psl.v1.0.0-high" in label and "us" in label: - lib_folder = "US-PSL1.0.0-high" - elif "psl.v1.0.0-low" in label and "us" in label: - lib_folder = "US-PSL1.0.0-low" - elif "jth.v1.1-std" in label: - lib_folder = "PAW-JTH1.1-standard" - elif "jth.v1.1-str" in label: - lib_folder = "PAW-JTH1.1-stringent" - elif "gbrv" in label: - lib_folder = "US-GBRV-1.x" - elif "wentzcovitch" in label and "neo" in label: - lib_folder = "PAW-RE-Wentzcovitch/neo" - elif "wentzcovitch" in label and "legacy" in label: - lib_folder = "PAW-RE-Wentzcovitch/legacy" - else: - lib_folder = "UNCATOGRIZED" - - btn = ipw.HTML( - f"""""" - ) - - return btn - - def _update_checkbox_group(self): - # Update the checkbox widgets view - self.children = [ - ipw.HBox(children=[v, self.dw_btn(k)], layout=ipw.Layout(width="55%")) - for k, v in self.checkbox_dict.items() - ] - - # Since all checkboxes are recreated set the observe for all of them - for checkbox in self.checkbox_dict.values(): - checkbox.observe(self._on_any_checkbox_change, names="value") - - # reset the outpput values - self._update_selected_labels() - - def _on_any_checkbox_change(self, change): - # Any time any checkbox ticked or unticked reset - self._update_selected_labels() - - def _update_selected_labels(self): - self.selected_labels = [ - label for label, checkbox in self.checkbox_dict.items() if checkbox.value - ] - - def unselecet_all(self): - with self.hold_trait_notifications(): - for key in self.checkbox_dict.keys(): - self.checkbox_dict[key].value = False - - self._update_selected_labels() - - def selecet_all(self): - with self.hold_trait_notifications(): - for key in self.checkbox_dict.keys(): - self.checkbox_dict[key].value = True - - self._update_selected_labels() - - @traitlets.observe("options") - def _observe_options_change(self, change): - # when options list (element rechoose) change update all checkboxes - self.checkbox_dict = { - f"{desc}": ipw.Checkbox( - description=parse_label(desc)["representive_label"], - value=self.tick_all, - style={"description_width": "initial"}, - layout=ipw.Layout(width="50%", height="50%"), - ) - for desc in self.options - } - - self._update_checkbox_group() - - -class PseudoSelectWidget(ipw.VBox): - # (input) all pseudos of a element, the whole dict from json fixed once element choosen - pseudos = traitlets.Dict(allow_none=True) - - # (output) selected pseudos of a element, the whole dict once pseudos selected - selected_pseudos = traitlets.Dict(allow_none=True) - - def __init__(self): - self.NO_PSEUDOS_FOR_SELECT_INFO = "No pseudopotentials available for compare, please select an element or upload a verification file." - self.help_info = ipw.HTML(self.NO_PSEUDOS_FOR_SELECT_INFO) - self.unselect_all = ipw.Button( - description="Unselect All", - button_style="info", - ) - self.unselect_all.on_click(self._unselect_all_click) - - self.select_all = ipw.Button( - description="Select All", - button_style="info", - ) - self.select_all.on_click(self._on_select_all_click) - - self.select_buttons = ipw.HBox(children=[self.unselect_all, self.select_all]) - self.select_buttons.layout.visibility = "hidden" - - self.multiple_selection = SelectMultipleCheckbox( - disabled=False, layout=ipw.Layout(width="98%") - ) - self.multiple_selection.observe( - self._on_multiple_selection_change, names="selected_labels" - ) - - super().__init__( - children=[ - self.help_info, - self.select_buttons, - self.multiple_selection, - ] - ) - - def _unselect_all_click(self, _): - """Unselect all""" - # self.selected_pseudos = {} - self.multiple_selection.unselecet_all() - - def _on_select_all_click(self, _): - """Selecet All""" - # self.selected_pseudos = _load_pseudos(self.element) - self.multiple_selection.selecet_all() - - @traitlets.observe("pseudos") - def _observe_pseudos(self, change): - if change["new"] is not None and change["new"] != {}: # pseudos is not empty - with self.hold_trait_notifications(): - self.select_buttons.layout.visibility = "visible" - # if select/unselect new element update prompt help info - self.help_info.value = "Please choose pseudopotentials to inspect:" - - # self.pseudos store all dict for the element the initial parsed from element json - # select all pseudos of element as default - self.multiple_selection.options = list(self.pseudos.keys()) - self.selected_pseudos = ( - self.pseudos.copy() - ) # the traitlets need to be a copy of the dict otherwise it will not trigger the change event - else: - # if empty dict passed (by unseleted the element) reset multiple select widget - self.reset() - - def _on_multiple_selection_change(self, change): - with self.hold_trait_notifications(): - self.selected_pseudos = { - k: self.pseudos[k] for k in sorted(change["new"], key=str.lower) - } - - def reset(self): - """Reset the widget to initial state, no checkbox widget at all""" - with self.hold_trait_notifications(): - self.select_buttons.layout.visibility = "hidden" - self.help_info.value = self.NO_PSEUDOS_FOR_SELECT_INFO - self.multiple_selection.options = list() - self.selected_pseudos = {} diff --git a/aiidalab_sssp/inspect/subwidgets/summary.py b/aiidalab_sssp/inspect/subwidgets/summary.py deleted file mode 100644 index b6d2b38..0000000 --- a/aiidalab_sssp/inspect/subwidgets/summary.py +++ /dev/null @@ -1,215 +0,0 @@ -import ipywidgets as ipw -import pandas as pd -import traitlets -from aiida_sssp_workflow.calculations.calculate_delta import rel_errors_vec_length -from aiida_sssp_workflow.workflows.verifications import ( - DEFAULT_CONVERGENCE_PROPERTIES_LIST, -) -from IPython.display import clear_output, display - -from aiidalab_sssp.inspect import extract_element, get_conf_list, parse_label -from aiidalab_sssp.inspect.subwidgets.utils import CONFIGURATIONS - - -class SummaryWidget(ipw.VBox): - """Summary of verification""" - - pseudos = traitlets.Dict(allow_none=True) - selected_criteria = traitlets.Unicode() - - def __init__(self): - # Delta mesure - self.accuracy_summary = ipw.Output() - self.convergence_summary = ipw.Output() - - self.toggle_criteria = ipw.ToggleButtons( - options=["Efficiency", "Precision"], - value="Efficiency", - tooltip="Toggle to switch criteria.", - ) - self.toggle_criteria.observe(self._on_toggle_criteria_change) - ipw.dlink((self.toggle_criteria, "value"), (self, "selected_criteria")) - - self._show_rho = False - self._show_dual = False - self.toggle_show_dual_or_rho = ipw.ToggleButtons( - options=["Default", "Show ρ cutoff", "Show dual"], - value="Default", - # description="Show extra cutoff information", - disabled=False, - tooltip="Toggle show rho or dual value", - ) - self.toggle_show_dual_or_rho.observe( - self._on_toggle_show_dual_or_rho_change, names="value" - ) - self.toggle_measure_type = ipw.ToggleButtons( - options=[ - ("ν", "nu"), - ("Δ", "delta"), - ], - value="nu", - tooltip="Toggle to switch between nu and delta", - ) - self.toggle_measure_type.observe( - self._on_toggle_measure_type_change, names="value" - ) - - super().__init__( - children=[ - self.accuracy_summary, - ipw.HTML("

Switch between ν and Δ

"), - self.toggle_measure_type, - self.convergence_summary, - ipw.HTML("

Switch criteria to:

"), - self.toggle_criteria, - ipw.HTML("

Show ρ or dual

"), - self.toggle_show_dual_or_rho, - ], - ) - self.layout.display = "none" - - def _on_toggle_measure_type_change(self, change): - """When the measure type is changed, update the summary.""" - if change["new"] is not None: - self.update_accuracy_summary(change["new"]) - - def update_accuracy_summary(self, measure_type="nu"): - """update accuracy summary""" - with self.accuracy_summary: - clear_output(wait=True) - display(self._render_accuracy(measure_type)) - - def update_convergence_summary(self): - """update convergence summary""" - with self.convergence_summary: - clear_output(wait=True) - display(self._render_convergence()) - - @traitlets.observe("pseudos") - def _on_pseudos_change(self, change): - if change["new"] is not None and len(change["new"]) > 0: - self.layout.display = "block" - self.update_accuracy_summary() - self.update_convergence_summary() - else: - self.layout.display = "none" - - def _on_toggle_show_dual_or_rho_change(self, change): - if change["new"] == "Show ρ cutoff": - self._show_dual = False - self._show_rho = True - elif change["new"] == "Show dual": - self._show_rho = False - self._show_dual = True - else: - self._show_dual = False - self._show_rho = False - - self.update_convergence_summary() - - def _on_toggle_criteria_change(self, _): - self.update_convergence_summary() - - def _render_accuracy(self, measure_type="nu"): - rows = [] - element = extract_element(self.pseudos) - conf_list = [ - i for i in CONFIGURATIONS if i in get_conf_list(element) and i != "TYPICAL" - ] - columns = ["Pseudopotential label"] + conf_list - for label, pseudo_out in self.pseudos.items(): - _data = pseudo_out["accuracy"]["delta"] - y_list = [] - for i in conf_list: - output_parameters = _data.get(i, {}).get("output_parameters", {}) - try: - if measure_type == "delta": - y = output_parameters["delta/natoms"] - else: - v0w, b0w, b1w = output_parameters["birch_murnaghan_results"] - v0f, b0f, b1f = output_parameters["reference_wien2k_V0_B0_B1"] - y = rel_errors_vec_length(v0w, b0w, b1w, v0f, b0f, b1f) - except KeyError: - # there is no delta/nu result for this conf of this pseudo - y = None - - if y: - y_list.append(round(y, 3)) - else: - # there is no delta/nu result for this conf of this pseudo - # print("Cannot find nu value of pseudo={label}, conf={i}") - # it will show in summary table as 'nan' - y_list.append("nan") - - output_label = parse_label(label)["representive_label"] - rows.append([output_label, *y_list]) - - df = pd.DataFrame(rows, columns=columns) - df.style.hide_index() - return df - - def _render_convergence(self): - rows = [] - prop_list = [i.split(".")[1] for i in DEFAULT_CONVERGENCE_PROPERTIES_LIST] - columns = ["Pseudopotential label"] + [i.replace("_", " ") for i in prop_list] - for label, pseudo_out in self.pseudos.items(): - _data = pseudo_out["convergence"] - cutoffs = [] - for prop in prop_list: - # We didn't not relly run the convergence on rho at precision criteria - # The trick here is the wft cutoff of rho cutoff is write and used, - # The dual is from wfccut/rhocut of efficiency criteria, and also applied to - # precision results display. The rhocut is derived then from rhocut=wfccut * dual. - wfc_cutoff = ( - _data.get(prop, {}) - .get("output_parameters", {}) - .get("wavefunction_cutoff", None) - ) - rho_cutoff = ( - _data.get(prop, {}) - .get("output_parameters", {}) - .get("chargedensity_cutoff", None) - ) - wfc_cutoff_str = f"{int(wfc_cutoff)} Ry" if wfc_cutoff else "nan" - rho_cutoff_str = f"{int(rho_cutoff)} Ry" if rho_cutoff else "nan" - # compute dual. The dual is same for both precision and efficiency - try: - dual = round(rho_cutoff / wfc_cutoff, 1) - except Exception: - dual = "nan" - - # if checking precision, update cutoff pair first - if self.toggle_criteria.value == "Precision": - wfc_cutoff = ( - _data.get(prop, {}) - .get("output_parameters", {}) - .get("all_criteria_wavefunction_cutoff", {}) - .get("precision", None) - ) - wfc_cutoff_str = f"{int(wfc_cutoff)} Ry" if wfc_cutoff else "nan" - try: - rho_cutoff = int(wfc_cutoff * dual) - except Exception: - rho_cutoff_str = "nan" - else: - rho_cutoff_str = f"{rho_cutoff} Ry" - - if not wfc_cutoff: - cutoffs.append(str("nan")) - continue - - # not allow to show at the same time - assert not (self._show_dual and self._show_rho) - if self._show_rho: - cutoffs.append(f"{wfc_cutoff_str} ({rho_cutoff_str})") - elif self._show_dual: - cutoffs.append(f"{wfc_cutoff_str} ({dual})") - else: - cutoffs.append(f"{wfc_cutoff_str}") - - output_label = parse_label(label)["representive_label"] - rows.append([output_label, *cutoffs]) - - df = pd.DataFrame(rows, columns=columns) - df.style.hide_index() - return df diff --git a/aiidalab_sssp/inspect/subwidgets/utils.py b/aiidalab_sssp/inspect/subwidgets/utils.py deleted file mode 100644 index 886bf78..0000000 --- a/aiidalab_sssp/inspect/subwidgets/utils.py +++ /dev/null @@ -1,3 +0,0 @@ -from aiida_sssp_workflow.utils import OXIDE_CONFIGURATIONS, UNARIE_CONFIGURATIONS - -CONFIGURATIONS = OXIDE_CONFIGURATIONS + UNARIE_CONFIGURATIONS + ["RE", "TYPICAL"] diff --git a/aiidalab_sssp/parameters/__init__.py b/aiidalab_sssp/parameters/__init__.py deleted file mode 100644 index bf26c25..0000000 --- a/aiidalab_sssp/parameters/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from importlib import resources - -import yaml - -from aiidalab_sssp import parameters - -DEFAULT_PARAMETERS = yaml.safe_load(resources.read_text(parameters, "ssspapp.yaml")) diff --git a/aiidalab_sssp/parameters/ssspapp.yaml b/aiidalab_sssp/parameters/ssspapp.yaml deleted file mode 100644 index daf121a..0000000 --- a/aiidalab_sssp/parameters/ssspapp.yaml +++ /dev/null @@ -1,20 +0,0 @@ ---- -# Default builder parameters for the SsspAppWorkChain - -## Properties -delta_measure: true -bands_measure: true -cohesive_energy_convergence: true -phonon_frequencies_convergence: true -pressure_convergence: true -delta_convergence: true -bands_convergence: true - -## Codes not set in default ask user to set cluster -# ph_code: ph-6.7@localhost -# pw_code: pw-6.7@localhost - -## Calculation settings -protocol: acwf -criteria: efficiency -calc_type: standard diff --git a/aiidalab_sssp/process.py b/aiidalab_sssp/process.py deleted file mode 100644 index 0d1b7b8..0000000 --- a/aiidalab_sssp/process.py +++ /dev/null @@ -1,153 +0,0 @@ -"""Widgets related to process management.""" -from dataclasses import dataclass -from threading import Event, Lock, Thread - -import ipywidgets as ipw -import traitlets -from aiida.cmdline.utils.query.calculation import CalculationQueryBuilder -from aiida.orm import load_node - - -class WorkChainSelector(ipw.HBox): - - # The PK of a 'aiida.workflows:quantumespresso.pw.bands' WorkChainNode. - value = traitlets.Unicode(allow_none=True) - - # When this trait is set to a positive value, the work chains are automatically - # refreshed every `auto_refresh_interval` seconds. - auto_refresh_interval = traitlets.Int() # seconds - - # Indicate whether the widget is currently updating the work chain options. - busy = traitlets.Bool(read_only=True) - - # Note: We use this class as a singleton to reset the work chains selector - # widget to its default stage (no work chain selected), because we cannot - # use `None` as setting the widget's value to None will lead to "no selection". - _NO_PROCESS = object() - - FMT_WORKCHAIN = "{wc.pk:6}{wc.ctime:>10}\t{wc.state:<16}\t{wc.element}" - - def __init__(self, **kwargs): - self.work_chains_prompt = ipw.HTML("Select workflow or start new: ") - self.work_chains_selector = ipw.Dropdown( - options=[("New workflow...", self._NO_PROCESS)], - layout=ipw.Layout(min_width="300px", flex="1 1 auto"), - ) - ipw.dlink( - (self.work_chains_selector, "value"), - (self, "value"), - transform=lambda pk: None if pk is self._NO_PROCESS else str(pk), - ) - - self.refresh_work_chains_button = ipw.Button(description="Refresh") - self.refresh_work_chains_button.on_click(self.refresh_work_chains) - - self._refresh_lock = Lock() - self._refresh_thread = None - self._stop_refresh_thread = Event() - self._update_auto_refresh_thread_state() - - super().__init__( - children=[ - self.work_chains_prompt, - self.work_chains_selector, - self.refresh_work_chains_button, - ], - **kwargs, - ) - - @dataclass - class WorkChainData: - pk: int - ctime: str - state: str - element: str - - @classmethod - def find_work_chains(cls): - builder = CalculationQueryBuilder() - filters = builder.get_filters( - process_label="VerificationWorkChain", - ) - query_set = builder.get_query_set( - filters=filters, - order_by={"ctime": "desc"}, - ) - projected = builder.get_projected( - query_set, projections=["pk", "ctime", "state"] - ) - - for process in projected[1:]: - pk = process[0] - element = load_node(pk).inputs.pseudo.element - yield cls.WorkChainData(element=element, *process) - - @traitlets.default("busy") - def _default_busy(self): - return True - - @traitlets.observe("busy") - def _observe_busy(self, change): - for child in self.children: - child.disabled = change["new"] - - def refresh_work_chains(self, _=None): - with self._refresh_lock: - try: - self.set_trait("busy", True) # disables the widget - - with self.hold_trait_notifications(): - # We need to restore the original value, because it may be reset due to this issue: - # https://github.com/jupyter-widgets/ipywidgets/issues/2230 - original_value = self.work_chains_selector.value - - self.work_chains_selector.options = [ - ("New calculation...", self._NO_PROCESS) - ] + [ - (self.FMT_WORKCHAIN.format(wc=wc), wc.pk) - for wc in self.find_work_chains() - ] - - self.work_chains_selector.value = original_value - finally: - self.set_trait("busy", False) # reenable the widget - - def _auto_refresh_loop(self): - while True: - self.refresh_work_chains() - if self._stop_refresh_thread.wait(timeout=self.auto_refresh_interval): - break - - def _update_auto_refresh_thread_state(self): - if self.auto_refresh_interval > 0 and self._refresh_thread is None: - # start thread - self._stop_refresh_thread.clear() - self._refresh_thread = Thread(target=self._auto_refresh_loop) - self._refresh_thread.start() - - elif self.auto_refresh_interval <= 0 and self._refresh_thread is not None: - # stop thread - self._stop_refresh_thread.set() - self._refresh_thread.join(timeout=30) - self._refresh_thread = None - - @traitlets.default("auto_refresh_interval") - def _default_auto_refresh_interval(self): - return 10 # seconds - - @traitlets.observe("auto_refresh_interval") - def _observe_auto_refresh_interval(self, change): - if change["new"] != change["old"]: - self._update_auto_refresh_thread_state() - - @traitlets.observe("value") - def _observe_value(self, change): - if change["old"] == change["new"]: - return - - new = self._NO_PROCESS if change["new"] is None else change["new"] - - if new not in {pk for _, pk in self.work_chains_selector.options}: - self.refresh_work_chains() - - self.work_chains_selector.value = new diff --git a/aiidalab_sssp/setup_codes.py b/aiidalab_sssp/setup_codes.py deleted file mode 100644 index bcc54bf..0000000 --- a/aiidalab_sssp/setup_codes.py +++ /dev/null @@ -1,375 +0,0 @@ -# -*- coding: utf-8 -*- -from pathlib import Path -from shutil import which -from subprocess import CalledProcessError, run -from threading import Event, Thread -from time import time - -import ipywidgets as ipw -import traitlets -from aiida.common.exceptions import NotExistent -from aiida.orm import load_code -from filelock import FileLock, Timeout - -__all__ = [ - "QESetupWidget", -] - -FN_LOCKFILE = Path.home().joinpath(".install-qe-on-localhost.lock") -FN_DO_NOT_SETUP = Path.cwd().joinpath(".do-not-setup-on-localhost") - -QE_VERSION = "6.7" - -CONDA_ENV_PREFIX = Path.home().joinpath( - ".conda", "envs", f"quantum-espresso-{QE_VERSION}" -) - -CODE_NAMES = ("pw", "ph") - - -def qe_installed(): - return CONDA_ENV_PREFIX.exists() - - -def install_qe(): - run( - [ - "conda", - "create", - "--yes", - "--override-channels", - "--channel", - "conda-forge", - "--prefix", - str(CONDA_ENV_PREFIX), - f"qe={QE_VERSION}", - ], - capture_output=True, - check=True, - ) - - -def _code_is_setup(name): - try: - load_code(f"{name}-{QE_VERSION}@localhost") - except NotExistent: - return False - else: - return True - - -def codes_are_setup(): - return all(_code_is_setup(code_name) for code_name in CODE_NAMES) - - -def _setup_code(code_name, computer_name="localhost"): - try: - load_code(f"{code_name}-{QE_VERSION}@localhost") - except NotExistent: - run( - [ - "verdi", - "code", - "setup", - "--non-interactive", - "--label", - f"{code_name}-{QE_VERSION}", - "--description", - f"{code_name}.x ({QE_VERSION}) setup by AiiDAlab.", - "--input-plugin", - f"quantumespresso.{code_name}", - "--computer", - computer_name, - "--prepend-text", - f"conda activate {CONDA_ENV_PREFIX}\nexport OMP_NUM_THREADS=1", - "--remote-abs-path", - CONDA_ENV_PREFIX.joinpath("bin", f"{code_name}.x"), - ], - check=True, - capture_output=True, - ) - else: - raise RuntimeError(f"Code {code_name} (v{QE_VERSION}) is already setup!") - - -def setup_codes(): - for code_name in CODE_NAMES: - _setup_code(code_name) - - -class QESetupWidget(ipw.VBox): - - installed = traitlets.Bool(allow_none=True).tag(readonly=True) - busy = traitlets.Bool().tag(readonly=True) - error = traitlets.Unicode().tag(readonly=True) - - def __init__(self, prefix=None, hide_by_default=True, auto_start=True, **kwargs): - self.prefix = prefix or f"QuantumESPRESSO (v{QE_VERSION}) @localhost: " - self.hide_by_default = hide_by_default - - self._progress_bar = ProgressBar( - description=self.prefix, - description_layout=ipw.Layout(min_width="300px"), - layout=ipw.Layout(width="auto", flex="1 1 auto"), - ) - - self._info_toggle_button = ipw.ToggleButton( - icon="info-circle", - disabled=True, - layout=ipw.Layout(width="36px"), - ) - self._info_toggle_button.observe(self._toggle_error_view, "value") - - self._reinstall_button = ipw.Button( - icon="cogs", - disabled=True, - description="Install codes...", - tooltip="Start another installation attempt.", - ) - self._reinstall_button.on_click(self._trigger_reinstall) - - self._error_output = ipw.HTML() - - super().__init__( - [ - ipw.HBox( - [self._progress_bar, self._info_toggle_button], - layout=ipw.Layout(width="auto"), - ), - ], - **kwargs, - ) - - if auto_start: - self.refresh() - - def set_message(self, msg): - self._progress_bar.description = f"{self.prefix}{msg}" - - def _refresh_installed(self): - AnimationRate = ProgressBar.AnimationRate # alias - conda_installed = which("conda") - - self.set_message("checking installation status...") - try: - self.set_trait("busy", True) - - # Check for "do not install file" and skip actual check. The purpose of - # this file is to not re-try this process on every app start in case - # that there are issues. - if FN_DO_NOT_SETUP.exists(): - self.set_message("Installation previously failed.") - self.error = "Installation failed in previous attempt." - return - - try: - with FileLock(FN_LOCKFILE, timeout=5): - # We assume that if the codes are already setup, everything - # is in order. Only if they are not present, should we take - # action, however we only do so if the environment has a - # conda binary present (`which conda`). If that is not the - # case then we assume that this is a custom user environment - # in which case we also take no further action. - self.installed = codes_are_setup() or not conda_installed - if self.installed: - self.error = "" - self.set_message("Codes are installed.") - else: - self.error = "" - self.set_message("installing...") - # To setup our own codes, we install QE on the local - # host: - if not qe_installed(): - self.set_message("Installing QE...") - self._progress_bar.value = AnimationRate(0.05) - try: - install_qe() - except CalledProcessError as error: - raise RuntimeError( - f"Failed to create conda environment: {error}" - ) - self.value = 0.7 - # After installing QE, we install the corresponding - # AiiDA codes: - for i, code_name in enumerate(CODE_NAMES): - if not _code_is_setup(code_name): - self.set_message( - f"Setting up AiiDA code ({code_name})..." - ) - self._progress_bar.value = AnimationRate(0.1) - _setup_code(code_name) - self.value = 0.8 + i * 0.1 - # After going through the installation procedure, we - # expect both our version of QE to be installed, as well - # as the codes to be setup. - self.installed = qe_installed() and codes_are_setup() - - except Timeout: - # assume that the installation was triggered by a different - # process - self.set_message("installing...") - self._progress_bar.value = AnimationRate(0.01) - with FileLock(FN_LOCKFILE, timeout=120): - self.installed = codes_are_setup() or not conda_installed - - # Raise error in case that the installation was not successful - # either in this process or a different one. - if not self.installed: - raise RuntimeError("Installation failed for unknown reasons.") - - except Exception as error: - self.set_message("Failed to setup QE on localhost.") - self.set_trait("error", str(error)) - FN_DO_NOT_SETUP.touch() - else: - self.set_message("OK") - finally: - self.set_trait("busy", False) - - def refresh(self): - thread = Thread(target=self._refresh_installed) - thread.start() - - @traitlets.default("installed") - def _default_installed(self): - return None - - @traitlets.default("busy") - def _default_busy(self): - return False - - @traitlets.default("failed") - def _default_error(self): - return "" - - @traitlets.observe("error") - def _observe_error(self, change): - with self.hold_trait_notifications(): - self._error_output.value = f""" -
-

Failed to setup QE on localhost, due to error:

- -

{change["new"]}

- -
-

This means you have to setup QE manually to run it on this host. - You can safely ignore this message if you do not plan on running - QuantumESPRESSO calculations directly on the localhost. Alternatively - you could try to make another installation attempt via the button - below.

- """ - self._info_toggle_button.disabled = not bool(change["new"]) - self._reinstall_button.disabled = not change["new"] - if not change["new"]: - self._info_toggle_button.value = False - - def _toggle_error_view(self, change): - self.children = [self.children[0]] + ( - [self._error_output, self._reinstall_button] if change["new"] else [] - ) - - @traitlets.observe("busy") - @traitlets.observe("error") - @traitlets.observe("installed") - def _update(self, change): - with self.hold_trait_notifications(): - if self.hide_by_default: - self.layout.visibility = ( - "visible" if (self.busy or self.error) else "hidden" - ) - - if self.error or self.installed: - self._progress_bar.value = 1.0 - - self._progress_bar.bar_style = ( - "info" - if self.busy - else ( - "warning" - if self.error - else {True: "success", False: ""}.get(self.installed, "") - ) - ) - - def _trigger_reinstall(self, _=None): - FN_DO_NOT_SETUP.unlink() - self.refresh() - - -class ProgressBar(ipw.HBox): - class AnimationRate(float): - pass - - description = traitlets.Unicode() - value = traitlets.Union([traitlets.Float(), traitlets.Instance(AnimationRate)]) - bar_style = traitlets.Unicode() - - _animation_rate = traitlets.Float() - - def __init__(self, description_layout=None, *args, **kwargs): - if description_layout is None: - description_layout = ipw.Layout(width="auto", flex="2 1 auto") - - self._label = ipw.Label(layout=description_layout) - self._progress_bar = ipw.FloatProgress( - min=0, max=1.0, layout=ipw.Layout(width="auto", flex="1 1 auto") - ) - - traitlets.link((self, "description"), (self._label, "value")) - traitlets.link((self, "bar_style"), (self._progress_bar, "bar_style")) - - self._animate_stop_event = Event() - self._animate_thread = None - - super().__init__([self._label, self._progress_bar], *args, **kwargs) - - def _animate(self, refresh_rate=0.01): - - v0 = self._progress_bar.value - t0 = time() - - while not self._animate_stop_event.wait(refresh_rate): - self._progress_bar.value = (v0 + (time() - t0) * self._animation_rate) % 1.0 - - def _start_animate(self): - if self._animate_thread is not None: - raise RuntimeError("Cannot start animation more than once!") - - self._animate_thread = Thread(target=self._animate) - self._animate_thread.start() - - def _stop_animate(self): - self._animate_stop_event.set() - self._animate_thread.join() - self._animate_stop_event.clear() - self._animate_thread = None - - @traitlets.default("_animation_rate") - def _default_animation_rate(self): - return 0 - - @traitlets.observe("_animation_rate") - def _observe_animation_rate(self, change): - if change["new"] and not change["old"]: - self._start_animate() - elif not change["new"] and change["old"]: - self._stop_animate() - - @traitlets.validate("value") - def _validate_value(self, proposal): - if isinstance(proposal["value"], self.AnimationRate): - if proposal["value"] < 0: - raise traitlets.TraitError("The animation rate must be non-negative.") - - elif not 0 <= proposal["value"] <= 1.0: - raise traitlets.TraitError("The value must be between 0 and 1.0.") - - return proposal["value"] - - @traitlets.observe("value") - def _observe_value(self, change): - if isinstance(change["new"], self.AnimationRate): - self._animation_rate = change["new"] - else: - self._animation_rate = 0 - self._progress_bar.value = change["new"] diff --git a/aiidalab_sssp/static/__init__.py b/aiidalab_sssp/static/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/aiidalab_sssp/static/style.css b/aiidalab_sssp/static/style.css deleted file mode 100644 index 446bcf7..0000000 --- a/aiidalab_sssp/static/style.css +++ /dev/null @@ -1,42 +0,0 @@ -:root { - --lab-blue: #2097F3; - --lab-background: #d3ecff; -} - -table { - font-family: arial, sans-serif; - border-collapse: collapse; - border:1px solid #bbbbbb; - width: 100%; -} -h3 { - margin-top: 0px; -} -td, tr { - border-left: 2px solid var(--lab-blue); - text-align: left; - padding: 8px; -} -tr { - background-color: var(--lab-background); -} -tr:nth-child(even) { - background-color: #ffffff; -} -td:nth-child(even) { - border-left: 0px; -} -/* Create two equal columns that floats next to each other */ -.column { - float: left; - width: 50%; - padding: 10px; - height: 300px; /* Should be removed. Only for demonstration */ -} - -/* Clear floats after the columns */ -.row:after { - content: ""; - display: table; - clear: both; -} diff --git a/aiidalab_sssp/static/welcome.jinja b/aiidalab_sssp/static/welcome.jinja deleted file mode 100644 index 50fb17a..0000000 --- a/aiidalab_sssp/static/welcome.jinja +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - -
-

Welcome to the AiiDAlab SSSP app! 👋

- - The SSSP app is a graphical interface for verification the pseudopotential under certain procedures to criticise the accuracy and convergence property of a pseudopotential. - Each of the properties is calculated by workflows powered by the AiiDA engine, and maintained in the SSSP plugin for AiiDA. For details about how the accuracy and convergence calculations are running, please check the documentation of protocols. - -

The SSSP app allows you to verify pseudopotential properties in a simple 4-step process:

- -
    -
  1. 🔍 Step 1: Select the pseudopotetial file you want to run.
  2. -
  3. ⚙️ Step 2: Select the properties you are interested in.
  4. -
  5. ℹ️ Step 3: Set the metadata of pseudopotential.
  6. -
  7. 🚀 Step 4: Submit your workflow!
  8. -
- -

If you want to run the verification on many pseudopotential files, -we recommond to run it through aiida-sssp-workflow plugin. -Please check the documentation on how to run it with aiida and aiida-sssp-workflow installed. -

- -

Happy pseudoing! 🎉

-
- - diff --git a/aiidalab_sssp/steps.py b/aiidalab_sssp/steps.py deleted file mode 100644 index 7fb60a3..0000000 --- a/aiidalab_sssp/steps.py +++ /dev/null @@ -1,1210 +0,0 @@ -"""widget for pseudo inmport""" -import io -import os - -import ipywidgets as ipw -import traitlets -from aiida import orm -from aiida.common import NotExistent -from aiida.engine import ProcessState -from aiida.orm import Node, load_code, load_node -from aiida.plugins import DataFactory, WorkflowFactory -from aiida_sssp_workflow.workflows.verifications import DEFAULT_PROPERTIES_LIST -from aiidalab_widgets_base import ( - ComputationalResourcesWidget, - ProcessMonitor, - ProcessNodesTreeWidget, - WizardAppWidgetStep, - viewer, -) -from IPython.display import clear_output, display - -from aiidalab_sssp.parameters import DEFAULT_PARAMETERS - -UpfData = DataFactory("pseudo.upf") -VerificationWorkChain = WorkflowFactory("sssp_workflow.verification") - - -class PseudoUploadWidget(ipw.VBox): - """Class that allows to upload pseudopotential from user's computer.""" - - pseudo = traitlets.Tuple() - error_message = traitlets.Unicode() - - def __init__(self, title="", description="Upload Pseudopotential"): - self.title = title - self.file_upload = ipw.FileUpload( - description=description, multiple=False, layout={"width": "initial"} - ) - supported_formats = ipw.HTML( - """ -Supported pseudo formats (Now only support UPF type) -""" - ) - self.file_upload.observe(self._on_file_upload, names="value") - self.error_message = "" - super().__init__(children=[self.file_upload, supported_formats]) - - def _on_file_upload(self, change=None): - """When file upload button is pressed.""" - fname, item = next(iter(change["new"].items())) - try: - content = item["content"] - - # Order matters make sure when pseudo change - # the pseudo_filename is set - with self.hold_trait_notifications(): - self.pseudo = (fname, UpfData(io.BytesIO(content))) - except ValueError: - self.error_message = ( - "wrong pseudopotential file type. (Only UPF support now)" - ) - - -class PseudoSelectionStep(ipw.VBox, WizardAppWidgetStep): - """ - Upload a pesudopotential and store it as UpfData in database - """ - - confirmed_pseudo = traitlets.Tuple(allow_none=True) - - def __init__(self, **kwargs): - self.pseudo_upload = PseudoUploadWidget() - self.pseudo_upload.observe(self._observe_pseudo_upload, "pseudo") - - self.description = ipw.HTML( - """ -

Select a pseudopotential from one of the following sources and then - click "Confirm" to go to the next step.

Currently only UPF pseudopotential file are - supported. - """ - ) - - self.pseudo_text = ipw.Text( - placeholder="[No pseudo selected]", - description="Selected:", - disabled=True, - layout=ipw.Layout(width="auto"), - ) - - self.confirm_button = ipw.Button( - description="Confirm", - tooltip="Confirm the currently selected pseudopotential and go to the next step.", - button_style="success", - icon="check-circle", - disabled=True, - layout=ipw.Layout(width="auto"), - ) - self.confirm_button.on_click(self.confirm) - self.message_area = ipw.HTML() - - super().__init__( - children=[ - self.description, - self.pseudo_upload, - self.pseudo_text, - self.message_area, - self.confirm_button, - ], - **kwargs, - ) - - @traitlets.default("state") - def _default_state(self): - return self.State.INIT - - def _update_state(self): - if self.pseudo_text is None: - self.state = self.State.READY - else: - if self.confirmed_pseudo: - self.state = self.State.SUCCESS - self.confirm_button.disabled = True - else: - self.state = self.State.CONFIGURED - self.confirm_button.disabled = False - - def _observe_pseudo_upload(self, _): - with self.hold_trait_notifications(): - if self.pseudo_upload.pseudo is None: - self.message_area.value = self.pseudo_upload.error_message - else: - # Upload then set pseudo and show filename on text board - self.pseudo_text.value = self.pseudo_upload.pseudo[0] - - if self.pseudo_upload.error_message: - self.message_area.value = self.pseudo_upload.error_message - - self._update_state() - - @traitlets.observe("confirmed_pseudo") - def _observe_confirmed_pseudo(self, _): - with self.hold_trait_notifications(): - self._update_state() - - def can_reset(self): - return self.confirmed_pseudo is not None - - def confirm(self, _=None): - self.confirmed_pseudo = self.pseudo_upload.pseudo - - self._update_state() - - def reset(self): # unconfirm - self.confirmed_pseudo = None - - self._update_state() - - -class WorkChainSettings(ipw.VBox): - - calc_type_help = ipw.HTML( - """
- The acwf protocol is used to set the parameters used for pseudopotential - verification. - The acwf calculation protocol represents a set of parameters compatible - with aiida-common-workflow. - Three different calculation type are provided for verification, -
    -
  1. ⚙️ Standard: production mode that run verification on a thourgh cutoff test.
  2. -
  3. ⚙️ Quick: quick mode is design to run quickly with sparse cutoff test sample points.
  4. -
  5. 🔍 Precheck: precheck is for running a pre-check verification to decide whether 200 Ry as reference is enough if not it is not valuable to run futher verification on smaller cutoff and whether small referece cutoff can be used for production verification.
  6. -
- The criteria determine when the wavefunction and - charge density cutoff tests are converged. - Choose the "efficiency" protocol for cutoff test to give a efficiency - pseudopotential. The "precision" protocol that provides more - accuracy pseudopotential but will take longer. -
""" - ) - properties_list = traitlets.List() - - def __init__(self, **kwargs): - - # Accuracy properties - self.delta_measure = ipw.Checkbox( - description="", - tooltip="Calculate the delta measure w.r.t AE.", - indent=False, - value=True, - layout=ipw.Layout(max_width="10%"), - ) - self.bands_measure = ipw.Checkbox( - description="", - tooltip="Calculate the bands measure for bands distance and bandstructure.", - indent=False, - value=True, - layout=ipw.Layout(max_width="10%"), - ) - - self.delta_measure.observe(self._update_properties_list, "value") - self.bands_measure.observe(self._update_properties_list, "value") - - # Convergenece properties - self.cohesive_energy_convergence = ipw.Checkbox( - description="", - tooltip="Convergence test on cohesive energy.", - indent=False, - value=True, - layout=ipw.Layout(max_width="10%"), - ) - self.pressure_convergence = ipw.Checkbox( - description="", - tooltip="Convergence test on pressue.", - indent=False, - value=True, - layout=ipw.Layout(max_width="10%"), - ) - self.phonon_frequencies_convergence = ipw.Checkbox( - description="", - tooltip="Convergence test on phonon frequencies.", - indent=False, - value=True, - layout=ipw.Layout(max_width="10%"), - ) - self.delta_convergence = ipw.Checkbox( - description="", - tooltip="Convergence test on delta factor.", - indent=False, - value=True, - layout=ipw.Layout(max_width="10%"), - ) - self.bands_convergence = ipw.Checkbox( - description="", - tooltip="Convergence test on bands distance.", - indent=False, - value=True, - layout=ipw.Layout(max_width="10%"), - ) - - self.cohesive_energy_convergence.observe(self._update_properties_list, "value") - self.pressure_convergence.observe(self._update_properties_list, "value") - self.phonon_frequencies_convergence.observe( - self._update_properties_list, "value" - ) - self.delta_convergence.observe(self._update_properties_list, "value") - self.bands_convergence.observe(self._update_properties_list, "value") - - self.properties_list = DEFAULT_PROPERTIES_LIST - - # Work chain calc_type - self.calc_type = ipw.ToggleButtons( - options=["standard", "quick", "precheck"], - value="standard", - ) - - self.criteria = ipw.ToggleButtons( - options=["efficiency", "precision"], - value="efficiency", - ) - - super().__init__( - children=[ - ipw.HTML("Properties to verified - For accuracy of pseudopotential:"), - ipw.HBox( - children=[ipw.HTML("Delta measure"), self.delta_measure] - ), - ipw.HBox( - children=[ipw.HTML("Bands measure"), self.bands_measure] - ), - ipw.HTML( - "Properties to verified - For convergence of pseudopotential:" - ), - ipw.HBox( - children=[ - ipw.HTML("Convergence: cohesive energy"), - self.cohesive_energy_convergence, - ] - ), - ipw.HBox( - children=[ - ipw.HTML("Convergence: phonon frequencies"), - self.phonon_frequencies_convergence, - ] - ), - ipw.HBox( - children=[ - ipw.HTML("Convergence: pressure"), - self.pressure_convergence, - ] - ), - ipw.HBox( - children=[ - ipw.HTML("Convergence: delta"), - self.delta_convergence, - ] - ), - ipw.HBox( - children=[ - ipw.HTML("Convergence: bands"), - self.bands_convergence, - ] - ), - # calculation type setup - self.calc_type_help, - ipw.HTML( - "Select calculation type:", - layout=ipw.Layout(flex="1 1 auto"), - ), - self.calc_type, - ipw.HTML("Select criteria:", layout=ipw.Layout(flex="1 1 auto")), - self.criteria, - ], - **kwargs, - ) - - def _update_properties_list(self, _): - lst = [] - if self.delta_measure.value: - lst.append("accuracy.delta") - - if self.bands_measure.value: - lst.append("accuracy.bands") - - if self.cohesive_energy_convergence.value: - lst.append("convergence.cohesive_energy") - - if self.phonon_frequencies_convergence.value: - lst.append("convergence.phonon_frequencies") - - if self.pressure_convergence.value: - lst.append("convergence.pressure") - - if self.delta_convergence.value: - lst.append("convergence.delta") - - if self.bands_convergence.value: - lst.append("convergence.bands") - - self.properties_list = lst - - -class ConfigureSsspWorkChainStep(ipw.VBox, WizardAppWidgetStep): - - confirmed = traitlets.Bool() - previous_step_state = traitlets.UseEnum(WizardAppWidgetStep.State) - workchain_settings = traitlets.Instance(WorkChainSettings, allow_none=True) - - def __init__(self, **kwargs): - self.workchain_settings = WorkChainSettings() - self.workchain_settings.delta_measure.observe(self._update_state, "value") - self.workchain_settings.bands_measure.observe(self._update_state, "value") - self.workchain_settings.cohesive_energy_convergence.observe( - self._update_state, "value" - ) - self.workchain_settings.phonon_frequencies_convergence.observe( - self._update_state, "value" - ) - self.workchain_settings.pressure_convergence.observe( - self._update_state, "value" - ) - self.workchain_settings.bands_convergence.observe(self._update_state, "value") - self.workchain_settings.delta_convergence.observe(self._update_state, "value") - - self._submission_blocker_messages = ipw.HTML() - - self.confirm_button = ipw.Button( - description="Confirm", - tooltip="Confirm the currently selected settings and go to the next step.", - button_style="success", - icon="check-circle", - disabled=True, - layout=ipw.Layout(width="auto"), - ) - - self.confirm_button.on_click(self.confirm) - - super().__init__( - children=[ - self.workchain_settings, - self._submission_blocker_messages, - self.confirm_button, - ], - **kwargs, - ) - - @traitlets.observe("previous_step_state") - def _observe_previous_step_state(self, change): - self._update_state() - - def set_input_parameters(self, parameters): - """Set the inputs in the GUI based on a set of parameters.""" - with self.hold_trait_notifications(): - # Wor chain settings - self.workchain_settings.delta_measure.value = parameters["delta_measure"] - self.workchain_settings.bands_measure.value = parameters["bands_measure"] - self.workchain_settings.cohesive_energy_convergence.value = parameters[ - "cohesive_energy_convergence" - ] - self.workchain_settings.phonon_frequencies_convergence.value = parameters[ - "phonon_frequencies_convergence" - ] - self.workchain_settings.pressure_convergence.value = parameters[ - "pressure_convergence" - ] - self.workchain_settings.delta_convergence.value = parameters[ - "delta_convergence" - ] - self.workchain_settings.bands_convergence.value = parameters[ - "bands_convergence" - ] - self.workchain_settings.criteria.value = parameters["criteria"] - self.workchain_settings.calc_type.value = parameters["calc_type"] - - def _update_state(self, _=None): - if self.previous_step_state == self.State.SUCCESS: - self.confirm_button.disabled = False - if not ( - self.workchain_settings.delta_measure.value - or self.workchain_settings.bands_measure.value - or self.workchain_settings.cohesive_energy_convergence.value - or self.workchain_settings.phonon_frequencies_convergence.value - or self.workchain_settings.pressure_convergence.value - or self.workchain_settings.delta_convergence.value - or self.workchain_settings.bands_convergence.value - ): - self.confirm_button.disabled = True - self.state = self.State.READY - self._submission_blocker_messages.value = """ -
- The configured work chain would not actually compute anything. - Select either at least one of the - the delta measure or the convergence calculations or both.
""" - else: - self._submission_blocker_messages.value = "" - self.state = self.State.CONFIGURED - elif self.previous_step_state == self.State.FAIL: - self.state = self.State.FAIL - else: - self.confirm_button.disabled = True - self.state = self.State.INIT - self.set_input_parameters(DEFAULT_PARAMETERS) - - def confirm(self, _None): - self.confirm_button.disabled = True - self.state = self.State.SUCCESS - - @traitlets.default("state") - def _default_state(self): - return self.State.INIT - - def reset(self): - with self.hold_trait_notifications(): - self.set_input_parameters(DEFAULT_PARAMETERS) - - -class MetadataSettings(ipw.VBox): - """ - This is the widget for storing the settings of pseudopotential - metadata. The part of the metadatas can be read from the input - pseudopotential file. User also allowed to set or modified them. - It will impact the primary key of how the verification result is - stored. - - Currently the primary key simply a string has the format: - ...... - - In the future this should be a formatted class of data that can be used - to identify the pseudopotentials. - """ - - pseudo = traitlets.Tuple(allow_none=True) - output_metadata = traitlets.Dict() - - def __init__(self, **kwargs): - extra = { - "style": {"description_width": "180px"}, - "layout": {"min_width": "310px"}, - } - - self.family = ipw.Text( - placeholder="Unresolved", - description="Pseudopotential family:", - **extra, - ) - self.gen_tool = ipw.Text( - placeholder="Unresolved", - description="Pseudopotential tool:", - **extra, - ) - self.version = ipw.Text( - placeholder="Unresolved", - description="Pseudopotential version:", - **extra, - ) - - self.family.observe(self._on_tag_change) - self.gen_tool.observe(self._on_tag_change) - self.version.observe(self._on_tag_change) - - self.description = ipw.Textarea( - placeholder="Optional", - description="Description:", - **extra, - ) - - super().__init__( - children=[ - self.family, - self.gen_tool, - self.version, - self.description, - ], - **kwargs, - ) - - def _on_tag_change(self, _): - """family/gen_tool/version/extra change. reset output label""" - self.output_metadata["family"] = self.family.value - self.output_metadata["tool"] = self.gen_tool.value - self.output_metadata["version"] = self.version.value - - @traitlets.observe("pseudo") - def _observe_pseudo(self, _): - from pseudo_parser.upf_parser import parse - - from aiidalab_sssp.inspect import parse_label - - try: - # when upload a standard named input - label = ".".join(self.pseudo[0].split(".")[:-1]) - label_dict = parse_label(label) - except IndexError: - # tuple index out of range. Pseudo not upload yet. - self.output_metadata = {} - except Exception: - # when upload a non-standard named input upf - pseudo_info = parse(self.pseudo[1].get_content()) - - self.output_metadata = { - "element": pseudo_info["element"], - "type": pseudo_info["pp_type"], - "z_valence": pseudo_info["z_valence"], - "family": self.family.value, - "tool": self.gen_tool.value, - "version": self.version.value, - } - else: - self.output_metadata = { - "element": label_dict["element"], - "type": label_dict["type"], - "z_valence": label_dict["z"], - "family": label_dict["family"], - "tool": label_dict["tool"], - "version": label_dict["version"], - } - self.family.value = label_dict["family"] - self.gen_tool.value = label_dict["tool"] - self.version.value = label_dict["version"] - - -class SettingPseudoMetadataStep(ipw.VBox, WizardAppWidgetStep): - """setting the extra metadata for the future query and description display of the pseudo""" - - previous_step_state = traitlets.UseEnum(WizardAppWidgetStep.State) - pseudo = traitlets.Tuple(allow_none=True) - confirmed = traitlets.Bool() - output_label = traitlets.Unicode() - - metadata_help = ipw.HTML( - """
-

There is no general rule of thumb on how to name the extra metadata.

-

The family and version field deduct from input pseudopotential.

-

In general:

-
    -
  • family indicate the library, sg15, gbrv etc.
  • -
  • version of library.
  • -
  • tool used to generate pseudopotential.
  • -
  • extra label is optional append to the label.
  • -
  • description of node.
  • -
-
""" - ) - - _TITLE_PLACEHODER = "

No pseudopotential detect

" - - def __init__(self, **kwargs): - self.title = ipw.HTML(self._TITLE_PLACEHODER) - - self.metadata_settings = MetadataSettings() - self.metadata_settings.observe(self._on_metadata_settings_change) - - self._submission_blocker_messages = ipw.HTML() - - self.confirm_button = ipw.Button( - description="Confirm", - tooltip="Confirm the currently metadata settings and go to the next step.", - button_style="success", - icon="check-circle", - disabled=True, - layout=ipw.Layout(width="auto"), - ) - - self.confirm_button.on_click(self.confirm) - - super().__init__( - children=[ - self.title, - ipw.HBox( - children=[self.metadata_settings, self.metadata_help], - layout=ipw.Layout(justify_content="space-between"), - ), - self._submission_blocker_messages, - self.confirm_button, - ], - **kwargs, - ) - - @traitlets.observe("pseudo") - def _on_pseudo_change(self, _): - self.metadata_settings.pseudo = self.pseudo - self._update_title() - - def _on_metadata_settings_change(self, _): - # FIXME: not triggered.??? - self._update_title() - - def _update_title(self): - label = self._get_label_from_metadata() - self.title.value = f"

The standard label of psedopotential is: {label}

" - - def _get_label_from_metadata(self): - metadata = self.metadata_settings.output_metadata - - try: - element = metadata["element"] - psp_type = metadata["type"] - z_valence = metadata["z_valence"] - family = metadata["family"] - tool = metadata["tool"] - version = metadata["version"] - - output_label = f"{element}.{psp_type}.{z_valence}.{tool}.{family}.{version}" - except KeyError: - # the metadata not set - output_label = "Not set." - - return output_label - - def _update_state(self, _=None): - if self.previous_step_state == self.State.SUCCESS: - self.confirm_button.disabled = False - elif self.previous_step_state == self.State.FAIL: - self.state = self.State.FAIL - else: - self.state = self.State.INIT - - @traitlets.observe("previous_step_state") - def _observe_previous_step_state(self, _): - self._update_state() - - def confirm(self, _): - for key in ["element", "type", "family", "z_valence", "tool", "version"]: - if key not in self.metadata_settings.output_metadata: - self.state = self.State.READY - self._submission_blocker_messages.value = f""" -
- {key} is not set for labelling.
""" - else: - self._submission_blocker_messages.value = "" - self.output_label = self._get_label_from_metadata() - self.confirm_button.disabled = True - self.state = self.State.SUCCESS - - @traitlets.default("state") - def _default_state(self): - return self.State.INIT - - -class ResourceSelectionWidget(ipw.VBox): - """Widget for the selection of compute resources.""" - - title = ipw.HTML( - """
-

Resources

-
""" - ) - prompt = ipw.HTML( - """
-

- Specify the resources to use for the pw.x calculation. -

""" - ) - - def __init__(self, **kwargs): - extra = { - "style": {"description_width": "150px"}, - "layout": {"min_width": "180px"}, - } - self.num_nodes = ipw.BoundedIntText( - value=1, step=1, min=1, max=1000, description="Nodes", **extra - ) - self.num_cpus = ipw.BoundedIntText( - value=1, step=1, min=1, description="CPUs", **extra - ) - - super().__init__( - children=[ - self.title, - ipw.HBox( - children=[self.prompt, self.num_nodes, self.num_cpus], - layout=ipw.Layout(justify_content="space-between"), - ), - ] - ) - - def reset(self): - self.num_nodes.value = 1 - self.num_cpus.value = 1 - - -class ParallelizationSettings(ipw.VBox): - """Widget for setting the parallelization settings.""" - - title = ipw.HTML( - """
-

Parallelization

-
""" - ) - prompt = ipw.HTML( - """
-

- Specify the number of k-points pools for the calculations. -

""" - ) - - def __init__(self, **kwargs): - extra = { - "style": {"description_width": "150px"}, - "layout": {"min_width": "180px"}, - } - self.npools = ipw.BoundedIntText( - value=1, step=1, min=1, max=128, description="Number of k-pools", **extra - ) - super().__init__( - children=[ - self.title, - ipw.HBox( - children=[self.prompt, self.npools], - layout=ipw.Layout(justify_content="space-between"), - ), - ] - ) - - def reset(self): - self.npools.value = 1 - - -class SubmitSsspWorkChainStep(ipw.VBox, WizardAppWidgetStep): - """step of submit verification""" - - pseudo = traitlets.Tuple(allow_none=True) - value = traitlets.Unicode(allow_none=True) - previous_step_state = traitlets.UseEnum(WizardAppWidgetStep.State) - pseudo_label = traitlets.Unicode() - workchain_settings = traitlets.Instance(WorkChainSettings, allow_none=True) - - _submission_blockers = traitlets.List(traitlets.Unicode) - - # Since for production it is now the only protocol - _PROTOCOL = "acwf" - - codes_title = ipw.HTML( - """
-

Codes

""" - ) - codes_help = ipw.HTML( - """
Select the code to use for running the calculations. Please - setup to run verification on the cluster with more than 16 cores. - Otherwise the localhost resource will fully occupied and stuck. You can - configure new ones on machines by clicking on - "Setup new code".
""" - ) - - def __init__(self, **kwargs): - self.pw_code = ComputationalResourcesWidget( - description="pw.x:", input_plugin="quantumespresso.pw" - ) - self.ph_code = ComputationalResourcesWidget( - description="ph.x:", - input_plugin="quantumespresso.ph", - ) - - self._submission_blocker_messages = ipw.HTML("") - - self.pw_code.observe(self._update_state, "value") - self.pw_code.observe(self._update_resources, "value") - - self.ph_code.observe(self._update_state, "value") - self.ph_code.observe(self._update_resources, "value") - - self.resources_config = ResourceSelectionWidget() - self.parallelization = ParallelizationSettings() - - self.submit_button = ipw.Button( - description="Submit", - tooltip="Submit the calculation with the selected parameters.", - icon="play", - button_style="success", - layout=ipw.Layout(width="auto", flex="1 1 auto"), - disabled=True, - ) - - self.submit_button.on_click(self._on_submit_button_clicked) - - # After all self variable set - self.set_resource_defaults() - - super().__init__( - children=[ - self.codes_title, - self.codes_help, - self.pw_code, - self.ph_code, - self.resources_config, - self.parallelization, - self._submission_blocker_messages, - self.submit_button, - ], - **kwargs, - ) - - @traitlets.observe("state") - def _observe_state(self, change): - with self.hold_trait_notifications(): - self.disabled = change["new"] not in ( - self.State.READY, - self.State.CONFIGURED, - ) - self.submit_button.disabled = change["new"] != self.State.CONFIGURED - - def _update_resources(self, change): - if change["new"] and ( - change["old"] is None - or change["new"].computer.pk != change["old"].computer.pk - ): - self.set_resource_defaults(change["new"].computer) - - def set_resource_defaults(self, computer=None): - - if computer is None or computer.hostname == "localhost": - self.resources_config.num_nodes.disabled = True - self.resources_config.num_nodes.value = 1 - self.resources_config.num_cpus.max = os.cpu_count() - self.resources_config.num_cpus.value = 1 - self.resources_config.num_cpus.description = "CPUs" - self.parallelization.npools.value = 1 - else: - default_mpiprocs = computer.get_default_mpiprocs_per_machine() - self.resources_config.num_nodes.disabled = False - self.resources_config.num_cpus.max = default_mpiprocs - self.resources_config.num_cpus.value = default_mpiprocs - self.resources_config.num_cpus.description = "CPUs/node" - # self.parallelization.npools.value = self._get_default_parallelization() - - # self._check_resources() - - @traitlets.observe("_submission_blockers") - def _observe_submission_blockers(self, change): - if change["new"]: - fmt_list = "\n".join((f"
  • {item}
  • " for item in sorted(change["new"]))) - self._submission_blocker_messages.value = f""" -
    - The submission is blocked, due to the following reason(s): -
      {fmt_list}
    """ - else: - self._submission_blocker_messages.value = "" - - def _toggle_install_widgets(self, change): - if change["new"]: - self.children = [ - child for child in self.children if child is not change["owner"] - ] - - def _auto_select_code(self, change): - if change["new"] and not change["old"]: - for code in [ - "pw_code", - "ph_code", - ]: - try: - code_widget = getattr(self, code) - code_widget.refresh() - code_widget.value = load_code(DEFAULT_PARAMETERS[code]) - except NotExistent: - pass - - def submit(self): - """Run the workflow to calculate delta factor""" - from aiida.engine import submit - - builder = VerificationWorkChain.get_builder() - - builder.pseudo = self.pseudo[1] - builder.pw_code = self.pw_code.value - builder.ph_code = self.ph_code.value - builder.label = orm.Str(self.pseudo_label) - - builder.accuracy = { - "protocol": orm.Str(self._PROTOCOL), - "cutoff_control": orm.Str(self.workchain_settings.calc_type.value), - } - - builder.convergence = { - "protocol": orm.Str(self._PROTOCOL), - "cutoff_control": orm.Str(self.workchain_settings.calc_type.value), - "criteria": orm.Str(self.workchain_settings.criteria.value), - } - - builder.properties_list = orm.List(list=self.workchain_settings.properties_list) - - builder.options = orm.Dict( - dict={ - "resources": { - "num_machines": self.resources_config.num_nodes.value, - "num_mpiprocs_per_machine": self.resources_config.num_cpus.value, - }, - } - ) - builder.parallelization = orm.Dict( - dict={"npool": self.parallelization.npools.value} - ) - builder.clean_workchain = orm.Bool(True) # anyway clean all - - # print("properties_list:", builder.properties_list.get_list()) - # print("protocol:", builder.accuracy.protocol.value) - # print("criteria:", builder.convergence.criteria.value) - # print("cutoff_control:", builder.accuracy.cutoff_control.value) - # print("options:", builder.options.get_dict()) - # print("parallelization:", builder.parallelization.get_dict()) - # print("clean_workdir_level:", builder.clean_workchain.value) - # print("label:", builder.label.value) - - process = submit(builder) - process.description = self.pseudo_label - - self.value = process.uuid - - def _on_submit_button_clicked(self, _): - self.submit_button.disabled = True - self.submit() - - self.state = self.State.SUCCESS - - @traitlets.observe("pseudo") - def _observe_pseudo(self, change): - self._update_state() - - def _update_state(self, _=None): - # Process is already running. - if self.value is not None: - self.state = self.State.SUCCESS - - # Input structure not specified. - if not self.pseudo: - self._submission_blockers = ["No pseudo selected."] - # This blocker is handled differently than the other blockers, - # because it is displayed as INIT state. - self.state = self.State.INIT - else: - blockers = list(self._identify_submission_blockers()) - if any(blockers): - self._submission_blockers = blockers - self.state = self.State.READY - else: - self._submission_blockers = [] - self.state = self.state.CONFIGURED - - def _identify_submission_blockers(self): - # No input pseudo specified. - if self.pseudo is None: - yield "No pseudo selected." - - # No code selected (this is ignored while the setup process is running). - if self.pw_code.value is None: - yield ( - 'No pw.x code selected. Go to "Codes & ' - 'Resources" to select a pw code.' - ) - - if self.ph_code.value is None: - yield ( - 'No ph.x code selected. Go to "Codes & ' - 'Resources" to select a ph code.' - ) - - -class NodeViewWidget(ipw.VBox): - - node = traitlets.Instance(Node, allow_none=True) - - def __init__(self, **kwargs): - self._output = ipw.Output() - super().__init__(children=[self._output], **kwargs) - - @traitlets.observe("node") - def _observe_node(self, change): - if change["new"] != change["old"]: - with self._output: - clear_output(wait=True) - if change["new"]: - display(viewer(change["new"])) - - -class ViewSsspAppWorkChainStatusAndResultsStep(ipw.VBox, WizardAppWidgetStep): - - value = traitlets.Unicode(allow_none=True) - - def __init__(self, **kwargs): - self.process_tree = ProcessNodesTreeWidget() - self.verification_status = ShowVerificationStatus() - ipw.dlink((self, "value"), (self.process_tree, "value")) - ipw.dlink((self, "value"), (self.verification_status, "value")) - - self.node_view = NodeViewWidget(layout={"width": "auto", "height": "auto"}) - ipw.dlink( - (self.process_tree, "selected_nodes"), - (self.node_view, "node"), - transform=lambda nodes: nodes[0] if nodes else None, - ) - self.process_status = ipw.VBox(children=[self.process_tree, self.node_view]) - - # Setup process monitor - self.process_monitor = ProcessMonitor( - timeout=0.2, - callbacks=[ - self.process_tree.update, - self._update_state, - ], - ) - ipw.dlink((self, "value"), (self.process_monitor, "value")) - - super().__init__( - [ - self.process_status, - # self.verification_status, - ], - **kwargs, - ) - - def can_reset(self): - "Do not allow reset while process is running." - return self.state is not self.State.ACTIVE - - def reset(self): - self.value = None - - def _update_state(self): - if self.value is None: - self.state = self.State.INIT - else: - process = load_node(self.value) - process_state = process.process_state - if process_state in ( - ProcessState.CREATED, - ProcessState.RUNNING, - ProcessState.WAITING, - ): - self.state = self.State.ACTIVE - elif process_state in (ProcessState.EXCEPTED, ProcessState.KILLED): - self.state = self.State.FAIL - elif process_state is ProcessState.FINISHED: - self.state = self.State.SUCCESS - - @traitlets.observe("value") - def _observe_process(self, _): - self._update_state() - - -def parse_state_to_info(process_state, exit_status=None) -> str: - if process_state == "finished": - if exit_status == 0: - return "FINISH OKAY " - else: - return f"FINISH FAILED[{exit_status}] " - - if process_state == "waiting": - return "RUNNING " - - return "NOT RUNNING " - - -class ShowVerificationStatus(ipw.VBox): - - value = traitlets.Unicode(allow_none=True) - - def __init__(self, **kwargs): - init_info = parse_state_to_info(None) - - self.delta_measure_state = ipw.HTML(init_info) - self.pressure_state = ipw.HTML(init_info) - self.cohesive_energy_state = ipw.HTML(init_info) - self.phonon_frequencies_state = ipw.HTML(init_info) - self.bands_distance_state = ipw.HTML(init_info) - - status_delta_measure = ipw.HBox( - children=[ - ipw.HTML("Delta factor:"), - self.delta_measure_state, - ] - ) - status_conv_pressure = ipw.HBox( - children=[ - ipw.HTML("Convergence: Pressure status:"), - self.pressure_state, - ] - ) - status_conv_cohesive_energy = ipw.HBox( - children=[ - ipw.HTML("Convergence - Cohesive energy:"), - self.cohesive_energy_state, - ] - ) - status_conv_phonon = ipw.HBox( - children=[ - ipw.HTML("Convergence - Phonon frequencies:"), - self.phonon_frequencies_state, - ] - ) - status_conv_bands = ipw.HBox( - children=[ - ipw.HTML("Convergence - Bands distance:"), - self.bands_distance_state, - ] - ) - refresh_button = ipw.Button( - description="Refresh", - tooltip="Refresh the verification status", - ) - refresh_button.on_click(self._on_refresh_button_clicked) - - super().__init__( - children=[ - status_delta_measure, - status_conv_cohesive_energy, - status_conv_pressure, - status_conv_phonon, - status_conv_bands, - refresh_button, - ], - **kwargs, - ) - - def _get_verification_info(self, process): - """ - Go through the called workflow state and set the infos. - """ - res = {} - - for sub in process.called: - label = sub.attributes.get("process_label") - process_state = sub.attributes.get("process_state") - exit_status = sub.attributes.get("exit_status", None) - - info = parse_state_to_info(process_state, exit_status) - - if label == "DeltaFactorWorkChain": - res["delta_measure"] = info - - if label == "ConvergencePressureWorkChain": - res["convergence:pressure"] = info - - if label == "ConvergenceCohesiveEnergyWorkChain": - res["convergence:cohesive_energy"] = info - - if label == "ConvergencePhononFrequenciesWorkChain": - res["convergence:bands_distance"] = info - - return res - - def _update_state(self): - if self.value is not None: - process = load_node(self.value) - infos = self._get_verification_info(process) - not_running_text = parse_state_to_info(None) - - self.delta_measure_state.value = infos.get( - "delta_measure", not_running_text - ) - self.pressure_state.value = infos.get( - "convergence:pressure", not_running_text - ) - self.cohesive_energy_state.value = infos.get( - "convergence:cohesive_energy", not_running_text - ) - self.phonon_frequencies_state.value = infos.get( - "convergence:phonon_frequencies", not_running_text - ) - self.bands_distance_state.value = infos.get( - "convergence:bands_distance", not_running_text - ) - - def _on_refresh_button_clicked(self, _): - self._update_state() - - @traitlets.observe("value") - def _observe_process(self, _): - self._update_state() diff --git a/aiidalab_sssp/version.py b/aiidalab_sssp/version.py deleted file mode 100644 index 4d46211..0000000 --- a/aiidalab_sssp/version.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -"""This module contains project version information.""" - -__version__ = "v23.03.0" diff --git a/inspect.ipynb b/inspect.ipynb deleted file mode 100644 index f5729f1..0000000 --- a/inspect.ipynb +++ /dev/null @@ -1,176 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "fe6d78c0", - "metadata": {}, - "outputs": [], - "source": [ - "%%javascript\n", - "IPython.OutputArea.prototype._should_scroll = function(lines) {\n", - " return false;\n", - "}\n", - "document.title='AiiDAlab SSSP app'" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fcd02c89", - "metadata": {}, - "outputs": [], - "source": [ - "%matplotlib widget" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ba9715ad", - "metadata": {}, - "outputs": [], - "source": [ - "import ipywidgets as ipw\n", - "from pathlib import Path\n", - "from IPython.display import display\n", - "import matplotlib.pyplot as plt\n", - "import os\n", - "import json\n", - "\n", - "from aiidalab_sssp.inspect.subwidgets.periodic_table import PeriodicTable\n", - "from aiidalab_sssp.inspect.subwidgets.select import PseudoSelectWidget\n", - "from aiidalab_sssp.inspect.subwidgets.summary import SummaryWidget\n", - "from aiidalab_sssp.inspect.subwidgets.delta import AccuracyMeritWidget, EosComparisonWidget\n", - "from aiidalab_sssp.inspect.subwidgets.bands import BandStructureWidget, BandChessboard\n", - "from aiidalab_sssp.inspect.subwidgets.convergence import ConvergenceWidget\n", - "\n", - "\n", - "from aiidalab_sssp.inspect import DB_FOLDER, SSSP_LOCAL_DB\n", - "\n", - "# Show plot one time dynamical\n", - "plt.ioff()\n", - "\n", - "# If cached DB folder not exist (first time app run), create folder\n", - "DB_FOLDER.mkdir(parents=True, exist_ok=True)\n", - "SSSP_LOCAL_DB.mkdir(parents=True, exist_ok=True)\n", - "\n", - "ptable = PeriodicTable(cache_folder=str(DB_FOLDER))\n", - "pseudo_select = PseudoSelectWidget()\n", - "summary = SummaryWidget()\n", - "\n", - "eos_comparison = EosComparisonWidget()\n", - "\n", - "nu_preview = AccuracyMeritWidget()\n", - "bandchessboard = BandChessboard()\n", - "convergence = ConvergenceWidget()\n", - "bandstucture = BandStructureWidget()\n", - "\n", - "ipw.dlink(\n", - " (ptable, 'pseudos'),\n", - " (pseudo_select, 'pseudos'),\n", - ")\n", - "\n", - "####################\n", - "# load process from url \n", - "# this need to happened before link to pseudos of following widgets\n", - "from urllib.parse import parse_qs, urlsplit\n", - "\n", - "def parse_url(url):\n", - " query = parse_qs(urlsplit(url).query)\n", - " pk = query.get('pk', None)\n", - "\n", - " # Can be a list with multiple nodes to load\n", - " return pk\n", - "\n", - "def _load_pseudos(element, db=SSSP_LOCAL_DB) -> dict:\n", - " \"\"\"Open result json file of element return as dict\"\"\"\n", - " if element:\n", - " json_fn = os.path.join(db, f\"{element}.json\")\n", - " with open(json_fn, \"r\") as fh:\n", - " pseudos = json.load(fh)\n", - "\n", - " return {key: pseudos[key] for key in sorted(pseudos.keys(), key=str.lower)}\n", - "\n", - " return dict()\n", - "\n", - "try: \n", - " pk = int(parse_url(jupyter_notebook_url)[0])\n", - "except Exception:\n", - " pk = None\n", - "\n", - "if pk:\n", - " import aiida\n", - " from aiida.orm import load_node\n", - " from aiidalab_sssp.inspect import dump_to_sssp_local_db\n", - "\n", - " aiida.load_profile()\n", - "\n", - " node = load_node(pk)\n", - " label = node.extras.get(\"label\").split()[-1]\n", - " element = node.extras.get(\"element\")\n", - "\n", - " try:\n", - " pseudos = _load_pseudos(element)\n", - " except FileNotFoundError:\n", - " pseudos = {}\n", - "\n", - " if label not in pseudos:\n", - " curated_results = pseudos\n", - " # dump bands/band structure and return a dict of pseudo\n", - " tmp_pseudo = dump_to_sssp_local_db(node)\n", - " pseudos[label] = tmp_pseudo\n", - " \n", - " ptable.selected_element = element\n", - " ptable.ptable.selected_elements = {element: 0} # TODO: this should be in ptabel widget\n", - " pseudo_select.selected_pseudos[f'{label}(custom)'] = pseudos[label]\n", - "\n", - " # TODO: add a custom instruction as output and chime into after the ptable\n", - " # to let user know that the local verified pseudo is to compare here.\n", - "########################\n", - "\n", - "\n", - "ipw.dlink((pseudo_select, 'selected_pseudos'), (summary, 'pseudos'))\n", - "ipw.dlink((pseudo_select, 'selected_pseudos'), (nu_preview, 'pseudos'))\n", - "ipw.dlink((pseudo_select, 'selected_pseudos'), (eos_comparison, 'pseudos'))\n", - "ipw.dlink((pseudo_select, 'selected_pseudos'), (bandchessboard, 'pseudos'))\n", - "ipw.dlink((pseudo_select, 'selected_pseudos'), (bandstucture, 'pseudos'))\n", - "ipw.dlink((pseudo_select, 'selected_pseudos'), (convergence, 'pseudos'))\n", - "\n", - "\n", - "display(ptable)\n", - "display(pseudo_select)\n", - "display(nu_preview)\n", - "display(summary)\n", - "display(eos_comparison)\n", - "display(bandchessboard)\n", - "display(bandstucture)\n", - "display(convergence)" - ] - } - ], - "metadata": { - "interpreter": { - "hash": "d4d1e4263499bec80672ea0156c357c1ee493ec2b1c70f0acce89fc37c4a6abe" - }, - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.13" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/miscellaneous/logo-sssp.png b/miscellaneous/logo-sssp.png deleted file mode 100644 index 8313201f46cabc75409a9da365e8b49062c08c73..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24514 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 U&L)QKy?swpRad22$>z=f0GS!kA^-pY diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..42472c3 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,3 @@ +[mypy] +check_untyped_defs = True +ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml index c3cb8d5..0cbad3e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,9 @@ classifiers = [ ] dependencies = [ "aiida-sssp-workflow", - "solara" + "solara", + "vaex", + "anywidget", ] [project.optional-dependencies] diff --git a/src/aiidalab_sssp/__init__.py b/src/aiidalab_sssp/__init__.py new file mode 100644 index 0000000..960f124 --- /dev/null +++ b/src/aiidalab_sssp/__init__.py @@ -0,0 +1,4 @@ +"""SSSP application to inspect and verify the pseudopotential""" + +__title__ = "SSSP application" +__version__ = "0.0.1" diff --git a/src/aiidalab_sssp/components/__init__.py b/src/aiidalab_sssp/components/__init__.py new file mode 100644 index 0000000..6ddd1cf --- /dev/null +++ b/src/aiidalab_sssp/components/__init__.py @@ -0,0 +1,2 @@ +from .header import Header # noqa +from .layout import Layout # noqa diff --git a/src/aiidalab_sssp/components/article.py b/src/aiidalab_sssp/components/article.py new file mode 100644 index 0000000..2f163cf --- /dev/null +++ b/src/aiidalab_sssp/components/article.py @@ -0,0 +1,21 @@ +import solara + +from aiidalab_sssp.data import html_cite, html_license + + +@solara.component +def Overview(): + with solara.ColumnsResponsive(12) as main: + with solara.VBox(): + solara.Details( + summary="How to cite", + children=[solara.HTML(unsafe_innerHTML=html_cite)], + expand=False, + ) + + solara.Details( + summary="License", + children=[solara.HTML(unsafe_innerHTML=html_license)], + expand=False, + ) + return main diff --git a/src/aiidalab_sssp/components/header.py b/src/aiidalab_sssp/components/header.py new file mode 100644 index 0000000..b243ff1 --- /dev/null +++ b/src/aiidalab_sssp/components/header.py @@ -0,0 +1,6 @@ +import solara + + +@solara.component +def Header(): + pass diff --git a/src/aiidalab_sssp/components/layout.py b/src/aiidalab_sssp/components/layout.py new file mode 100644 index 0000000..15c860d --- /dev/null +++ b/src/aiidalab_sssp/components/layout.py @@ -0,0 +1,6 @@ +import solara + + +@solara.component +def Layout(children=[]): + return solara.VBox(children=children) diff --git a/src/aiidalab_sssp/components/periodic_table/__init__.py b/src/aiidalab_sssp/components/periodic_table/__init__.py new file mode 100644 index 0000000..7e17001 --- /dev/null +++ b/src/aiidalab_sssp/components/periodic_table/__init__.py @@ -0,0 +1,140 @@ +import pathlib + +import anywidget +import traitlets +from traitlets import validate, observe, TraitError + +from .utils import color_as_rgb, CHEMICAL_ELEMENTS +from copy import deepcopy + + +class PeriodicTableWidget(anywidget.AnyWidget): + _esm = pathlib.Path(__file__).parent / "widget.js" + _css = pathlib.Path(__file__).parent / "widget.css" + + selected_elements = traitlets.Dict({}).tag(sync=True) + disabled_elements = traitlets.List([]).tag(sync=True) + display_names_replacements = traitlets.Dict({}).tag(sync=True) + disabled_color = traitlets.Unicode('gray').tag(sync=True) + unselected_color = traitlets.Unicode('pink').tag(sync=True) + states = traitlets.Int(1).tag(sync=True) + selected_colors = traitlets.List([]).tag(sync=True) + border_color = traitlets.Unicode('#cc7777').tag(sync=True) + disabled = traitlets.Bool(False, help="Enable or disable user changes.").tag(sync=True) + width = traitlets.Unicode('38px').tag(sync=True) + allElements = traitlets.List(CHEMICAL_ELEMENTS).tag(sync=True) + + # in + # element_color_mapping + # elements_disabled + + # out + # element_clicked + # hover??? If hover display is not possible, then input a element_description_mapping + + _STANDARD_COLORS = [ + "#a6cee3", + "#b2df8a", + "#fdbf6f", + "#6a3d9a", + "#b15928", + "#e31a1c", + "#1f78b4", + "#33a02c", + "#ff7f00", + "#cab2d6", + "#ffff99", + ] + + def __init__( + self, + states=1, + selected_elements=None, + disabled_elements=None, + disabled_color=None, + unselected_color=None, + selected_colors=[], + border_color=None, + width=None, + layout=None, + ): + super().__init__() + self.states = states if states else 1 + self.selected_elements = selected_elements if selected_elements else {} + self.disabled_elements = disabled_elements if disabled_elements else [] + self.disabled_color = disabled_color if disabled_color else 'gray' + self.unselected_color = unselected_color if unselected_color else 'pink' + self.selected_colors = ( + selected_colors if selected_colors else self._STANDARD_COLORS + ) + self.border_color = border_color if border_color else '#cc7777' + self.width = width if width else '38px' + + if layout is not None: + self.layout = layout + + if len(selected_colors) < states: + self.selected_colors = selected_colors + self._STANDARD_COLORS * ( + 1 + (states - len(selected_colors)) // len(self._STANDARD_COLORS) + ) + self.selected_colors = self.selected_colors[:states] + + def set_element_state(self, elementName, state): + if elementName not in self.allElements: + raise TraitError('Element not found') + if state not in range(self.states): + raise TraitError('State value is wrong') + x = deepcopy(self.selected_elements) + x[elementName] = state + self.selected_elements = x + + @validate('disabled_color', 'unselected_color', 'border_color') + def _color_change(self, proposal): + """Convert to rgb(X, Y, Z) type color""" + return color_as_rgb(proposal['value']) + + @validate('selected_colors') + def _selectedColors_change(self, proposal): + """Convert to rgb(X, Y, Z) type color""" + res = [] + for color in proposal['value']: + res.append(color_as_rgb(color)) + return res + + @validate('selected_elements') + def _selectedElements_change(self, proposal): + for x, y in proposal['value'].items(): + if x not in self.allElements and x != 'Du': + raise TraitError('Element not found') + if not isinstance(y, int) or y not in range(self.states): + raise TraitError('State value is wrong') + return proposal['value'] + + @observe('disabled_elements') + def _disabledList_change(self, change): + for i in change['new']: + if i in self.selected_elements: + del self.selected_elements[i] + + @observe('states') + def _states_change(self, change): + if change['new'] < 1: + raise TraitError('State value cannot smaller than 1') + else: + if len(self.selected_colors) < change["new"]: + self.selected_colors = self.selected_colors + self._STANDARD_COLORS * ( + 1 + + (change["new"] - len(self.selected_colors)) + // len(self._STANDARD_COLORS) + ) + self.selected_colors = self.selected_colors[: change["new"]] + elif len(self.selected_colors) > change["new"]: + self.selected_colors = self.selected_colors[: change["new"]] + + def get_elements_by_state(self, state): + if state not in range(self.states): + raise TraitError("State value is wrong") + else: + return [ + i for i in self.selected_elements if self.selected_elements[i] == state + ] diff --git a/src/aiidalab_sssp/components/periodic_table/utils.py b/src/aiidalab_sssp/components/periodic_table/utils.py new file mode 100644 index 0000000..ad48ca4 --- /dev/null +++ b/src/aiidalab_sssp/components/periodic_table/utils.py @@ -0,0 +1,166 @@ +import re + + +HTML_COLOR_MAP = { + "white": (255,) * 3, + "silver": tuple(round(0.75 * i) for i in (255,) * 3), + "gray": tuple(round(0.5 * i) for i in (255,) * 3), + "grey": tuple(round(0.5 * i) for i in (255,) * 3), + "black": (0,) * 3, + "red": (255, 0, 0), + "maroon": (round(0.5 * 255), 0, 0), + "yellow": (255, 255, 0), + "olive": tuple(round(0.5 * i) for i in (255, 255, 0)), + "lime": (0, 255, 0), + "green": (0, round(0.5 * 255), 0), + "aqua": (0, 255, 255), + "teal": tuple(round(0.5 * i) for i in (0, 255, 255)), + "blue": (0, 0, 255), + "navy": (0, 0, round(0.5 * 255)), + "fuchsia": (255, 0, 255), + "purple": tuple(round(0.5 * i) for i in (255, 0, 255)), + "pink": (255, 192, 203), +} + + +def color_as_rgb(color: str) -> str: + """Convert hex and named color to rgb formatting""" + if not color: + return "" + + if re.match(r"#[a-fA-F0-9]{6}", color): + # Hex color + color = color.lstrip("#") + color = tuple(int(color[i : i + 2], 16) for i in (0, 2, 4)) + elif re.match(r"rgb\([0-9]+,[0-9]+,[0-9]+\)", color): + # RGB color + return color + else: + # Color name + color = HTML_COLOR_MAP.get(color) + + if color is None: + return "" + return "".join(f"rgb{color!r}".split(" ")) + + +CHEMICAL_ELEMENTS = [ + "H", + "He", + "Li", + "Be", + "B", + "C", + "N", + "O", + "F", + "Ne", + "Na", + "Mg", + "Al", + "Si", + "P", + "S", + "Cl", + "Ar", + "K", + "Ca", + "Sc", + "Ti", + "V", + "Cr", + "Mn", + "Fe", + "Co", + "Ni", + "Cu", + "Zn", + "Ga", + "Ge", + "As", + "Se", + "Br", + "Kr", + "Rb", + "Sr", + "Y", + "Zr", + "Nb", + "Mo", + "Tc", + "Ru", + "Rh", + "Pd", + "Ag", + "Cd", + "In", + "Sn", + "Sb", + "Te", + "I", + "Xe", + "Cs", + "Ba", + "Hf", + "Ta", + "W", + "Re", + "Os", + "Ir", + "Pt", + "Au", + "Hg", + "Tl", + "Pb", + "Bi", + "Po", + "At", + "Rn", + "Fr", + "Ra", + "Rf", + "Db", + "Sg", + "Bh", + "Hs", + "Mt", + "Ds", + "Rg", + "Cn", + "Nh", + "Fi", + "Mc", + "Lv", + "Ts", + "Og", + "La", + "Ce", + "Pr", + "Nd", + "Pm", + "Sm", + "Eu", + "Gd", + "Tb", + "Dy", + "Ho", + "Er", + "Tm", + "Yb", + "Lu", + "Ac", + "Th", + "Pa", + "U", + "Np", + "Pu", + "Am", + "Cm", + "Bk", + "Cf", + "Es", + "Fm", + "Md", + "No", + "Lr", +] diff --git a/src/aiidalab_sssp/components/periodic_table/widget.css b/src/aiidalab_sssp/components/periodic_table/widget.css new file mode 100644 index 0000000..9ea7a37 --- /dev/null +++ b/src/aiidalab_sssp/components/periodic_table/widget.css @@ -0,0 +1,68 @@ +.periodic-table-entry { + border: 1px solid; + border-color: #cc7777; + border-radius: 3px; + width: 38px; + height: 38px; + display: table-cell; + text-align: center; + vertical-align: middle; + background-color: #ffaaaa; +} + +.periodic-table-disabled { + border-radius: 3px; + width: 38px; + height: 38px; + display: table-cell; + text-align: center; + vertical-align: middle; + background-color: #999999; +} + +.periodic-table-empty { + border: 0px; + width: 38px; + height: 38px; + display: table-cell; + text-align: center; + vertical-align: middle; +} + +.periodic-table-row { + display: table-row; +} + +.periodic-table-body { + display: table; + border-spacing: 4px; + margin: 10px; +} + +.periodic-table-entry:hover { + background-color: #cc7777; + transform: scale(1.4); +} + +.periodic-table-entry.elementOn { + background-color: #aaaaff; + border: 1px solid #7777cc; + border-radius: 4px; +} + + +.periodic-table-entry.elementOn:hover { + background-color: #7777cc; + border: 1px solid #7777cc; + border-radius: 4px; +} + +.noselect { + -webkit-touch-callout: none; /* iOS Safari */ + -webkit-user-select: none; /* Safari */ + -khtml-user-select: none; /* Konqueror HTML */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* Internet Explorer/Edge */ + user-select: none; /* Non-prefixed version, currently + supported by Chrome and Opera */ + } diff --git a/src/aiidalab_sssp/components/periodic_table/widget.js b/src/aiidalab_sssp/components/periodic_table/widget.js new file mode 100644 index 0000000..98d0b1e --- /dev/null +++ b/src/aiidalab_sssp/components/periodic_table/widget.js @@ -0,0 +1,256 @@ +import * as _ from 'https://cdn.jsdelivr.net/npm/underscore@1/+esm'; +import $ from 'https://cdn.jsdelivr.net/npm/jquery@3/+esm'; + +const elementTable = [ + ['H', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', 'He'], + ['Li', 'Be', '', '', '', '', '', '', '', '', '', '', 'B', 'C', 'N', 'O', 'F', 'Ne'], + ['Na', 'Mg', '', '', '', '', '', '', '', '', '', '', 'Al', 'Si', 'P', 'S', 'Cl', 'Ar'], + ['K', 'Ca', 'Sc', 'Ti', 'V', 'Cr', 'Mn', 'Fe', 'Co', 'Ni', 'Cu', 'Zn', 'Ga', 'Ge', 'As', 'Se', 'Br', 'Kr'], + ['Rb', 'Sr', 'Y', 'Zr', 'Nb', 'Mo', 'Tc', 'Ru', 'Rh', 'Pd', 'Ag', 'Cd', 'In', 'Sn', 'Sb', 'Te', 'I', 'Xe'], + ['Cs', 'Ba', '*', 'Hf', 'Ta', 'W', 'Re', 'Os', 'Ir', 'Pt', 'Au', 'Hg', 'Tl', 'Pb', 'Bi', 'Po', 'At', 'Rn'], + ['Fr', 'Ra', '#', 'Rf', 'Db', 'Sg', 'Bh', 'Hs', 'Mt', 'Ds', 'Rg', 'Cn', 'Nh', 'Fi', 'Mc', 'Lv', 'Ts', 'Og'], + ['', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ''], + ['', '', '*', 'La', 'Ce', 'Pr', 'Nd', 'Pm', 'Sm', 'Eu', 'Gd', 'Tb', 'Dy', 'Ho', 'Er', 'Tm', 'Yb', 'Lu'], + ['', '', '#', 'Ac', 'Th', 'Pa', 'U', 'Np', 'Pu', 'Am', 'Cm', 'Bk', 'Cf', 'Es', 'Fm', 'Md', 'No', 'Lr']]; + +const tableTemplate = _.template( + '<% for (let elementRow of elementTable) { ' + + 'print("
    "); ' + + 'for (let elementName of elementRow) { ' + + 'if ( (elementName === "") || (elementName == "*" ) || (elementName == "#" ) ) { %>' + + ' <%= elementName %>' + + '<% } else { %>' + + ' ' + + ' noselect element-<%= elementName %><% if (selectedElements.includes(elementName) && (! disabledElements.includes(elementName)) ) { print(" elementOn"); } %>" ' + + 'style="width: <%= elementWidth %>; height: <%= elementWidth %>; ' + + 'border-color: <% if (disabled) { colors = borderColor.replace(/[^\\d,]/g, "").split(","); ' + + 'red = Math.round(255 - 0.38 * ( 255 - parseInt(colors[0], 10) )); ' + + 'green = Math.round(255 - 0.38 * ( 255 - parseInt(colors[1], 10) )); ' + + 'blue = Math.round(255 - 0.38 * ( 255 - parseInt(colors[2], 10) )); ' + + 'print("rgb(" + red.toString(10) + "," + green.toString(10) + "," + blue.toString(10) + ")"); ' + + '} else { print(borderColor); } %>; ' + + 'background-color: <% if (disabledElements.includes(elementName)) { color = disabledColor; } ' + + 'else if (selectedElements.includes(elementName)) { ' + + 'i = selectedElements.indexOf(elementName); color = selectedColors[selectedStates[i]]; ' + + '} else { color = unselectedColor; } ' + + 'if (disabled) { colors = color.replace(/[^\\d,]/g, "").split(","); ' + + 'red = Math.round(255 - 0.38 * ( 255 - parseInt(colors[0], 10) )); ' + + 'green = Math.round(255 - 0.38 * ( 255 - parseInt(colors[1], 10) )); ' + + 'blue = Math.round(255 - 0.38 * ( 255 - parseInt(colors[2], 10) )); ' + + 'print("rgb(" + red.toString(10) + "," + green.toString(10) + "," + blue.toString(10) + ")"); ' + + '} else { print(color); } %>"' + + // 'title="state: <% if (selectedElements.includes(elementName)) { i = selectedElements.indexOf(elementName); print(selectedStates[i]);} '+ + // 'else if (disabledElements.includes(elementName)){print("disabled");} else {print("unselected");} %>" ><% '+ + '><% print(displayNamesReplacements[elementName] || elementName); %>' + + '<% } }; print("
    "); } %>', +); + +const elementList = []; +for (const elementRow of elementTable) { + for (const elementName of elementRow) { + if (elementName === '' || elementName === '*') { + continue; + } else { + elementList.push(elementName); + } + } +} + +function render({ model, el }) { + rerenderScratch({ el, model }); + + model.on('change:selected_elements', () => { + rerenderScratch({ el, model }); + }); + + model.on('change:disabled_elements', () => { + rerenderScratch({ el, model }); + }); + + model.on('change:display_names_replacements', () => { + rerenderScratch({ el, model }); + }); + + model.on('change:border_color', () => { + renderBorder(model.get('border_color')); + }); + + model.on('change:width', () => { + rerenderScratch({ el, model }); + }); + + model.on('change:disabled', () => { + rerenderScratch({ el, model }); + }); +} + +function rerenderScratch({ el, model }) { + // Re-render full widget when the list of selected elements + // changed from python + const selectedElements = model.get('selected_elements'); + const disabledElements = model.get('disabled_elements'); + const disabledColor = model.get('disabled_color'); + const unselectedColor = model.get('unselected_color'); + const selectedColors = model.get('selected_colors'); + const newSelectedColors = selectedColors.slice(); + const elementWidth = model.get('width'); + const borderColor = model.get('border_color'); + + let newSelectedElements = []; + const newSelectedStates = []; + + if ('Du' in selectedElements) { + return; + } + + for (const key in selectedElements) { + newSelectedElements.push(key); + newSelectedStates.push(selectedElements[key]); + } + + if (newSelectedElements.length !== newSelectedStates.length) { + return; + } + + // Here I want to clean up the two elements lists, to avoid + // to have unknown elements in the selectedElements, and + // to remove disabled Elements from the selectedElements list. + // I use s variable to check if anything changed, so I send + // back the data to python only if needed + + const selectedElementsLength = newSelectedElements.length; + // Remove disabled elements from the selectedElements list + newSelectedElements = _.difference(newSelectedElements, disabledElements); + // Remove unknown elements from the selectedElements list + newSelectedElements = _.intersection(newSelectedElements, elementList); + + const changed = newSelectedElements.length !== selectedElementsLength; + + // call the update (to python) only if I actually removed/changed + // something + if (changed) { + // Make a copy before setting + // while (newSelectedElements.length > newSelectedStates.length){ + // newSelectedStates.push(0); + // }; + + for (const key in selectedElements) { + if (!newSelectedElements.includes(key)) { + delete selectedElements[key]; + } + } + + model.set('selected_elements', selectedElements); + model.save_changes(); + } + + // Render the full widget using the template + el.innerHTML = + '
    ' + + tableTemplate({ + elementTable: elementTable, + displayNamesReplacements: model.get('display_names_replacements'), + selectedElements: newSelectedElements, + disabledElements: disabledElements, + disabledColor: disabledColor, + unselectedColor: unselectedColor, + selectedColors: newSelectedColors, + selectedStates: newSelectedStates, + elementWidth: elementWidth, + borderColor: borderColor, + disabled: model.get('disabled'), + }) + + '
    '; + + $(() => { + $('.periodic-table-entry').on('click', (event) => { + toggleElement({ el, model, event }); + }); + }); +}; + +function toggleElement({ el, model, event }) { + const classNames = _.map(event.target.classList, (a) => { + return a; + }); + const elementName = _.chain(classNames) + .filter((a) => { + return a.startsWith('element-'); + }) + .map((a) => { + return a.slice('element-'.length); + }) + .first() + .value(); + + const isOn = _.includes(classNames, 'elementOn'); + const isDisabled = _.includes(classNames, 'periodic-table-disabled'); + // If this button is disabled, do not do anything + // (Actually, this function should not be triggered if the button + // is disabled, this is just a safety measure) + + const states = model.get('states'); + const disabled = model.get('disabled'); + + if (disabled) { + return; + }; + + // Check if we understood which element we are + if (typeof elementName !== 'undefined') { + const currentList = model.get('selected_elements'); + // NOTE! it is essential to duplicate the list, + // otherwise the value will not be updated. + + let newList = []; + const newStatesList = []; + + for (const key in currentList) { + newList.push(key); + newStatesList.push(currentList[key]); + }; + + const num = newList.indexOf(elementName); + + if (isOn) { + // remove the element from the selected_elements + + if (newStatesList[num] < states - 1) { + newStatesList[num]++; + currentList[elementName] = newStatesList[num]; + } else { + newList = _.without(newList, elementName); + newStatesList.splice(num, 1); + delete currentList[elementName]; + // Swap CSS state + event.target.classList.remove('elementOn'); + } + } else if (!isDisabled) { + // add the element from the selected_elements + newList.push(elementName); + newStatesList.push(0); + currentList[elementName] = 0; + // Swap CSS state + event.target.classList.add('elementOn'); + } else { + return; + }; + + // Update the model (send back data to python) + // I have to make some changes, since there is some issue + // for Dict in Traitlets, which cannot trigger the update + model.set('selected_elements', { Du: 0 }); + model.set('selected_elements', currentList); + model.save_changes(); + }; +}; + +function renderBorder(color) { + const a = document.getElementsByClassName('periodic-table-entry'); + + for (let i = 0; i < a.length; i++) { + a[i].style.border = '1px solid ' + color; + } +}; + +export default { render }; diff --git a/src/aiidalab_sssp/components/viz.py b/src/aiidalab_sssp/components/viz.py new file mode 100644 index 0000000..3800118 --- /dev/null +++ b/src/aiidalab_sssp/components/viz.py @@ -0,0 +1,13 @@ +import solara +from .periodic_table import PeriodicTableWidget + + + +@solara.component +def PerioicTable(): + with solara.ColumnsResponsive(12) as main: + solara.display(PeriodicTableWidget()) + + return main + + diff --git a/src/aiidalab_sssp/content/articles/equis-in-vidi.md b/src/aiidalab_sssp/content/articles/equis-in-vidi.md new file mode 100644 index 0000000..0476d3f --- /dev/null +++ b/src/aiidalab_sssp/content/articles/equis-in-vidi.md @@ -0,0 +1,85 @@ +--- +author: maarten +title: Equis in vidi +description: Equis in vidi +image: https://images.unsplash.com/photo-1429041966141-44d228a42775?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=2500&q=80 +thumbnail: https://images.unsplash.com/photo-1429041966141-44d228a42775?ixid=MXwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHw%3D&ixlib=rb-1.2.1&auto=format&fit=crop&w=350&q=80 +alt: "Equis in vidi" +createdAt: 2021-03-9 +duration: 6 min read +category: + - general +--- +# Equis in vidi + +## Hoc adducere premunt + +Lorem markdownum propter limite aetas contenta servatrix *perpetuaque potest* +oculis paventem omnem; pater. Spatium tamen poterant habuit in excita et ignibus +relevat voluptas, Diana praesensque sonarent. AI eventu, ire ratus ostendens +totiens Attonitae tinus in, fratrem. Loqui qui divum plaudenda concrescere in +nova, amplectitur liquidas prendique rigidas terrae seque haut putat deprendit +Eurynome Sol gens? + + jsf *= vfatAnalogQuery; + if (1 != server_pipeline) { + executable_partition_chipset += bar(server_vle_website, ict); + android = module_uddi_mouse + bitImpactInput; + } else { + white_cc_smartphone(40673 + igp, filenameAccess, plugSyntaxZone(monitor, + 955493)); + sequencePcCopyright.management = up_lifo(wimax, pitch, pointEsports); + } + if (microcomputer) { + controlTraceroute = 1; + } else { + key.vlb_lamp_e -= xml_gigabit + real; + animated_burn += ping_olap_apache; + } + if (serp(encryption_internet.sdk.wais_floppy_node(3, adapter, + dns_reimage_pack), microphone_namespace * guid, pageMetal)) { + qbeNetworkProtocol.fiber_drive += domain(ieeeAccess); + crt *= exploit_meme + art; + } else { + moodle -= ccd_cgi_cursor; + } + +## Ara moras + +Et senectae [nostrum exigui et](http://misererefuisset.org/cumacuta) et ergo ut +vitae labare, sed sive ubi, tot dea? Tollere abit. Gravis maternaque tendens +quamquam Phaeocomes mihi tendens saltem; atque sunt gemina cetera, dixit +gratissima oculos. Urbem Euboea *et* putes in magnanimus quae Chersidamante +procul, ait viri fictumque relinquet arce, ire. + +## Tamen vero torquet tibi + +Quaque studio, et Iuppiter, sui, pro Erycis nec somnique protinus caelo, +comitata. *Et* fata lacrimas vis hanc, pede cursu Quirini unam! + + var httpsMeta = cyberspace_post + white_srgb + bot_compression / -5 + -2; + minisite_lifo_sound(isa(clean, 79) * 3); + if (fontNewsgroup) { + website.gigabit_frozen_spam(card_balancing, viral_market, + tftPlagiarismChecksum); + data.fullPython(icmp(matrix_cd), 3); + } + qwerty.opticalOpticalUnmount += drop_cisc + promptNewsgroup - winsock; + lion_mainframe_key = key; + +## Imbres abiit + +Rata ipsi ieiunia potentia tibi, qui morte brevi carina [processit geminato +Aeacide](http://pro.org/induitur-nisi), auribus. Mero eram Numici iactantem +velles vetat lustra busta iussit concubitusque timor altis solvit *bene*! +Caelumque concipiunt moveri unus. In magna habenas querenda in florentia hiems +vetat tam habebam ignes Latoius, maxima primoque. + +Oceano laboriferi dicentum Veneris donec, veniam pectine vota retusa. Vacca quis +non cuius collesque in ortas Olenos tenuere sit genitor ut quisque Laomedonve +teste, uterque Deucalion auro, qui. Mixtos est, et tibi mihi sum. + +Urbe huic soporem. Sine optima secuta, ante ignarus currus [parabant +robore](http://haemoniae-quique.io/portas-cecidere) corpore tremens qui erat +aura mediusque **virgo**, iactis quae vellera. Dixerat ferebat siccaeque +penetralia oculis, est umeroque hic vero. Modo tempora fuit. diff --git a/src/aiidalab_sssp/content/articles/substiterat-vati.md b/src/aiidalab_sssp/content/articles/substiterat-vati.md new file mode 100644 index 0000000..72944de --- /dev/null +++ b/src/aiidalab_sssp/content/articles/substiterat-vati.md @@ -0,0 +1,70 @@ +--- +author: jovan +title: Substiterat vati +description: Substiterat vati +thumbnail: https://miro.medium.com/max/350/1*oC9sUtSrHmvv23yYSg__LA.jpeg +image: https://miro.medium.com/max/1350/1*oC9sUtSrHmvv23yYSg__LA.jpeg +alt: "Substiterat vati" +createdAt: 2020-02-17 +duration: 6 min read +category: + - general +--- + +# Substiterat vati + +## Porrigis cecinit absentes + +Lorem markdownum infelix **caeli**, quaeque molitur; Thyesteis gerunt ab urbem. +Cum docta et creditur utrumque inmisit regem modo similes acceptaque forte. Ne +facesque, et egredior, aut libera iaculum, morem. Maius peperisse floribus +dapibus ad reparare lintea, [illa mente](http://in.com/nautas) superi avis; vix +reppulit! Alium faciebat, suo sed dignus et, fuit apte sacra ad! + +1. Gemitu gloria et sed iste ulla delubra +2. Petraeum in patria coniunx mare quod plenaque +3. Pecudis attonitos perdam monstris passim non plumbo +4. Gemino serpentis aditum cuius se novitate et +5. Nihil aeraque hostem deus vehit pharetra +6. Sic inde labori inaniter gelidae transferre radium + +Pronumque regna da congestaque iuvenalis formae! Umeris eodem sinumque viscera. +Ille nymphe; poma filia, quam miserum traho certis Atridae. Vi habet, addit +nomen venit. Quoque **convertor vestigia** iura, sum inquire sexangula equorum +invenio, plumis cristis exarsit et terga, praecordia. + +## Illi rursus + +Sibila petit amare visa Ulixem, est, ab tamen. Animalia **non prolem omnia** +adplicor in certa flerunt? + +Habes nova corpora, nobis solidumque nostri et solvit, ater illis Palladis vatum +Crocon cadunt mixta caelum subitusque tegmine! Ponderis onere, dignare detrusit +femineae annis: non quae Actaea magni ille, corpusque. Crimine negabamus Lydas +lacusque colus aquosis vocis retinacula figentem nubes pallet; quod intrat +nostra nos secreta nostro, Titan? Non Bromiumque possunt nunc per, et plebe +quamvis antra huc prodere stant. + +Orbem sollicitae **Ganymedis** carinis ulli Mavortis Iuppiter cavas, iam Ascanii +[vindicta](http://extemplo.io/gelido.html). Vident quid iste cum Styga primum +ignavi genitoris effugit falsum nova est. Novitate quos retro compos sarisa et +[sanguine](http://anno-carpit.org/), ferre manus praestet praevisos numeratur +Aeson et o nec. + +## Concepit lymphata in isque iamdudum ituras iuga + +Morisque cum: uni rauca cantus sed nomine, *reditum* inspiratque dedit +**abstulerit ungulaque**. Usus parantem oriuntur reminiscitur quot vulnera +hominis cuspis! + +Tulit medicamina Nycteliusque *socero*, latens dixerat **sic Actaeas** oculos +sub plenissima felix. Heliadum [Tyrrhenaque](http://oad.com/medio.html) terras +siqua infelix ultra, adsunt eurus infelix cum: si aethere **locum** permiscuit +sustinet Helopsque osculaque. + +Hunc ille murmure candore et mecum conchaeque dumque corpore in lati sortita; +atra nata! Fortes quisquam iudice cavata: genitas desilit conspecta ad ante +possidet tenet *enim dixit*? Memoratis nec ira triceps **primus**. Moliri +aequaret ope sedes ore aliena siquidem se timuit hostem, carina sequantur +*concita invisumque vestes* signataque amoris, mandabat. Ortu iam ora reicere +Isthmo: fremebant spoliavit. diff --git a/src/aiidalab_sssp/data.py b/src/aiidalab_sssp/data.py new file mode 100644 index 0000000..174f0f3 --- /dev/null +++ b/src/aiidalab_sssp/data.py @@ -0,0 +1,139 @@ +import dataclasses +from pathlib import Path +from typing import Any, Dict + +import vaex.datasets +import yaml + + +@dataclasses.dataclass +class DataFrame: + title: str + df: Any + image_url: str + + +dfs = { + "titanic": DataFrame( + df=vaex.datasets.titanic(), + title="Titanic", + image_url="https://images.unsplash.com/photo-1561625116-df74735458a5?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=3574&q=80", # noqa + ), + "iris": DataFrame( + df=vaex.datasets.iris(), + title="Iris", + image_url="https://images.unsplash.com/photo-1540163502599-a3284e17072d?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=3870&q=80", # noqa + ), + # uncomment for a larger dataset to be included + # "taxi": DataFrame( + # df=vaex.datasets.taxi(), + # title="New York Taxi", + # image_url="https://images.unsplash.com/photo-1514749204155-24e484635226?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1587&q=80", # noqa + # ), +} + +names = list(dfs) +# def load(name): +# if name == "titanic" + +HERE = Path(__file__) + + +@dataclasses.dataclass +class Article: + markdown: str + title: str + description: str + image_url: str + + +articles: Dict[str, Article] = {} + +for file in (HERE.parent / "content/articles").glob("*.md"): + content = file.read_text() + lines = [k.strip() for k in content.split("\n")] + frontmatter_start = lines.index("---", 0) + frontmatter_end = lines.index("---", frontmatter_start + 1) + yamltext = "\n".join(lines[frontmatter_start + 1 : frontmatter_end - 2]) + metadata = yaml.safe_load(yamltext) + markdown = "\n".join(lines[frontmatter_end + 1 :]) + articles[file.stem] = Article(markdown=markdown, title=metadata["title"], description=metadata["description"], image_url=metadata["image"]) + +# XXX:: move me to static html file +html_cite = """ +Please always cite the library you are using as: +
    + +

    + Please make an effort to acknowledge original authors and to ensure + reproducibility of your calculations by listing or citing all + pseudopotentials used, and being compliant with the corresponding licenses. + + Click here + for the acknowledgements list. + +

    +""" + +html_about = """ + +""" + +html_ack = """ + +""" + +html_old_version = """ + +""" + +html_license = """ +

    + The SSSP efficiency and precision pseudopotential libraries are a collection of pseudopotentials generated + by different authors with different methodologies, selected according to the SSSP protocol. + Each single pseudopotential is distributed with its original license (e.g. GPL or Creative Commons) + as chosen by its authors. +

    +

    + By downloading the SSSP efficiency or precision libraries, you accept the + terms and conditions of the original licenses. +

    +

    + Please make an effort to acknowledge original authors and to ensure reproducibility of your calculations + by listing or citing all pseudopotentials used, and being compliant with the corresponding licenses. + + Click here + for the acknowledgements list. +

    +""" diff --git a/src/aiidalab_sssp/pages/__init__.py b/src/aiidalab_sssp/pages/__init__.py new file mode 100644 index 0000000..dfdd6e9 --- /dev/null +++ b/src/aiidalab_sssp/pages/__init__.py @@ -0,0 +1,59 @@ +import solara +from solara.alias import rv + +from aiidalab_sssp.pages import explore, howto +from aiidalab_sssp.data import articles, names + + +@solara.component +def PeopleCard(name): + with solara.Card(f"Employee of the Month: {name}") as main: + with rv.CardText(): + solara.Markdown( + """ + * Department: foo + * Skills: bar, baz + """ + ) + with solara.Link(f"/people/{name}"): + solara.Button("View employee", text=True, icon_name="mdi-profile") + return main + + +@solara.component +def Layout(children=[]): + router = solara.use_context(solara.routing.router_context) + with solara.VBox() as navigation: + with rv.List(dense=True): + with rv.ListItemGroup(v_model=router.path): + with solara.Link(solara.resolve_path("/")): + with solara.ListItem("Home", icon_name="mdi-home", value="/"): + pass + with solara.ListItem("tabular data", icon_name="mdi-database"): + for name in names: + pathname = f"/tabular/{name}" + with solara.Link(solara.resolve_path(pathname)): + solara.ListItem(name, value=pathname) + with solara.ListItem("Articles", icon_name="mdi-book-open"): + for name, article_ in articles.items(): + pathname = f"/article/{name}" + with solara.Link(solara.resolve_path(pathname)): + solara.ListItem(article_.title, value=pathname) + + with solara.AppLayout(navigation=navigation, title="Solara demo", children=children) as main: + pass + return main + + +@solara.component +def Page(): + with solara.VBox() as main: + solara.Title("Standard Solid-State Pseudopotential (SSSP) » Home") + with solara.ColumnsResponsive(12): + howto.Overview() + with solara.ColumnsResponsive([6, 6]): + explore.Overview() + verification.Overview() + + return main + diff --git a/src/aiidalab_sssp/pages/explore/__init__.py b/src/aiidalab_sssp/pages/explore/__init__.py new file mode 100644 index 0000000..34a9601 --- /dev/null +++ b/src/aiidalab_sssp/pages/explore/__init__.py @@ -0,0 +1,84 @@ +"""This Page components sets in the package root and takes two non-optional arguments, +meaning it will catch urls like /viz/scatter/titanic and pass two argument to the Page component, +so we can render content dynamically. +""" + +from typing import Optional + +import plotly.express as px +import solara + +from ... import data +from ...components import viz + +from aiidalab_sssp.components.viz import PerioicTable + +def title(type: str, name: str): + return f"Solara viz view: {type} - {name}" + + +@solara.component +def Page(type: Optional[str] = None, name: Optional[str] = None, x: Optional[str] = None, y: Optional[str] = None): + type, set_type = solara.use_state_or_update(type) + name, set_name = solara.use_state_or_update(name) + x, set_x = solara.use_state_or_update(x) + y, set_y = solara.use_state_or_update(y) + + # XXX: depend on name show/noshow the detail of a specific element + + with solara.ColumnsResponsive(12) as main: + with solara.ColumnsResponsive(12): + viz.PerioicTable() + + if type is None: + type = "scatter" + set_type("scatter") + with solara.Sidebar(): + with solara.Card("Viz configuration"): + solara.Select(label="dataset", value=name, values=list(data.dfs), on_value=set_name) + solara.ToggleButtonsSingle(value=type, values=["scatter", "histogram"], on_value=set_type) + if name not in data.dfs: + set_name(list(data.dfs)[0]) + if name in data.dfs: + df = data.dfs[name].df + column_names = df.get_column_names() + df = df.to_pandas_df() + if x not in column_names: + set_x(column_names[0]) + if y not in column_names: + set_y(column_names[1]) + if x not in column_names: + set_x(column_names[0]) + if y not in column_names: + set_y(column_names[1]) + solara.Title(f"Solara demo » viz » {type} » {name}") + fig = None + if type == "scatter": + with solara.Sidebar(): + with solara.Card("Columns"): + solara.Select(label="x", value=x, values=column_names, on_value=set_x) + solara.Select(label="y", value=y, values=column_names, on_value=set_y) + if x and y and x in column_names and y in column_names: + fig = px.scatter(df, x=x, y=y) + else: + solara.Warning("Please provide x and y") + elif type == "histogram": + with solara.Sidebar(): + with solara.Card("Columns"): + solara.Select(label="x", value=x, values=column_names, on_value=set_x) + if x and x in column_names: + fig = px.histogram(df, x=x) + else: + solara.Warning("Please provide x") + else: + solara.Error("Uknonwn ") + if fig: + solara.FigurePlotly(fig, dependencies=[name, type, x, y]) + return main + +@solara.component +def Overview(): + with solara.ColumnsResponsive(12) as main: + with solara.Card("Explore"): + PerioicTable() + return main diff --git a/src/aiidalab_sssp/pages/howto/__init__.py b/src/aiidalab_sssp/pages/howto/__init__.py new file mode 100644 index 0000000..6dfd264 --- /dev/null +++ b/src/aiidalab_sssp/pages/howto/__init__.py @@ -0,0 +1,27 @@ +from typing import Optional + +import solara + +from ... import data +from ...components.article import Overview + + +# XXX: this page I may don't need. +@solara.component +def Page(name: Optional[str] = None, page: int = 0, page_size=100): + if name is None: + with solara.Column() as main: + solara.Title("Home » Howto") + Overview() + return main + if name not in data.articles: + return solara.Error(f"No such article: {name!r}") + article = data.articles[name] + with solara.ColumnsResponsive(12) as main: + solara.Title("Home » Article » " + article.title) + with solara.Link("/article"): + solara.Text("« Back to overview") + with solara.Card(): + pre = f"# {article.title}\n\n" + solara.Markdown(pre + article.markdown) + return main diff --git a/src/aiidalab_sssp/pages/verification/__init__.py b/src/aiidalab_sssp/pages/verification/__init__.py new file mode 100644 index 0000000..1dc3476 --- /dev/null +++ b/src/aiidalab_sssp/pages/verification/__init__.py @@ -0,0 +1,79 @@ +"""This Page takes an extra argument, meaning that it can cache urls like /tabular/titanic +and pass the last part of the url as argument to the Page component, so we can render content +dynamically. +""" +from typing import Optional + +import solara +import textwrap +from solara.components.file_drop import FileInfo + + + +@solara.component +def Page(name: Optional[str] = None, page: int = 0, page_size=100): + with solara.ColumnsResponsive(12) as main: + solara.HTML(unsafe_innerHTML="""

    More detailed verification instructions

    +

    +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +""") + with solara.ColumnsResponsive([6, 6]): + with solara.Card(title="UPF file upload"): + UpfFileDrop() + with solara.Card(title="Verification setup"): + DetailSetup() + + return main + +@solara.component +def DetailSetup(): + with solara.Column() as main: + # buttons for tuning protocol, dry-run (inspect the PP) or real-run of verification + # ComputationalResourcesWidget from AWB (can I??) + solara.HTML(unsafe_innerHTML="""PLACEHOLDER""") + UpfFileDrop() + return main + + +@solara.component +def UpfFileDrop(): + content, set_content = solara.use_state(b"") + filename, set_filename = solara.use_state("") + # size, set_size = solara.use_state(0) + + # TODO: should read as a stream. Should stop and raise when the file is too large. + + def on_file(f: FileInfo): + set_filename(f["name"]) + set_size(f["size"]) + set_content(f["file_obj"].read()) + + solara.FileDrop( + label="Drag and drop a UPF file here to run verification.", + on_file=on_file, + lazy=True, # XXX: check the detail, UPF file can be big ~1MB so lazy is the better choice? + ) + + # TODO: check it is a valide UPF file, by checking extension is not enough, should be a txt not a binary + # Should be able to be parsed. + # Raise on error if it is not valid + + # TODO: warning if it is not a PBE or relativistic PP. + + if content: + solara.Info(f"UPF {filename} is uploaded.") + solara.Preformatted("\n".join(textwrap.wrap(repr(content)))) + +html_content = """ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +""" + +@solara.component +def Overview(): + with solara.Card(title="Verification") as main: + with solara.Column(): + solara.HTML(unsafe_innerHTML=html_content) + with solara.Card(title="UPF file upload"): + UpfFileDrop() + + return main diff --git a/sssp-docs.ipynb b/sssp-docs.ipynb deleted file mode 100644 index 85ba71e..0000000 --- a/sssp-docs.ipynb +++ /dev/null @@ -1,176 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "dd3be55d", - "metadata": {}, - "source": [ - "## $\\Delta$ measures (normol $\\Delta$-factor and $\\nu\\Delta$ the 'length of the vector' measure)\n", - "\n", - "### $\\Delta$-factor\n", - "The $\\Delta$-factor is defined from the result of Birch-Murnanghan fitting which is:\n", - "\n", - "$$\n", - " E_\\text{BM}(V) = E_0 + \\frac{9 V_0 B_0}{16} {[(V_0 / V)^{2/3}-1]^3 B_0^{'} + [(V_0 / V)^{2/3}-1]^2[6-4(V_0 / V)^{2/3}]}\n", - "$$\n", - "\n", - "Where the energies are calculated from changing lattice parameters at different volumes diviate from the balance position. The the $\\Delta$-factor is integrated difference of two Birch-Murnanghan fitting result:\n", - "\n", - "$$\n", - " \\Delta = \\sqrt{\\frac{\\int \\mathrm{d} V \\Delta E_{\\text{BM}}^2(V)}{\\Delta{V}}} \n", - "$$\n", - "\n", - "### $\\Delta'$-factor\n", - "However $\\Delta$ is stiffness-dependent quantity, being proportional to $B_0$. The $\\Delta'$ is introduced to renormalized $\\Delta$-factor which is similar to the original $\\Delta$-factor except it is \"renormalized\"\n", - "to reference values of $V^{\\text{ref}}$ and $B^{\\text{ref}}$ for all the elements\n", - "\n", - "$$\n", - "\\Delta' = \\frac{V_{\\text{ref}}B_{\\text{ref}}}{V_{\\text{AE}}B_{\\text{AE}}} \\Delta\n", - "$$\n", - "\n", - "- $V^{\\text{ref}}=30 \\text{Bohr}$\n", - "- $B^{\\text{ref}}=100\\text{GPa}$ \n", - "\n", - "are correspond approximatively to the mean values of $V_0$ and $B_0$ over the 71 unitary elements tested.\n", - "\n", - "### length of the vector measure\n", - "We also defined the measure 'length of the vector' formed by the relative error of $V_0$, $B_0$, $B_1$\n", - "\n", - "In this page I choose prefactor ($B_0=\\frac{1}{8}$) and ($B_1=\\frac{1}{64}$).\n", - "\n", - "In order to compare the result with $\\Delta$ and $\\Delta'$, I multiply the result with prefactor $\\times 400$ so they are in the same order." - ] - }, - { - "cell_type": "markdown", - "id": "800d43b8", - "metadata": {}, - "source": [ - "## Cohesive energy (eV/atom)\n", - "\n", - "The cohesive energy $E_{\\text{coh}}$ is the energy gained by arranging the atoms in a crystalline state.\n", - "\n", - "$$\n", - " E_{\\text{coh}} = E_{\\text{bulk}} - E_{\\text{atom}}\n", - "$$\n", - "\n", - "The different of cohesive energy with respect to reference is defined as:\n", - "\n", - "$$\n", - " \\delta E_{\\text{coh}} = E_{\\text{coh}}(x_c) - E_{\\text{coh}}(x_{ref})\n", - "$$\n", - "\n", - "- $x_c$: the cutoff setting of sample point\n", - "- $x_{ref}$: the cutoff setting of the reference point (the converged value), " - ] - }, - { - "cell_type": "markdown", - "id": "475abb76", - "metadata": {}, - "source": [ - "## Cohesive energy error (eV/atom)\n", - "\n", - "The error of cohesive energy w.r.t converged reference cutoff (200 Ry for wavefunction cutoff) is defined as:\n", - "\n", - "$$\n", - " \\delta E_{\\text{coh}} = E_{\\text{coh}}(x_c) - E_{\\text{coh}}(x_{ref})\n", - "$$\n", - "\n", - "- $x_c$: the cutoff setting of sample point\n", - "- $x_{ref}$: the cutoff setting of the reference point (the converged value), " - ] - }, - { - "cell_type": "markdown", - "id": "a81a9a26", - "metadata": {}, - "source": [ - "## Phonon frequencies absolute error ($cm^{-1}$)\n", - "\n", - "The absolute error of phonon frequencies w.r.t reference cutoff (200 Ry for wavefunction cutoff) is defined as:\n", - "\n", - "$$\n", - " \\delta \\bar{\\omega} = \n", - " \\sqrt{\\frac{1}{N} \\sum^N_{i=1} \\omega_i(x_c)-\\omega_i(x_{ref})}\n", - "$$\n", - "\n", - "- $\\omega_i$: the frequencies of every modes. \n", - "- N: the number of modes" - ] - }, - { - "cell_type": "markdown", - "id": "44922944", - "metadata": {}, - "source": [ - "## Phonon frequencies relative error (%)\n", - "\n", - "The relative error of phonon frequencies w.r.t reference cutoff (200 Ry for wavefunction cutoff) is defined as:\n", - "\n", - "$$\n", - " \\delta \\bar{\\omega} = \n", - " \\sqrt{\\frac{1}{N} \\sum^N_{i=1} \\left | \\frac{\\omega_i(x_c)-\\omega_i(x_{ref})}{\\omega_i(x_{ref})} \n", - " \\right|^2}\n", - "$$\n", - "\n", - "- $\\omega_i$: the frequencies of every modes. \n", - "- N: the number of modes" - ] - }, - { - "cell_type": "markdown", - "id": "62b8337f", - "metadata": {}, - "source": [ - "## Pressure absolute error (GPa)\n", - "\n", - "The absolute error of pressure are complex defined w.r.t converged reference cutoff (200 Ry for wavefunction cutoff) is defined as:\n", - "\n", - "$$\n", - " \\delta P = P(x_c) - P(x_{ref})\n", - "$$" - ] - }, - { - "cell_type": "markdown", - "id": "1a874e26", - "metadata": {}, - "source": [ - "## Pressure relative error (GPa)\n", - "\n", - "The relative error of pressure are complex defined in terms of volume difference where the volume is converted from the pressure difference of the cutoff setting and converged reference setting:\n", - "\n", - "$$\n", - " \\delta V = \\frac{V'-V_0}{V_0}\n", - "$$\n", - "\n", - "- $V'$ is the deviation volume as the one closest to the equilibrium volume \n", - "- $V_0$ is read from the converged reference (wave function cutoff 200Ry) Birch-Murnaghan fitting. \n", - "\n", - "$$P_\\text{BM}(V')=\\delta P$$ " - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.7.9" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/start.py b/start.py deleted file mode 100644 index 83ecca3..0000000 --- a/start.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- - -import ipywidgets as ipw - -template = """ - - - - - -
    -""" - - -def get_start_widget(appbase, jupbase, notebase): - html = template.format(appbase=appbase, jupbase=jupbase, notebase=notebase) - return ipw.HTML(html) - - -# EOF diff --git a/verification.ipynb b/verification.ipynb deleted file mode 100644 index c1fd012..0000000 --- a/verification.ipynb +++ /dev/null @@ -1,151 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%javascript\n", - "IPython.OutputArea.prototype._should_scroll = function(lines) {\n", - " return false;\n", - "}\n", - "document.title='AiiDAlab SSSP app'" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import ipywidgets as ipw\n", - "from jinja2 import Environment\n", - "from importlib_resources import files\n", - "from IPython.display import display\n", - "\n", - "from aiida.orm import load_node\n", - "from aiidalab_widgets_base import WizardAppWidget, WizardAppWidgetStep\n", - "from aiidalab_widgets_base.bug_report import install_create_github_issue_exception_handler\n", - "\n", - "from aiidalab_sssp import static\n", - "from aiidalab_sssp.process import WorkChainSelector\n", - "from aiidalab_sssp.steps import SubmitSsspWorkChainStep\n", - "from aiidalab_sssp.steps import ConfigureSsspWorkChainStep\n", - "from aiidalab_sssp.steps import ViewSsspAppWorkChainStatusAndResultsStep\n", - "from aiidalab_sssp.steps import PseudoSelectionStep\n", - "from aiidalab_sssp.steps import SettingPseudoMetadataStep\n", - "from aiidalab_sssp.steps import ViewSsspAppWorkChainStatusAndResultsStep\n", - "from aiidalab_sssp.version import __version__\n", - "\n", - "pseudo_selection_step = PseudoSelectionStep(auto_advance=True)\n", - "configure_sssp_app_work_chain_step = ConfigureSsspWorkChainStep(auto_advance=True)\n", - "setting_pseudo_metadata_step = SettingPseudoMetadataStep(auto_advance=True)\n", - "submit_sssp_work_chain_step = SubmitSsspWorkChainStep(auto_advance=True)\n", - "view_sssp_app_work_chain_status_and_results_step = ViewSsspAppWorkChainStatusAndResultsStep()\n", - "\n", - "# Link the application steps\n", - "ipw.dlink((pseudo_selection_step, 'state'), (configure_sssp_app_work_chain_step, 'previous_step_state'))\n", - "ipw.dlink((configure_sssp_app_work_chain_step, 'state'), (setting_pseudo_metadata_step, 'previous_step_state'))\n", - "ipw.dlink((setting_pseudo_metadata_step, 'state'), (submit_sssp_work_chain_step, 'previous_step_state'))\n", - "\n", - "ipw.dlink((pseudo_selection_step, 'confirmed_pseudo'), (setting_pseudo_metadata_step, 'pseudo'))\n", - "ipw.dlink((pseudo_selection_step, 'confirmed_pseudo'), (submit_sssp_work_chain_step, 'pseudo'))\n", - "ipw.dlink((configure_sssp_app_work_chain_step, 'workchain_settings'), (submit_sssp_work_chain_step, 'workchain_settings'))\n", - "ipw.dlink((setting_pseudo_metadata_step, 'output_label'), (submit_sssp_work_chain_step, 'pseudo_label'))\n", - "\n", - "ipw.dlink((submit_sssp_work_chain_step, 'value'), (view_sssp_app_work_chain_status_and_results_step, 'value'))\n", - "\n", - "# Add the application steps to the application\n", - "app = WizardAppWidget(\n", - " steps=[\n", - " ('Select pseudo', pseudo_selection_step),\n", - " ('Configure work chain', configure_sssp_app_work_chain_step),\n", - " ('Pseudopotential metadata setting', setting_pseudo_metadata_step),\n", - " ('Choose computational resources', submit_sssp_work_chain_step),\n", - " ('Status & Results', view_sssp_app_work_chain_status_and_results_step),\n", - " ])\n", - "\n", - "# # Reset all subsequent steps in case that a new pseudo is selected\n", - "# def _observe_pseudo_selection(change):\n", - "# with pseudo_selection_step.hold_sync():\n", - "# if pseudo_selection_step.confirmed_pseudo is not None and \\\n", - "# pseudo_selection_step.confirmed_pseudo != change['new']:\n", - "# app.reset()\n", - " \n", - "# pseudo_selection_step.observe(_observe_pseudo_selection, 'pseudo')\n", - "\n", - "# Add process selection header\n", - "work_chain_selector = WorkChainSelector(layout=ipw.Layout(width='auto'))\n", - "\n", - "def _observe_process_selection(change):\n", - " if change['old'] == change['new']:\n", - " return\n", - "\n", - " pk = change['new']\n", - " if pk is None:\n", - " app.reset()\n", - " app.selected_index = 0\n", - " else:\n", - " process = load_node(pk)\n", - " with pseudo_selection_step.hold_sync():\n", - " app.selected_index = 4\n", - " pseudo_selection_step.confirmed_pseudo = (None, process.inputs.pseudo)\n", - " configure_sssp_app_work_chain_step.state = WizardAppWidgetStep.State.SUCCESS\n", - " submit_sssp_work_chain_step.process = process\n", - "\n", - " \n", - "work_chain_selector.observe(_observe_process_selection, 'value')\n", - "ipw.dlink((submit_sssp_work_chain_step, 'value'), (work_chain_selector, 'value'))\n", - "\n", - "env = Environment()\n", - "\n", - "template = files(static).joinpath(\"welcome.jinja\").read_text()\n", - "style = files(static).joinpath(\"style.css\").read_text()\n", - "welcome_message = ipw.HTML(env.from_string(template).render(style=style))\n", - "footer = ipw.HTML(f'

    Copyright (c) 2022 AiiDAlab team (EPFL) Version: {__version__}

    ')\n", - "\n", - "app_with_work_chain_selector = ipw.VBox(children=[work_chain_selector, app])\n", - "\n", - "output = ipw.Output()\n", - "install_create_github_issue_exception_handler(\n", - " output,\n", - " url='https://github.com/aiidalab/aiidalab-sssp/issues/new',\n", - " labels=('bug', 'automated-report'))\n", - "\n", - "with output:\n", - " display(welcome_message, app_with_work_chain_selector, footer)\n", - " \n", - "display(output)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "[inspect result](./inspect.ipynb?pk=35858)" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.13" - } - }, - "nbformat": 4, - "nbformat_minor": 2 -} diff --git a/viewers.py b/viewers.py deleted file mode 100644 index f4aaefd..0000000 --- a/viewers.py +++ /dev/null @@ -1,99 +0,0 @@ -import warnings - -import ipywidgets as ipw -from aiida.orm import Node -from aiidalab_widgets_base.viewers import ( - BandsDataViewer, - DictViewer, - FolderDataViewer, - StructureDataViewer, -) - - -def viewer(obj, downloadable=True, **kwargs): - """Display AiiDA data types in Jupyter notebooks. - - :param downloadable: If True, add link/button to download the content of displayed AiiDA object. - :type downloadable: bool - - Returns the object itself if the viewer wasn't found.""" - if not isinstance(obj, Node): # only working with AiiDA nodes - warnings.warn( - "This viewer works only with AiiDA objects, got {}".format(type(obj)) - ) - return obj - - try: - _viewer = AIIDA_VIEWER_MAPPING[obj.node_type] - return _viewer(obj, downloadable=downloadable, **kwargs) - except (KeyError) as exc: - if obj.node_type in str(exc): - warnings.warn( - "Did not find an appropriate viewer for the {} object. Returning the object " - "itself.".format(type(obj)) - ) - return obj - raise exc - - -class XyDataViewer(ipw.VBox): - """Viewer class for BandsData object. - - :param bands: XyData object to be viewed - :type bands: XyData""" - - _PLOT_WIDTH = 900 - _LINE_WIDTH = 2 - _LINE_COLOR = "red" - _CIRCLE_SIZE = 8 - _RES_COLOR = "red" - - def __init__(self, xydata, **kwargs): - from bokeh.io import output_notebook, show - from bokeh.layouts import column - from bokeh.plotting import figure - - output_notebook(hide_banner=True) - out = ipw.Output() - with out: - # Extract relevant data - x_data = xydata.get_x()[1] - x_axis_label = xydata.get_x()[0] - x_unit = xydata.get_x()[2] - - figure_list = [] - for y in xydata.get_y(): - y_data = y[1] - y_axis_label = y[0] - y_unit = y[2] - # Create the figure - plot = figure( - plot_width=self._PLOT_WIDTH, - sizing_mode="stretch_width", - x_axis_label=f"{x_axis_label} ({x_unit})", - y_axis_label=f"{y_axis_label} ({y_unit})", - ) - plot.line( - x_data, - y_data, - line_width=self._LINE_WIDTH, - line_color=self._LINE_COLOR, - ) # pylint: disable=too-many-function-args - plot.square( - x_data, y_data, fill_color=self._RES_COLOR, size=self._CIRCLE_SIZE - ) - figure_list.append(plot) - - show(column(*figure_list)) - children = [out] - super().__init__(children, **kwargs) - - -AIIDA_VIEWER_MAPPING = { - "data.dict.Dict.": DictViewer, - "data.structure.StructureData.": StructureDataViewer, - "data.cif.CifData.": StructureDataViewer, - "data.folder.FolderData.": FolderDataViewer, - "data.array.bands.BandsData.": BandsDataViewer, - "data.array.xy.XyData.": XyDataViewer, -}