From 94f23c7813a7a5d1ec968600fc37a0a5864384f2 Mon Sep 17 00:00:00 2001 From: Edan Bainglass <45081142+edan-bainglass@users.noreply.github.com> Date: Mon, 6 Jan 2025 16:43:15 +0100 Subject: [PATCH] Handle space/point-group correctly (#1055) This PR uses pymatgen symmetry analyzer classes to extract symmetry group information from the structure for use in the summary. --- .../app/result/components/summary/model.py | 64 ++++++++++++++----- .../app/result/components/summary/schema.json | 5 ++ tests/test_result.py | 30 +++++++++ 3 files changed, 83 insertions(+), 16 deletions(-) diff --git a/src/aiidalab_qe/app/result/components/summary/model.py b/src/aiidalab_qe/app/result/components/summary/model.py index 1c99a0736..5fc4bfcf9 100644 --- a/src/aiidalab_qe/app/result/components/summary/model.py +++ b/src/aiidalab_qe/app/result/components/summary/model.py @@ -161,7 +161,7 @@ def _generate_report_parameters(self): return {} inputs = qeapp_wc.inputs - initial_structure = inputs.structure + structure: orm.StructureData = inputs.structure basic = ui_parameters["workchain"] advanced = ui_parameters["advanced"] ctime = qeapp_wc.ctime @@ -176,21 +176,24 @@ def _generate_report_parameters(self): "creation_time": f"{format_time(ctime)} ({relative_time(ctime)})", "modification_time": f"{format_time(mtime)} ({relative_time(mtime)})", }, + } + + report |= { "initial_structure_properties": { - "structure_pk": initial_structure.pk, - "structure_uuid": initial_structure.uuid, - "formula": initial_structure.get_formula(), - "num_atoms": len(initial_structure.sites), - "space_group": "{} ({})".format( - *initial_structure.get_pymatgen().get_space_group_info() - ), - "cell_lengths": "{:.3f} {:.3f} {:.3f}".format( - *initial_structure.cell_lengths - ), - "cell_angles": "{:.0f} {:.0f} {:.0f}".format( - *initial_structure.cell_angles - ), - }, + "structure_pk": structure.pk, + "structure_uuid": structure.uuid, + "formula": structure.get_formula(), + "num_atoms": len(structure.sites), + } + } + + report["initial_structure_properties"] |= { + **self._get_symmetry_group_info(structure), + "cell_lengths": "{:.3f} {:.3f} {:.3f}".format(*structure.cell_lengths), + "cell_angles": "{:.0f} {:.0f} {:.0f}".format(*structure.cell_angles), + } + + report |= { "basic_settings": { "relaxed": "off" if basic["relax_type"] == "none" @@ -198,7 +201,7 @@ def _generate_report_parameters(self): "protocol": basic["protocol"], "spin_type": "off" if basic["spin_type"] == "none" else "on", "electronic_type": basic["electronic_type"], - "periodicity": PERIODICITY_MAPPING.get(initial_structure.pbc, "xyz"), + "periodicity": PERIODICITY_MAPPING.get(structure.pbc, "xyz"), }, "advanced_settings": {}, } @@ -282,6 +285,35 @@ def _generate_report_parameters(self): return report + def _get_symmetry_group_info(self, structure: orm.StructureData) -> dict: + # HACK the use of the clone for non-molecular systems is due to a rigid + # condition in AiiDA < 2.6 that only considers 3D systems as pymatgen + # `Structure` objects (`Molecule` otherwise). Once AiiDAlab is updated with + # AiiDA 2.6 throughout, we can fall back to using `StructureData.get_pymatgen` + # (which this method mimics) to obtain the correct pymatgen object + # (`Molecule` for 0D systems | `Structure` otherwise) + if any(structure.pbc): + return self._get_pymatgen_structure(structure) + return self._get_pymatgen_molecule(structure) + + @staticmethod + def _get_pymatgen_structure(structure: orm.StructureData) -> dict: + from pymatgen.symmetry.analyzer import SpacegroupAnalyzer + + clone = structure.clone() + clone.pbc = (True, True, True) + analyzer = SpacegroupAnalyzer(structure=clone.get_pymatgen_structure()) + symbol = analyzer.get_space_group_symbol() + number = analyzer.get_space_group_number() + return {"space_group": f"{symbol} ({number})"} + + @staticmethod + def _get_pymatgen_molecule(structure: orm.StructureData) -> dict: + from pymatgen.symmetry.analyzer import PointGroupAnalyzer + + analyzer = PointGroupAnalyzer(mol=structure.get_pymatgen_molecule()) + return {"point_group": analyzer.get_pointgroup()} + @staticmethod def _get_final_calcjob(node: orm.WorkChainNode) -> orm.CalcJobNode | None: """Get the final calculation job node called by a workchain node. diff --git a/src/aiidalab_qe/app/result/components/summary/schema.json b/src/aiidalab_qe/app/result/components/summary/schema.json index 7eef8832c..401f8b37f 100644 --- a/src/aiidalab_qe/app/result/components/summary/schema.json +++ b/src/aiidalab_qe/app/result/components/summary/schema.json @@ -54,6 +54,11 @@ "type": "text", "description": "The space group of the structure" }, + "point_group": { + "title": "Point group", + "type": "text", + "description": "The point group of the molecule" + }, "cell_lengths": { "title": "Cell lengths in Å", "type": "text", diff --git a/tests/test_result.py b/tests/test_result.py index fe682d938..abbcbc8ca 100644 --- a/tests/test_result.py +++ b/tests/test_result.py @@ -1,3 +1,4 @@ +import pytest from bs4 import BeautifulSoup from aiidalab_qe.app.main import App @@ -78,6 +79,35 @@ def test_summary_report_advanced_settings(data_regression, generate_qeapp_workch assert moments["Si"] == 0.1 +@pytest.mark.parametrize( + ("pbc", "symmetry_key"), + [ + [(False, False, False), "point_group"], # 0D + [(True, False, False), "space_group"], # 1D + [(True, True, False), "space_group"], # 2D + [(True, True, True), "space_group"], # 3D + ], +) +def test_summary_report_symmetry_group( + generate_qeapp_workchain, + generate_structure_data, + pbc, + symmetry_key, +): + """Test summary report includes correct symmetry group for all system dimension.""" + + system = generate_structure_data("silicon", pbc=pbc) + workchain = generate_qeapp_workchain( + structure=system, + run_bands=False, + relax_type="none", + ) + model = WorkChainSummaryModel() + model.process_uuid = workchain.node.uuid + report_parameters = model._generate_report_parameters() + assert symmetry_key in report_parameters["initial_structure_properties"] + + def test_summary_view(generate_qeapp_workchain): """Test the report html can be properly generated.""" workchain = generate_qeapp_workchain()