diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17fb687e..8ae8cae1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,11 +51,13 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 30 + services: rabbitmq: image: rabbitmq:latest ports: - 5672:5672 + steps: - name: Check out app diff --git a/miscellaneous/structures/SiO2.xyz b/miscellaneous/structures/SiO2.xyz deleted file mode 100644 index 5361d298..00000000 --- a/miscellaneous/structures/SiO2.xyz +++ /dev/null @@ -1,8 +0,0 @@ -6 -Lattice="4.1801 0.0 0.0 0.0 4.1801 0.0 0.0 0.0 2.6678" Properties=species:S:1:pos:R:3:tags:I:1 spacegroup="P 42/m n m" unit_cell=conventional pbc="T T T" -Si 0.00000000 0.00000000 0.00000000 0 -Si 2.09005000 2.09005000 1.33390000 0 -O 1.28203667 1.28203667 0.00000000 1 -O 2.89806333 2.89806333 0.00000000 1 -O 3.37208667 0.80801333 1.33390000 1 -O 0.80801333 3.37208667 1.33390000 1 diff --git a/setup.cfg b/setup.cfg index c3def697..ec6d2e0c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,6 +53,7 @@ dev = [options.package_data] aiidalab_qe.app.parameters = qeapp.yaml aiidalab_qe.app.static = * +aiidalab_qe.app.structure.examples = * [options.entry_points] aiidalab_qe.properties = diff --git a/src/aiidalab_qe/app/structure/__init__.py b/src/aiidalab_qe/app/structure/__init__.py index 670dcfed..90327e6b 100644 --- a/src/aiidalab_qe/app/structure/__init__.py +++ b/src/aiidalab_qe/app/structure/__init__.py @@ -2,6 +2,7 @@ Authors: AiiDAlab team """ +import pathlib import warnings import aiida @@ -27,13 +28,14 @@ # The Examples list of (name, file) tuple curretly passed to # StructureExamplesWidget. +file_path = pathlib.Path(__file__).parent Examples = [ - ("Silicon (diamond)", "miscellaneous/structures/Si.xyz"), - ("Silicon oxide", "miscellaneous/structures/SiO2.xyz"), - ("Diamond", "miscellaneous/structures/diamond.cif"), - ("Gallium arsenide", "miscellaneous/structures/GaAs.xyz"), - ("Gold (fcc)", "miscellaneous/structures/Au.cif"), - ("Cobalt (hcp)", "miscellaneous/structures/Co.cif"), + ("Silicon (diamond)", file_path / "examples" / "Si.xyz"), + ("Silicon oxide", file_path / "examples" / "SiO2.xyz"), + ("Diamond", file_path / "examples" / "diamond.cif"), + ("Gallium arsenide", file_path / "examples" / "GaAs.xyz"), + ("Gold (fcc)", file_path / "examples" / "Au.cif"), + ("Cobalt (hcp)", file_path / "examples" / "Co.cif"), ] diff --git a/miscellaneous/structures/Au.cif b/src/aiidalab_qe/app/structure/examples/Au.cif similarity index 100% rename from miscellaneous/structures/Au.cif rename to src/aiidalab_qe/app/structure/examples/Au.cif diff --git a/miscellaneous/structures/Co.cif b/src/aiidalab_qe/app/structure/examples/Co.cif similarity index 100% rename from miscellaneous/structures/Co.cif rename to src/aiidalab_qe/app/structure/examples/Co.cif diff --git a/miscellaneous/structures/GaAs.xyz b/src/aiidalab_qe/app/structure/examples/GaAs.xyz similarity index 100% rename from miscellaneous/structures/GaAs.xyz rename to src/aiidalab_qe/app/structure/examples/GaAs.xyz diff --git a/miscellaneous/structures/Si.xyz b/src/aiidalab_qe/app/structure/examples/Si.xyz similarity index 100% rename from miscellaneous/structures/Si.xyz rename to src/aiidalab_qe/app/structure/examples/Si.xyz diff --git a/src/aiidalab_qe/app/structure/examples/SiO2.xyz b/src/aiidalab_qe/app/structure/examples/SiO2.xyz new file mode 100644 index 00000000..5838ae55 --- /dev/null +++ b/src/aiidalab_qe/app/structure/examples/SiO2.xyz @@ -0,0 +1,8 @@ +6 +Lattice="4.1801 0.0 0.0 0.0 4.1801 0.0 0.0 0.0 2.6678" Properties=species:S:1:pos:R:3:tags:I:1 spacegroup="P 42/m n m" unit_cell=conventional pbc="T T T" +Si 0.00000000 0.00000000 0.00000000 0 +Si 2.09005000 2.09005000 1.33390000 0 +O 1.28203667 1.28203667 0.00000000 1 +O 2.89806333 2.89806333 0.00000000 1 +O 3.37208667 0.80801333 1.33390000 1 +O 0.80801333 3.37208667 1.33390000 1 diff --git a/src/aiidalab_qe/app/structure/examples/__init__.py b/src/aiidalab_qe/app/structure/examples/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/miscellaneous/structures/diamond.cif b/src/aiidalab_qe/app/structure/examples/diamond.cif similarity index 100% rename from miscellaneous/structures/diamond.cif rename to src/aiidalab_qe/app/structure/examples/diamond.cif diff --git a/tests/conftest.py b/tests/conftest.py index 753cc834..b5a66416 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,28 @@ def fixture_localhost(aiida_localhost): return localhost +@pytest.fixture +def fixture_code(fixture_localhost): + """Return an ``InstalledCode`` instance configured to run calculations of given entry point on localhost.""" + + def _fixture_code(entry_point_name): + from aiida.orm import InstalledCode, load_code + + label = f"test.{entry_point_name}" + + try: + return load_code(label=label) + except Exception: + return InstalledCode( + label=label, + computer=fixture_localhost, + filepath_executable="/bin/true", + default_calc_job_plugin=entry_point_name, + ) + + return _fixture_code + + @pytest.fixture def generate_structure_data(): """generate a `StructureData` object.""" @@ -51,6 +73,93 @@ def _generate_structure_data(name="silicon"): return _generate_structure_data +@pytest.fixture +def generate_xy_data(): + """Return an ``XyData`` instance.""" + + def _generate_xy_data(xvals=None, yvals=None, xlabel=None, ylabel=None): + """Return an ``XyData`` node. + xvals and yvals are lists, and should have the same length. + """ + from aiida.orm import XyData + + xvals = xvals + yvals = yvals + xlabel = xlabel + ylabel = ylabel + xunits = "n/a" + yunits = ["n/a"] * len(ylabel) + + xy_node = XyData() + xy_node.set_x(xvals, xlabel, xunits) + xy_node.set_y(yvals, ylabel, yunits) + xy_node.store() + return xy_node + + return _generate_xy_data + + +@pytest.fixture +def generate_bands_data(): + """Return a `BandsData` node.""" + + def _generate_bands_data(): + """Return a `BandsData` instance with some basic `kpoints` and `bands` arrays.""" + import numpy as np + from aiida.plugins import DataFactory + + BandsData = DataFactory("core.array.bands") + + kpoints = np.array([[0.0, 0.0, 0.0]]) + bands = np.array([[-5.64024889]]) + bands_data = BandsData() + bands_data.set_kpoints(kpoints) + bands_data.set_bands(bands, units="eV") + bands_data.store() + + return bands_data + + return _generate_bands_data + + +@pytest.fixture +def generate_projection_data(generate_bands_data): + """Return an ``ProjectionData`` instance.""" + + def _generate_projection_data(): + """Return an ``ProjectionData`` node.""" + import numpy as np + from aiida.plugins import DataFactory, OrbitalFactory + + ProjectionData = DataFactory("core.array.projection") + OrbitalCls = OrbitalFactory("core.realhydrogen") + + state_dict = { + "kind_name": "C", + "angular_momentum": 0, + "magnetic_number": 0, + "radial_nodes": 1, + "position": [0.0, 0.0, 0.0], + } + orbitals = [OrbitalCls(**state_dict)] + energy_arrays = np.array([1]) + pdos_arrays = np.array([1]) + + projection_data = ProjectionData() + bands_data = generate_bands_data() + projection_data.set_reference_bandsdata(bands_data) + projection_data.set_projectiondata( + orbitals, + list_of_energy=energy_arrays, + list_of_pdos=pdos_arrays, + bands_check=False, + ) + projection_data.store() + return projection_data + + return _generate_projection_data + + @pytest.fixture(scope="session", autouse=True) def sssp(aiida_profile, generate_upf_data): """Create an SSSP pseudo potential family from scratch.""" @@ -133,8 +242,9 @@ def _generate_upf_data(element, filename=None): @pytest.fixture def pw_code(aiida_local_code_factory): """Return a `Code` configured for the pw.x executable.""" + return aiida_local_code_factory( - label="pw@localhost", executable="bash", entry_point="quantumespresso.pw" + label="pw", executable="bash", entry_point="quantumespresso.pw" ) @@ -142,7 +252,7 @@ def pw_code(aiida_local_code_factory): def dos_code(aiida_local_code_factory): """Return a `Code` configured for the dos.x executable.""" return aiida_local_code_factory( - label="dos@localhost", executable="bash", entry_point="quantumespresso.dos" + label="dos", executable="bash", entry_point="quantumespresso.dos" ) @@ -150,7 +260,7 @@ def dos_code(aiida_local_code_factory): def projwfc_code(aiida_local_code_factory): """Return a `Code` configured for the projwfc.x executable.""" return aiida_local_code_factory( - label="projwfc@localhost", + label="projwfc", executable="bash", entry_point="quantumespresso.projwfc", ) @@ -202,6 +312,21 @@ def app(pw_code, dos_code, projwfc_code, sssp): from aiidalab_qe.app.main import App app = App(qe_auto_setup=False) + # set up codes + app.submit_step.pw_code.refresh() + app.submit_step.dos_code.refresh() + app.submit_step.projwfc_code.refresh() + app.submit_step.pw_code.value = ( + app.submit_step.pw_code.code_select_dropdown.options[pw_code.full_label] + ) + app.submit_step.dos_code.value = ( + app.submit_step.dos_code.code_select_dropdown.options[dos_code.full_label] + ) + app.submit_step.projwfc_code.value = ( + app.submit_step.projwfc_code.code_select_dropdown.options[ + projwfc_code.full_label + ] + ) yield app @@ -210,9 +335,6 @@ def app(pw_code, dos_code, projwfc_code, sssp): @pytest.mark.usefixtures("sssp") def submit_app_generator( app, - pw_code, - dos_code, - projwfc_code, generate_structure_data, workchain_settings_generator, smearing_settings_generator, @@ -262,10 +384,257 @@ def _submit_app_generator( submit_step = app.submit_step submit_step.input_structure = generate_structure_data() - submit_step.pw_code.value = pw_code.uuid - submit_step.dos_code.value = dos_code.uuid - submit_step.projwfc_code.value = projwfc_code.uuid - return app return _submit_app_generator + + +@pytest.fixture +def app_to_submit(app): + # Step 1: select structure from example + step1 = app.steps.steps[0][1] + structure = step1.manager.children[0].children[3] + structure.children[0].value = structure.children[0].options[1][1] + step1.confirm() + # Step 2: configure calculation + step2 = app.steps.steps[1][1] + step2.workchain_settings.properties["bands"].run.value = True + step2.workchain_settings.properties["pdos"].run.value = True + step2.confirm() + yield app + + +@pytest.fixture +def generate_workchain(): + """Generate an instance of a `WorkChain`.""" + + def _generate_workchain(process_class, inputs): + """Generate an instance of a `WorkChain` with the given entry point and inputs. + + :param entry_point: entry point name of the work chain subclass. + :param inputs: inputs to be passed to process construction. + :return: a `WorkChain` instance. + """ + from aiida.engine.utils import instantiate_process + from aiida.manage.manager import get_manager + + runner = get_manager().get_runner() + process = instantiate_process(runner, process_class, **inputs) + + return process + + return _generate_workchain + + +@pytest.fixture +def generate_pdos_workchain( + generate_structure_data, + fixture_localhost, + fixture_code, + generate_xy_data, + generate_projection_data, + generate_workchain, +): + """Generate an instance of a `XpsWorkChain`.""" + + def _generate_pdos_workchain(spin_type="none"): + import numpy as np + from aiida import engine + from aiida.orm import Dict, FolderData, RemoteData + from aiida_quantumespresso.workflows.pdos import PdosWorkChain + + inputs = { + "pw_code": fixture_code("quantumespresso.pw"), + "dos_code": fixture_code("quantumespresso.dos"), + "projwfc_code": fixture_code("quantumespresso.projwfc"), + "structure": generate_structure_data(), + } + builder = PdosWorkChain.get_builder_from_protocol(**inputs) + inputs = builder._inputs() + wkchain = generate_workchain(PdosWorkChain, inputs) + wkchain.setup() + # wkchain.run_pdos() + remote = RemoteData(remote_path="/tmp/aiida_run") + remote.computer = fixture_localhost + remote.store() + retrieved = FolderData(tree="/tmp/aiida_run") + retrieved.store() + output_parameters = Dict(dict={"fermi_energy": 2.0}) + output_parameters.store() + proj = generate_projection_data() + proj.store() + if spin_type == "none": + xy = generate_xy_data( + np.array([1, 2, 3]), [np.array([1, 2, 3])], "X", ["dos"] + ) + xy.store() + wkchain.out( + "dos", + { + "output_dos": xy, + "output_parameters": output_parameters, + "remote_folder": remote, + "retrieved": retrieved, + }, + ) + wkchain.out( + "projwfc", + { + "Dos": xy, + "projections": proj, + "output_parameters": output_parameters, + "remote_folder": remote, + "retrieved": retrieved, + }, + ) + else: + xy = generate_xy_data( + np.array([1, 2, 3]), + [np.array([1, 2, 3]), np.array([1, 2, 3])], + "X", + ["dos_spin_up", "dos_spin_down"], + ) + xy.store() + wkchain.out( + "dos", + { + "output_dos": xy, + "output_parameters": output_parameters, + "remote_folder": remote, + "retrieved": retrieved, + }, + ) + wkchain.out( + "projwfc", + { + "Dos": xy, + "projections_up": proj, + "projections_down": proj, + "output_parameters": output_parameters, + "remote_folder": remote, + "retrieved": retrieved, + }, + ) + wkchain.out( + "nscf", + { + "output_parameters": output_parameters, + "remote_folder": remote, + "retrieved": retrieved, + }, + ) + wkchain.update_outputs() + pdos_node = wkchain.node + pdos_node.set_exit_status(0) + pdos_node.set_process_state(engine.ProcessState.FINISHED) + # set + return wkchain + + return _generate_pdos_workchain + + +@pytest.fixture +def generate_bands_workchain( + generate_structure_data, + fixture_localhost, + fixture_code, + generate_xy_data, + generate_bands_data, + generate_workchain, +): + """Generate an instance of a the WorkChain.""" + + def _generate_bands_workchain(): + from copy import deepcopy + + from aiida import engine + from aiida.orm import Dict + from aiida_quantumespresso.workflows.pw.bands import PwBandsWorkChain + + inputs = { + "code": fixture_code("quantumespresso.pw"), + "structure": generate_structure_data(), + } + builder = PwBandsWorkChain.get_builder_from_protocol(**inputs) + inputs = builder._inputs() + inputs["relax"]["base_final_scf"] = deepcopy(inputs["relax"]["base"]) + wkchain = generate_workchain(PwBandsWorkChain, inputs) + wkchain.setup() + # run bands and return the process + output_parameters = Dict(dict={"fermi_energy": 2.0}) + output_parameters.store() + wkchain.out("scf_parameters", output_parameters) + wkchain.out("band_parameters", output_parameters) + # + band_structure = generate_bands_data() + band_structure.store() + wkchain.out("band_structure", band_structure) + wkchain.update_outputs() + # + bands_node = wkchain.node + bands_node.set_exit_status(0) + bands_node.set_process_state(engine.ProcessState.FINISHED) + return wkchain + + return _generate_bands_workchain + + +@pytest.fixture +def generate_qeapp_workchain( + app, generate_workchain, generate_pdos_workchain, generate_bands_workchain +): + """Generate an instance of the WorkChain.""" + + def _generate_qeapp_workchain( + relax_type="positions_cell", run_bands=True, run_pdos=True, spin_type="none" + ): + from copy import deepcopy + + from aiidalab_qe.workflows import QeAppWorkChain + + # Step 1: select structure from example + s1 = app.structure_step + structure = s1.manager.children[0].children[3] + structure.children[0].value = structure.children[0].options[1][1] + s1.confirm() + # step 2 configure + s2 = app.configure_step + s2.workchain_settings.relax_type.value = relax_type + # In order to parepare a complete inputs, I set all the properties to true + # this can be overrided later + s2.workchain_settings.properties["bands"].run.value = run_bands + 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.confirm() + # step 3 setup code and resources + s3 = app.submit_step + s3.resources_config.num_cpus.value = 4 + builder = s3._create_builder() + inputs = builder._inputs() + inputs["relax"]["base_final_scf"] = deepcopy(inputs["relax"]["base"]) + if run_bands: + inputs["properties"].append("bands") + if run_pdos: + inputs["properties"].append("pdos") + wkchain = generate_workchain(QeAppWorkChain, inputs) + wkchain.setup() + # mock output + if run_pdos: + from aiida_quantumespresso.workflows.pdos import PdosWorkChain + + pdos = generate_pdos_workchain(spin_type) + wkchain.out_many( + wkchain.exposed_outputs(pdos.node, PdosWorkChain, namespace="pdos") + ) + if run_bands: + from aiida_quantumespresso.workflows.pw.bands import PwBandsWorkChain + + bands = generate_bands_workchain() + wkchain.out_many( + wkchain.exposed_outputs(bands.node, PwBandsWorkChain, namespace="bands") + ) + wkchain.update_outputs() + return wkchain + + return _generate_qeapp_workchain diff --git a/tests/test_plugins_bands.py b/tests/test_plugins_bands.py new file mode 100644 index 00000000..3b1c3a18 --- /dev/null +++ b/tests/test_plugins_bands.py @@ -0,0 +1,14 @@ +def test_result(generate_qeapp_workchain): + from widget_bandsplot import BandsPlotWidget + + from aiidalab_qe.plugins.bands.result import Result, export_bands_data + + wkchain = generate_qeapp_workchain() + # + data = export_bands_data(wkchain.node.outputs.bands) + assert data is not None + # generate structure for scf calculation + result = Result(wkchain.node) + assert result.identifier == "bands" + result._update_view() + assert isinstance(result.children[0], BandsPlotWidget) diff --git a/tests/test_plugins_pdos.py b/tests/test_plugins_pdos.py new file mode 100644 index 00000000..ce5d1406 --- /dev/null +++ b/tests/test_plugins_pdos.py @@ -0,0 +1,35 @@ +def test_result(generate_qeapp_workchain): + from aiidalab_qe.plugins.pdos.result import Result, export_pdos_data + + wkchain = generate_qeapp_workchain() + data = export_pdos_data(wkchain.node.outputs.pdos) + assert data is not None + # generate structure for scf calculation + result = Result(node=wkchain.node) + assert result.identifier == "pdos" + result._update_view() + assert len(result.children) == 2 + + +def test_result_spin(generate_qeapp_workchain): + from aiidalab_qe.plugins.pdos.result import Result, export_pdos_data + + wkchain = generate_qeapp_workchain(spin_type="collinear") + data = export_pdos_data(wkchain.node.outputs.pdos) + assert data is not None + # generate structure for scf calculation + result = Result(node=wkchain.node) + result._update_view() + assert len(result.children) == 2 + + +def test_result_group_by(generate_qeapp_workchain): + from aiidalab_qe.plugins.pdos.result import Result, export_pdos_data + + wkchain = generate_qeapp_workchain() + data = export_pdos_data(wkchain.node.outputs.pdos) + assert data is not None + # generate structure for scf calculation + result = Result(node=wkchain.node) + result._update_view() + result.children[0].children[0].children[1].value = "angular" diff --git a/tests_notebooks/test_qe_app.py b/tests_notebooks/test_qe_app.py index 741c1b87..5f4dc83c 100755 --- a/tests_notebooks/test_qe_app.py +++ b/tests_notebooks/test_qe_app.py @@ -32,17 +32,19 @@ def test_qe_app_select_silicon_and_confirm( ) element.click() - driver.find_element(By.XPATH, "//option[@value='Diamond']").click() - time.sleep(10) - + try: + driver.find_element(By.XPATH, "//option[@value='Diamond']").click() + time.sleep(10) + element = WebDriverWait(driver, 60).until( + EC.element_to_be_clickable((By.XPATH, "//button[text()='Confirm']")) + ) + element.click() + except Exception: + driver.find_element(By.TAG_NAME, "summary").click() + time.sleep(10) + # Take a screenshot of the selected diamond driver.get_screenshot_as_file( str(Path.joinpath(screenshot_dir, "qe-app-select-diamond-selected.png")) ) - - element = WebDriverWait(driver, 60).until( - EC.element_to_be_clickable((By.XPATH, "//button[text()='Confirm']")) - ) - element.click() - # Test that we have indeed proceeded to the next step driver.find_element(By.XPATH, "//span[contains(.,'✓ Step 1')]")