From 2c5ca5543ed3f63707936e3dcc2b3610774b46c8 Mon Sep 17 00:00:00 2001 From: AndresOrtegaGuerrero <34098967+AndresOrtegaGuerrero@users.noreply.github.com> Date: Fri, 5 Apr 2024 14:43:53 +0200 Subject: [PATCH 1/2] Fix tot magnetization (#512) * updating Magnetization widget, and including logic for insulator * adapting logic of bands and pdos for bandspdoswidget * starting_magnetization as default --- src/aiidalab_qe/app/configuration/advanced.py | 146 ++++++++++++++---- src/aiidalab_qe/app/result/summary_viewer.py | 4 + .../app/static/workflow_summary.jinja | 8 + src/aiidalab_qe/common/bandpdoswidget.py | 115 +++++++++++--- src/aiidalab_qe/plugins/bands/workchain.py | 5 + src/aiidalab_qe/plugins/pdos/workchain.py | 7 + src/aiidalab_qe/workflows/__init__.py | 9 ++ tests/conftest.py | 20 ++- tests/test_result.py | 2 +- tests/test_result/test_summary_report.yml | 1 + 10 files changed, 260 insertions(+), 57 deletions(-) diff --git a/src/aiidalab_qe/app/configuration/advanced.py b/src/aiidalab_qe/app/configuration/advanced.py index 0140685f2..ad2b33264 100644 --- a/src/aiidalab_qe/app/configuration/advanced.py +++ b/src/aiidalab_qe/app/configuration/advanced.py @@ -211,6 +211,11 @@ def _update_input_structure(self, change): self.magnetization.input_structure = None self.pseudo_setter.structure = None + @tl.observe("electronic_type") + def _electronic_type_changed(self, change): + """Input electronic_type changed, update the widget values.""" + self.magnetization.electronic_type = change["new"] + @tl.observe("protocol") def _protocol_changed(self, _): """Input protocol changed, update the widget values.""" @@ -255,17 +260,16 @@ def get_panel_value(self): # XXX: start from parameters = {} and then bundle the settings by purposes (e.g. pw, bands, etc.) parameters = { "initial_magnetic_moments": None, - "pw": { - "parameters": { - "SYSTEM": {}, - }, - }, + "pw": {"parameters": {"SYSTEM": {}}}, + "clean_workdir": self.clean_workdir.value, + "pseudo_family": self.pseudo_family_selector.value, + "kpoints_distance": self.value.get("kpoints_distance"), } - # add clean_workdir to the parameters - parameters["clean_workdir"] = self.clean_workdir.value - # add the pseudo_family to the parameters - parameters["pseudo_family"] = self.pseudo_family_selector.value + # Set total charge + parameters["pw"]["parameters"]["SYSTEM"]["tot_charge"] = self.total_charge.value + + # Set the pseudos if self.pseudo_setter.pseudos: parameters["pw"]["pseudos"] = self.pseudo_setter.pseudos parameters["pw"]["parameters"]["SYSTEM"]["ecutwfc"] = ( @@ -303,8 +307,35 @@ def get_panel_value(self): self.smearing.degauss_value ) + # Set tot_magnetization for collinear simulations. + if self.spin_type == "collinear": + # Conditions for metallic systems. Select the magnetization type and set the value if override is True + if self.electronic_type == "metal" and self.override.value is True: + self.set_metallic_magnetization(parameters) + # Conditions for insulator systems. Default value is 0.0 + elif self.electronic_type == "insulator": + self.set_insulator_magnetization(parameters) + return parameters + def set_insulator_magnetization(self, parameters): + """Set the parameters for collinear insulator calculation. Total magnetization.""" + parameters["pw"]["parameters"]["SYSTEM"]["tot_magnetization"] = ( + self.magnetization.tot_magnetization.value + ) + + def set_metallic_magnetization(self, parameters): + """Set the parameters for magnetization calculation in metals""" + magnetization_type = self.magnetization.magnetization_type.value + if magnetization_type == "tot_magnetization": + parameters["pw"]["parameters"]["SYSTEM"]["tot_magnetization"] = ( + self.magnetization.tot_magnetization.value + ) + else: + parameters["initial_magnetic_moments"] = ( + self.magnetization.get_magnetization() + ) + def set_panel_value(self, parameters): """Set the panel value from the given parameters.""" @@ -338,11 +369,18 @@ def set_panel_value(self, parameters): parameters["pw"]["parameters"]["SYSTEM"].get("vdw_corr", "none"), ) + # Logic to set the magnetization if parameters.get("initial_magnetic_moments"): self.magnetization._set_magnetization_values( parameters.get("initial_magnetic_moments") ) + if "tot_magnetization" in parameters["pw"]["parameters"]["SYSTEM"]: + self.magnetization.magnetization_type.value = "tot_magnetization" + self.magnetization._set_tot_magnetization( + parameters["pw"]["parameters"]["SYSTEM"]["tot_magnetization"] + ) + def reset(self): """Reset the widget and the traitlets""" @@ -363,7 +401,11 @@ def reset(self): self.override.value = False self.smearing.reset() # reset the pseudo setter - self.pseudo_setter._reset() + if self.input_structure is None: + self.pseudo_setter.structure = None + self.pseudo_setter._reset() + else: + self.pseudo_setter._reset() # reset the magnetization self.magnetization.reset() # reset mesh grid @@ -387,10 +429,13 @@ def _display_mesh(self, _=None): class MagnetizationSettings(ipw.VBox): - """Widget to set the initial magnetic moments for each kind names defined in the StructureData (StructureDtaa.get_kind_names()) + """Widget to set the type of magnetization used in the calculation: + 1) Tot_magnetization: Total majority spin charge - minority spin charge. + 2) Starting magnetization: Starting spin polarization on atomic type 'i' in a spin polarized (LSDA or noncollinear/spin-orbit) calculation. + + For Starting magnetization you can set each kind names defined in the StructureData (StructureDtaa.get_kind_names()) Usually these are the names of the elements in the StructureData (For example 'C' , 'N' , 'Fe' . However the StructureData can have defined kinds like 'Fe1' and 'Fe2') - The widget generate a dictionary that can be used to set initial_magnetic_moments in the builder of PwBaseWorkChain Attributes: @@ -398,30 +443,45 @@ class MagnetizationSettings(ipw.VBox): """ input_structure = tl.Instance(orm.StructureData, allow_none=True) - + electronic_type = tl.Unicode() disabled = tl.Bool() + _DEFAULT_TOT_MAGNETIZATION = 0.0 + _DEFAULT_DESCRIPTION = "Magnetization: Input structure not confirmed" def __init__(self, **kwargs): self.input_structure = orm.StructureData() self.input_structure_labels = [] - self.description = ipw.HTML( - "Define magnetization: Input structure not confirmed" + self.tot_magnetization = ipw.BoundedIntText( + min=0, + max=100, + step=1, + value=self._DEFAULT_TOT_MAGNETIZATION, + disabled=True, + description="Total magnetization:", + style={"description_width": "initial"}, + ) + self.magnetization_type = ipw.ToggleButtons( + options=[ + ("Starting Magnetization", "starting_magnetization"), + ("Tot. Magnetization", "tot_magnetization"), + ], + value="starting_magnetization", + style={"description_width": "initial"}, ) + self.description = ipw.HTML(self._DEFAULT_DESCRIPTION) self.kinds = self.create_kinds_widget() self.kinds_widget_out = ipw.Output() + self.magnetization_out = ipw.Output() + self.magnetization_type.observe(self._render, "value") super().__init__( children=[ - ipw.HBox( - [ - self.description, - self.kinds_widget_out, - ], - ), + self.description, + self.magnetization_out, + self.kinds_widget_out, ], layout=ipw.Layout(justify_content="space-between"), **kwargs, ) - self.display_kinds() @tl.observe("disabled") def _disabled_changed(self, _): @@ -429,19 +489,19 @@ def _disabled_changed(self, _): if hasattr(self.kinds, "children") and self.kinds.children: for i in range(len(self.kinds.children)): self.kinds.children[i].disabled = self.disabled + self.tot_magnetization.disabled = self.disabled + self.magnetization_type.disabled = self.disabled def reset(self): self.disabled = True + self.tot_magnetization.value = self._DEFAULT_TOT_MAGNETIZATION + # if self.input_structure is None: - self.description.value = ( - "Define magnetization: Input structure not confirmed" - ) + self.description.value = self._DEFAULT_DESCRIPTION self.kinds = None - with self.kinds_widget_out: - clear_output() - else: - self.update_kinds_widget() + self.description.value = "Magnetization" + self.kinds = self.create_kinds_widget() def create_kinds_widget(self): if self.input_structure_labels: @@ -462,11 +522,30 @@ def create_kinds_widget(self): return kinds_widget + @tl.observe("electronic_type") + def _electronic_type_changed(self, change): + with self.magnetization_out: + clear_output() + if change["new"] == "metal": + display(self.magnetization_type) + self._render({"new": self.magnetization_type.value}) + else: + display(self.tot_magnetization) + with self.kinds_widget_out: + clear_output() + def update_kinds_widget(self): self.input_structure_labels = self.input_structure.get_kind_names() self.kinds = self.create_kinds_widget() - self.description.value = "Define magnetization: " - self.display_kinds() + self.description.value = "Magnetization" + + def _render(self, value): + if value["new"] == "tot_magnetization": + with self.kinds_widget_out: + clear_output() + display(self.tot_magnetization) + else: + self.display_kinds() def display_kinds(self): if "PYTEST_CURRENT_TEST" not in os.environ and self.kinds: @@ -477,6 +556,7 @@ def display_kinds(self): def _update_widget(self, change): self.input_structure = change["new"] self.update_kinds_widget() + self.display_kinds() def get_magnetization(self): """Method to generate the dictionary with the initial magnetic moments""" @@ -497,6 +577,10 @@ def _set_magnetization_values(self, magnetic_moments): else: self.kinds.children[i].value = magnetic_moments + def _set_tot_magnetization(self, tot_magnetization): + """Set the total magnetization""" + self.tot_magnetization.value = tot_magnetization + class SmearingSettings(ipw.VBox): # accept protocol as input and set the values diff --git a/src/aiidalab_qe/app/result/summary_viewer.py b/src/aiidalab_qe/app/result/summary_viewer.py index 59f12e2c6..95840dd00 100644 --- a/src/aiidalab_qe/app/result/summary_viewer.py +++ b/src/aiidalab_qe/app/result/summary_viewer.py @@ -123,6 +123,10 @@ def generate_report_parameters(qeapp_wc): report["periodicity"] = PERIODICITY_MAPPING.get( qeapp_wc.inputs.structure.pbc, "xyz" ) + report["tot_magnetization"] = pw_parameters["SYSTEM"].get( + "tot_magnetization", False + ) + # hard code bands and pdos if "bands" in qeapp_wc.inputs: report["bands_kpoints_distance"] = PwBandsWorkChain.get_protocol_inputs( diff --git a/src/aiidalab_qe/app/static/workflow_summary.jinja b/src/aiidalab_qe/app/static/workflow_summary.jinja index 33630127d..dd54e3718 100644 --- a/src/aiidalab_qe/app/static/workflow_summary.jinja +++ b/src/aiidalab_qe/app/static/workflow_summary.jinja @@ -104,10 +104,18 @@ Van der Waals Correction {{ vdw_corr }} + + {% if tot_magnetization %} + + Total magnetization + {{ tot_magnetization }} + + {% else %} Initial Magnetic Moments {{ initial_magnetic_moments }} + {% endif %} diff --git a/src/aiidalab_qe/common/bandpdoswidget.py b/src/aiidalab_qe/common/bandpdoswidget.py index 540c2c647..04d43a1b5 100644 --- a/src/aiidalab_qe/common/bandpdoswidget.py +++ b/src/aiidalab_qe/common/bandpdoswidget.py @@ -43,12 +43,21 @@ def __init__(self, bands_data=None, pdos_data=None): self._dos_yaxis = self._dos_yaxis() def _get_fermi_energy(self): - fermi_energy = ( - self.pdos_data["fermi_energy"] - if self.pdos_data - else self.bands_data["fermi_energy"] - ) - return fermi_energy + """Function to return the Fermi energy information depending on the data available.""" + fermi_data = {} + if self.pdos_data: + if "fermi_energy_up" in self.pdos_data: + fermi_data["fermi_energy_up"] = self.pdos_data["fermi_energy_up"] + fermi_data["fermi_energy_down"] = self.pdos_data["fermi_energy_down"] + else: + fermi_data["fermi_energy"] = self.pdos_data["fermi_energy"] + else: + if "fermi_energy_up" in self.bands_data: + fermi_data["fermi_energy_up"] = self.bands_data["fermi_energy_up"] + fermi_data["fermi_energy_down"] = self.bands_data["fermi_energy_down"] + else: + fermi_data["fermi_energy"] = self.bands_data["fermi_energy"] + return fermi_data def _band_xaxis(self): """Function to return the xaxis for the bands plot.""" @@ -267,7 +276,7 @@ def _add_band_traces(self, fig, paths, plot_type): scatter_objects.append( go.Scatter( x=band["x"], - y=bands_np - self.fermi_energy, + y=bands_np - self.fermi_energy["fermi_energy"], mode="lines", line=dict( color=self.SETTINGS["bands_linecolor"], @@ -286,16 +295,15 @@ def _add_band_traces(self, fig, paths, plot_type): color_first_half = self.SETTINGS["bands_up_linecolor"] # Blue line for the Spin down color_second_half = self.SETTINGS["bands_down_linecolor"] - - for bands, color in zip( - (first_half, second_half), (color_first_half, color_second_half) - ): - for band_values in bands: - bands_np = np.array(band_values) + if "fermi_energy" in self.fermi_energy: + for bands, color in zip( + (first_half, second_half), (color_first_half, color_second_half) + ): + bands_np = np.array(bands) scatter_objects.append( go.Scatter( x=band["x"], - y=bands_np - self.fermi_energy, + y=bands_np - self.fermi_energy["fermi_energy"], mode="lines", line=dict( color=color, @@ -305,6 +313,30 @@ def _add_band_traces(self, fig, paths, plot_type): showlegend=False, ) ) + else: + for bands, color, fermi_energy in zip( + (first_half, second_half), + (color_first_half, color_second_half), + ( + self.fermi_energy["fermi_energy_up"], + self.fermi_energy["fermi_energy_down"], + ), + ): + for band_values in bands: + bands_np = np.array(band_values) + scatter_objects.append( + go.Scatter( + x=band["x"], + y=bands_np - fermi_energy, + mode="lines", + line=dict( + color=color, + shape="spline", + smoothing=1.3, + ), + showlegend=False, + ) + ) if plot_type == "bands_only": fig.add_traces(scatter_objects) @@ -325,12 +357,41 @@ def _add_dos_traces(self, fig, plot_type): for i, trace in enumerate(dos_data): dos_np = np.array(trace["x"]) fill = "tozerox" if plot_type == "combined" else "tozeroy" - x_data = ( - trace["y"] if plot_type == "combined" else dos_np - self.fermi_energy - ) - y_data = ( - dos_np - self.fermi_energy if plot_type == "combined" else trace["y"] - ) + + if "fermi_energy" in self.fermi_energy: + y_data = ( + dos_np - self.fermi_energy["fermi_energy"] + if plot_type == "combined" + else trace["y"] + ) + x_data = ( + trace["y"] + if plot_type == "combined" + else dos_np - self.fermi_energy["fermi_energy"] + ) + else: + if trace["label"].endswith("(↑)"): + y_data = ( + dos_np - self.fermi_energy["fermi_energy_up"] + if plot_type == "combined" + else trace["y"] + ) + x_data = ( + trace["y"] + if plot_type == "combined" + else dos_np - self.fermi_energy["fermi_energy_up"] + ) + else: + y_data = ( + dos_np - self.fermi_energy["fermi_energy_down"] + if plot_type == "combined" + else trace["y"] + ) + x_data = ( + trace["y"] + if plot_type == "combined" + else dos_np - self.fermi_energy["fermi_energy_down"] + ) scatter_objects[i] = go.Scatter( x=x_data, y=y_data, @@ -632,9 +693,15 @@ def get_pdos_data(pdos, group_tag, plot_tag, selected_atoms): ) data_dict = { - "fermi_energy": pdos.nscf.output_parameters["fermi_energy"], "dos": dos, } + if "fermi_energy_up" in pdos.nscf.output_parameters: + data_dict["fermi_energy_up"] = pdos.nscf.output_parameters["fermi_energy_up"] + data_dict["fermi_energy_down"] = pdos.nscf.output_parameters[ + "fermi_energy_down" + ] + else: + data_dict["fermi_energy"] = pdos.nscf.output_parameters["fermi_energy"] return json.loads(json.dumps(data_dict)) @@ -802,7 +869,11 @@ def export_bands_data(outputs, fermi_energy=None): data = json.loads(outputs.band_structure._exportcontent("json", comments=False)[0]) # The fermi energy from band calculation is not robust. - data["fermi_energy"] = outputs.band_parameters["fermi_energy"] or fermi_energy + if "fermi_energy_up" in outputs.band_parameters: + data["fermi_energy_up"] = outputs.band_parameters["fermi_energy_up"] + data["fermi_energy_down"] = outputs.band_parameters["fermi_energy_down"] + else: + data["fermi_energy"] = outputs.band_parameters["fermi_energy"] or fermi_energy data["pathlabels"] = get_bands_labeling(data) return data diff --git a/src/aiidalab_qe/plugins/bands/workchain.py b/src/aiidalab_qe/plugins/bands/workchain.py index 484ec082f..e60c611f4 100644 --- a/src/aiidalab_qe/plugins/bands/workchain.py +++ b/src/aiidalab_qe/plugins/bands/workchain.py @@ -217,6 +217,11 @@ def get_builder(codes, structure, parameters, **kwargs): bands.pop("relax") bands.pop("structure", None) bands.pop("clean_workdir", None) + + if scf_overrides["pw"]["parameters"]["SYSTEM"].get("tot_magnetization") is not None: + bands.scf["pw"]["parameters"]["SYSTEM"].pop("starting_magnetization", None) + bands.bands["pw"]["parameters"]["SYSTEM"].pop("starting_magnetization", None) + return bands diff --git a/src/aiidalab_qe/plugins/pdos/workchain.py b/src/aiidalab_qe/plugins/pdos/workchain.py index bd9915f2e..b0c8325e7 100644 --- a/src/aiidalab_qe/plugins/pdos/workchain.py +++ b/src/aiidalab_qe/plugins/pdos/workchain.py @@ -66,6 +66,13 @@ def get_builder(codes, structure, parameters, **kwargs): # pop the inputs that are exclueded from the expose_inputs pdos.pop("structure", None) pdos.pop("clean_workdir", None) + + if ( + scf_overrides["pw"]["parameters"]["SYSTEM"].get("tot_magnetization") + is not None + ): + pdos.scf["pw"]["parameters"]["SYSTEM"].pop("starting_magnetization", None) + pdos.nscf["pw"]["parameters"]["SYSTEM"].pop("starting_magnetization", None) else: raise ValueError("The dos_code and projwfc_code are required.") return pdos diff --git a/src/aiidalab_qe/workflows/__init__.py b/src/aiidalab_qe/workflows/__init__.py index ac292cb4a..c2d002815 100644 --- a/src/aiidalab_qe/workflows/__init__.py +++ b/src/aiidalab_qe/workflows/__init__.py @@ -146,6 +146,15 @@ def get_builder_from_protocol( relax_builder.pop("base_final_scf", None) # never run a final scf builder.relax = relax_builder + # remove starting magnetization if tot_magnetization is set + if ( + relax_builder["base"]["pw"]["parameters"]["SYSTEM"].get("tot_magnetization") + is not None + ): + builder.relax["base"]["pw"]["parameters"]["SYSTEM"].pop( + "starting_magnetization", None + ) + if properties is None: properties = [] builder.properties = orm.List(list=properties) diff --git a/tests/conftest.py b/tests/conftest.py index a83ac32cf..cca1ed0d0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -603,7 +603,10 @@ def _generate_qeapp_workchain( run_bands=True, run_pdos=True, spin_type="none", + electronic_type="metal", + magnetization_type="starting_magnetization", # Options: "starting_magnetization", "tot_magnetization" initial_magnetic_moments=0.0, + tot_magnetization=0.0, ): from copy import deepcopy @@ -635,9 +638,20 @@ def _generate_qeapp_workchain( s2.workchain_settings.properties["pdos"].run.value = run_pdos s2.workchain_settings.workchain_protocol.value = "fast" s2.workchain_settings.spin_type.value = spin_type - s2.advanced_settings.magnetization._set_magnetization_values( - initial_magnetic_moments - ) + s2.workchain_settings.electronic_type.value = electronic_type + if spin_type == "collinear": + s2.advanced_settings.override.value = True + magnetization_values = ( + initial_magnetic_moments + if magnetization_type == "starting_magnetization" + else tot_magnetization + ) + s2.advanced_settings.magnetization._set_tot_magnetization( + tot_magnetization + ) if electronic_type == "insulator" else s2.advanced_settings.magnetization._set_magnetization_values( + magnetization_values + ) + s2.confirm() # step 3 setup code and resources s3: SubmitQeAppWorkChainStep = app.submit_step diff --git a/tests/test_result.py b/tests/test_result.py index 7fca7ba85..c6e087d17 100644 --- a/tests/test_result.py +++ b/tests/test_result.py @@ -34,7 +34,7 @@ def test_summary_report_advanced_settings(data_regression, generate_qeapp_workch from aiidalab_qe.app.result.summary_viewer import SummaryView wkchain = generate_qeapp_workchain( - spin_type="collinear", initial_magnetic_moments=0.1 + spin_type="collinear", electronic_type="metal", initial_magnetic_moments=0.1 ) viewer = SummaryView(wkchain.node) report = viewer.report diff --git a/tests/test_result/test_summary_report.yml b/tests/test_result/test_summary_report.yml index 734ec9e11..d41f91b10 100644 --- a/tests/test_result/test_summary_report.yml +++ b/tests/test_result/test_summary_report.yml @@ -27,4 +27,5 @@ relaxed: positions_cell scf_kpoints_distance: 0.5 smearing: cold tot_charge: 0.0 +tot_magnetization: false vdw_corr: none From 0aab1e65dd0733c4f3f636d5a0263b7995b4d7f1 Mon Sep 17 00:00:00 2001 From: Xing Wang Date: Fri, 5 Apr 2024 19:00:09 +0200 Subject: [PATCH 2/2] Add plugin list page (#646) To help users easily find and manage the plugins, this PR adds a new plugin list page. ## Plugin registry User can add their plugin entry point into the `plugins.yaml` file in this repository. ## Home page On the home page, I added a new **Preference** section on the right side of the logo. ## Plugin list page The UI has the following features: - **Accordion Display for Plugins**: Each plugin is listed within an accordion-style component. This design keeps the interface clean and organized, allowing users to quickly scan through available plugins. The accordion's title bar displays essential information: - Plugin Name: Clearly indicates the name of the plugin for easy identification. - Installation Status: An icon indicator shows whether the plugin is currently installed. - **Expandable Details**: Users can interact with the accordion to expand it, revealing more detailed information about each plugin. - **Install button and remove button.** --- docs/source/development/index.rst | 1 + docs/source/development/plugin.rst | 4 + docs/source/development/plugin_registry.rst | 46 +++++ plugin_list.ipynb | 196 ++++++++++++++++++++ plugins.yaml | 17 ++ start.py | 9 + 6 files changed, 273 insertions(+) create mode 100644 docs/source/development/plugin_registry.rst create mode 100644 plugin_list.ipynb create mode 100644 plugins.yaml diff --git a/docs/source/development/index.rst b/docs/source/development/index.rst index b81d49d9e..712b0d37e 100644 --- a/docs/source/development/index.rst +++ b/docs/source/development/index.rst @@ -11,3 +11,4 @@ This guide explains the architecture of the application and how to extend the fu architecture plugin + plugin_registry diff --git a/docs/source/development/plugin.rst b/docs/source/development/plugin.rst index 0f885e421..06a6b3254 100644 --- a/docs/source/development/plugin.rst +++ b/docs/source/development/plugin.rst @@ -362,4 +362,8 @@ Further Reading QuantumESPRESSO app comes with several built-in plugins, which can be found in the ``aiidalab_qe.plugins`` folder. You can also use them as a start point to create your own plugins. + +You can register your plugin to facilitate its discovery and use by the community. +Please refer to the :doc:`Plugin registry ` for more details. + .. _aiidalab-qe-plugin-demos: https://github.com/aiidalab/aiidalab-qe-plugin-demos diff --git a/docs/source/development/plugin_registry.rst b/docs/source/development/plugin_registry.rst new file mode 100644 index 000000000..9d5fcb77a --- /dev/null +++ b/docs/source/development/plugin_registry.rst @@ -0,0 +1,46 @@ + + +Plugin Registry +========================================= + +If you are either in the process of creating a new plugin or already have one developed, you're encouraged to register your plugin here to become part of the official AiiDAlab Quantum ESPRESSO App plugin ecosystem. + +Registering Your Plugin +----------------------- + +To include your plugin in the registry, follow these steps: + +1. Fork this `repository `_. + +2. Add your plugin to the `plugins.yaml` file. Place your entry at the end of the file, following this example: + + .. code-block:: yaml + + aiidalab-qe-xyz: + description: "Quantum ESPRESSO plugin for XYZ by AiiDAlab." + author: "Alice Doe" + github: "https://github.com/alicedoe/aiidalab-qe-xyz" + documentation: "https://aiidalab-qe-xyz.readthedocs.io/" + pip: "aiidalab-qe-xyz" + +3. Submit a Pull Request. Direct it to `this repository's Pull Requests section `_. + +Plugin Entry Requirements +------------------------- + +**Required Keys** + +- **Top-level key:** The plugin's distribution name, which should be lowercase and prefixed by ``aiidalab-`` or ``aiida-``. For example, ``aiidalab-qe-coolfeature`` or ``aiidalab-neutron``. +- **description:** A brief description of your plugin. + +**Optional Keys** + +- **github:** If provided, this should be the URL to the plugin's GitHub homepage. + +At least one of ``github`` or ``pip`` is required. + +- **pip:** The PyPI package name for your plugin, useful for installation via pip. Example: ``aiida-quantum``. +- **documentation:** The URL to your plugin's online documentation, such as ReadTheDocs. +- **author:** The developer of the plugin. + +By following these guidelines, you can ensure your plugin is correctly listed and accessible within the AiiDAlab Quantum ESPRESSO app, facilitating its discovery and use by the community. diff --git a/plugin_list.ipynb b/plugin_list.ipynb new file mode 100644 index 000000000..7f206a19f --- /dev/null +++ b/plugin_list.ipynb @@ -0,0 +1,196 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## AiiDAlab Quantum ESPRESSO Plugin manager\n", + "\n", + "This page lets you manage the plugins. You can find all the plugins that available in the official AiiDAlab Quantum ESPRESSO Plugin registry. You can install and remove plugins from this page.\n", + "\n", + "### Plugin registry\n", + "\n", + "If you are starting to develop a new plugin or if you already have one, and want it discoveried and used by the community. Please refer to this [page](https://aiidalab-qe.readthedocs.io/development/plugin_registry.html) to learn how to register a plugin.\n", + "\n", + "\n", + "### Available plugins\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "import yaml\n", + "\n", + "# URL of the YAML file\n", + "filepath = 'https://raw.githubusercontent.com/aiidalab/aiidalab-qe-plugin-registry/main/plugins.yaml'\n", + "\n", + "# Fetch the contents of the URL\n", + "response = requests.get(filepath)\n", + "\n", + "# Check if the request was successful\n", + "if response.status_code == 200:\n", + " # Load the YAML content\n", + " data = yaml.safe_load(response.content)\n", + " # Now 'data' contains the YAML file's contents as a Python object\n", + "else:\n", + " print(f\"Failed to fetch the YAML file: HTTP {response.status_code}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import subprocess\n", + "from threading import Thread\n", + "\n", + "import ipywidgets as ipw\n", + "from IPython.display import display\n", + "\n", + "\n", + "def is_package_installed(package_name):\n", + " import importlib\n", + " package_name = package_name.replace('-', '_')\n", + " try:\n", + " importlib.import_module(package_name)\n", + " return True\n", + " except ImportError:\n", + " return False\n", + "\n", + "\n", + "def stream_output(process, output_widget):\n", + " \"\"\"Reads output from the process and forwards it to the output widget.\"\"\"\n", + " while True:\n", + " output = process.stdout.readline()\n", + " if process.poll() is not None and output == '':\n", + " break\n", + " if output:\n", + " output_widget.value += f\"\"\"
{output}
\"\"\"\n", + "\n", + "\n", + "def execute_command_with_output(command, output_widget, install_btn, remove_btn, action=\"install\"):\n", + " \"\"\"Execute a command and stream its output to the given output widget.\"\"\"\n", + " output_widget.value = \"\" # Clear the widget\n", + " process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1)\n", + " # Create a thread to read the output stream and write it to the output widget\n", + " thread = Thread(target=stream_output, args=(process, output_widget))\n", + " thread.start()\n", + " thread.join() # Wait for the thread to finish\n", + "\n", + " if process.returncode == 0 and action == \"install\":\n", + " output_widget.value += \"\"\"
Command executed successfully.
\"\"\"\n", + " install_btn.disabled = True\n", + " remove_btn.disabled = False\n", + " return True\n", + " elif process.returncode == 0 and action == \"remove\":\n", + " output_widget.value += \"\"\"
Command executed successfully.
\"\"\"\n", + " install_btn.disabled = False\n", + " remove_btn.disabled = True\n", + " return True\n", + " else:\n", + " output_widget.value += \"\"\"
Command failed.
\"\"\"\n", + " return False\n", + "\n", + "\n", + "def install_package(pip, github, output_container, install_btn, remove_btn, accordion, index):\n", + " if pip:\n", + " command = [\"pip\", \"install\", pip]\n", + " else:\n", + " command = [\"pip\", \"install\", \"git+\" + github]\n", + " result = execute_command_with_output(command, output_container, install_btn, remove_btn)\n", + " if result:\n", + " # restart daemon\n", + " accordion.set_title(index, f\"{accordion.get_title(index)[:-2]} ✅\")\n", + " command = [\"verdi\", \"daemon\", \"restart\"]\n", + " subprocess.run(command, capture_output=True, shell=False)\n", + "\n", + "\n", + "def remove_package(package_name, output_container, install_btn, remove_btn, accordion, index):\n", + " package_name = package_name.replace('-', '_')\n", + " command = [\"pip\", \"uninstall\", \"-y\", package_name]\n", + " result = execute_command_with_output(command, output_container, install_btn, remove_btn, action=\"remove\")\n", + " if result:\n", + " accordion.set_title(index, f\"{accordion.get_title(index)[:-2]} ☐\")\n", + " command = [\"verdi\", \"daemon\", \"restart\"]\n", + " subprocess.run(command, capture_output=True, shell=False)\n", + "\n", + "\n", + "accordion = ipw.Accordion()\n", + "\n", + "for i, (plugin_name, plugin_data) in enumerate(data.items()):\n", + " installed = is_package_installed(plugin_name)\n", + " \n", + " # Output container with customized styling\n", + " output_container = ipw.HTML(\n", + " value=\"\"\"\n", + "
\n", + "
\n", + " \"\"\",\n", + " layout=ipw.Layout(\n", + " max_height='250px', \n", + " overflow='auto',\n", + " border='2px solid #CCCCCC'\n", + " )\n", + " )\n", + " \n", + " details = f\"Author: {plugin_data.get('author', 'N/A')}
\" \\\n", + " f\"Description: {plugin_data.get('description', 'No description available')}
\"\n", + " if 'documentation' in plugin_data:\n", + " details += f\"Documentation: Visit
\"\n", + " if 'github' in plugin_data:\n", + " details += f\"Github: Visit\"\n", + "\n", + " install_btn = ipw.Button(description=\"Install\", button_style='success', disabled=installed)\n", + " remove_btn = ipw.Button(description=\"Remove\", button_style='danger', disabled=not installed)\n", + "\n", + " install_btn.on_click(lambda btn, pip=plugin_data.get('pip', None), github=plugin_data.get('github', ''), oc=output_container, ib=install_btn, rb=remove_btn, ac=accordion, index=i: install_package(pip, github, oc, ib, rb, ac, index))\n", + " remove_btn.on_click(lambda btn, pn=plugin_name, oc=output_container, ib=install_btn, rb=remove_btn, ac=accordion, index=i: remove_package(pn, oc, ib, rb, ac, index))\n", + "\n", + " box = ipw.VBox([\n", + " ipw.HTML(details),\n", + " ipw.HBox([install_btn, remove_btn]),\n", + " output_container # Include the output container in the VBox\n", + " ])\n", + "\n", + " title_with_icon = f\"{plugin_name} {'✅' if installed else '☐'}\"\n", + " accordion.set_title(i, title_with_icon)\n", + " accordion.children = list(accordion.children) + [box]\n", + "\n", + "display(accordion)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "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/plugins.yaml b/plugins.yaml new file mode 100644 index 000000000..929d5cfc3 --- /dev/null +++ b/plugins.yaml @@ -0,0 +1,17 @@ +--- +aiida-bader: + description: AiiDA plugin for the Bader analysis + author: Xing Wang + github: https://github.com/superstar54/aiida-bader + documentation: https://aiida-bader.readthedocs.io/ + pip: aiida-bader + +aiidalab-qe-vibroscopy: + description: Plugin to compute vibrational properties of materials via the aiida-vibroscopy AiiDA plugin + author: Miki Bonacci, Andres Ortega Guerrero + github: https://github.com/mikibonacci/aiidalab-qe-vibroscopy + +aiidalab-qe-muon: + description: Plugin to compute muon stopping sites and related properties via the aiida-muon and aiida-musconv AiiDA plugins + author: Miki Bonacci + github: https://github.com/mikibonacci/aiidalab-qe-muon diff --git a/start.py b/start.py index 322943c15..fb41a8f08 100644 --- a/start.py +++ b/start.py @@ -4,6 +4,15 @@ def get_start_widget(appbase, jupbase, notebase): return ipw.HTML( f""" + + + + + + +
Preferences