From fac569afed85695936e0306dd9763f89359ca75b Mon Sep 17 00:00:00 2001 From: QuantumMisaka Date: Sat, 17 Oct 2026 00:27:48 +0800 Subject: [PATCH 01/12] test: add independent abacuslite ASE candidate lane --- .github/workflows/ase_plugin_test.yml | 73 ++++++++++++++---- docs/advanced/interface/ase.md | 10 +++ interfaces/ASE_interface/README.md | 33 ++++++++ interfaces/ASE_interface/tests/band.py | 19 +++-- interfaces/ASE_interface/tests/common.py | 47 ++++++++++++ interfaces/ASE_interface/tests/magnetic.py | 16 +++- interfaces/ASE_interface/tests/md.py | 34 ++++++--- interfaces/ASE_interface/tests/relax.py | 32 +++++--- interfaces/ASE_interface/tests/scf.py | 16 +++- .../ASE_interface/tests/socket_handoff.py | 76 +++++++++++++++++++ .../tests/test_external_provenance.py | 42 ++++++++++ .../ASE_interface/tests/verify_external.py | 40 ++++++++++ interfaces/ASE_interface/tests/xtest.sh | 3 + 13 files changed, 392 insertions(+), 49 deletions(-) create mode 100644 interfaces/ASE_interface/tests/socket_handoff.py create mode 100644 interfaces/ASE_interface/tests/test_external_provenance.py create mode 100644 interfaces/ASE_interface/tests/verify_external.py diff --git a/.github/workflows/ase_plugin_test.yml b/.github/workflows/ase_plugin_test.yml index 822afcd957c..ccbb1b63141 100644 --- a/.github/workflows/ase_plugin_test.yml +++ b/.github/workflows/ase_plugin_test.yml @@ -2,6 +2,12 @@ name: Atomic Simulation Environment (ASE) Plugin Test on: pull_request: + workflow_dispatch: + inputs: + plugin_ref: + description: Reviewed QuantumMisaka/abacuslite commit or ref + required: false + default: 136783baf4b6832c13d02a2542753439fbacc6df defaults: run: @@ -9,15 +15,32 @@ defaults: jobs: test: - name: abacuslite + name: abacuslite (${{ matrix.package }}) runs-on: ubuntu-latest + timeout-minutes: 120 container: image: ghcr.io/deepmodeling/abacus-gnu + strategy: + fail-fast: false + matrix: + package: [bundled, external] + env: + ABACUS_COMMAND: mpirun -np 2 abacus + ASE_ABACUS_ASSETS: ${{ github.workspace }}/tests/PP_ORB steps: - - name: Checkout code + - name: Checkout engine PR uses: actions/checkout@v7.0.1 + - name: Checkout reviewed candidate + if: matrix.package == 'external' + uses: actions/checkout@v7.0.1 + with: + repository: QuantumMisaka/abacuslite + ref: ${{ inputs.plugin_ref || '136783baf4b6832c13d02a2542753439fbacc6df' }} + path: candidate-plugin + persist-credentials: false + - name: Set up Miniconda uses: conda-incubator/setup-miniconda@v4 with: @@ -31,12 +54,24 @@ jobs: conda install -y pip pip install numpy scipy matplotlib - - name: Install abacuslite + - name: Install bundled plugin + if: matrix.package == 'bundled' shell: bash -l {0} run: | cd interfaces/ASE_interface pip install . + - name: Build and install candidate wheel + if: matrix.package == 'external' + shell: bash -l {0} + run: | + python -m pip install 'build>=1.2' + python -m build --wheel --outdir "$RUNNER_TEMP/candidate-wheel" candidate-plugin + sha256sum "$RUNNER_TEMP"/candidate-wheel/*.whl + python -m pip install "$RUNNER_TEMP"/candidate-wheel/*.whl + git -C candidate-plugin rev-parse HEAD + python -m pip show abacuslite ase numpy seekpath + - name: Install external tools from toolchain run: | sudo apt update && sudo apt install -y xz-utils ninja-build pkg-config @@ -53,38 +88,48 @@ jobs: rm -rf build cmake -B build -G Ninja cmake --build build -j2 - + - name: Install and Soft Link ABACUS run: | cmake --install build if [ -f /usr/local/bin/abacus_2p ]; then ln -sf /usr/local/bin/abacus_2p /usr/local/bin/abacus fi + git rev-parse HEAD + abacus --version - - name: Run unit tests + - name: Run bundled unit tests + if: matrix.package == 'bundled' shell: bash -l {0} run: | cd interfaces/ASE_interface/abacuslite chmod +x xtest.sh ./xtest.sh - - name: Run integration tests + - name: Run provenance regressions + if: matrix.package == 'external' shell: bash -l {0} run: | cd interfaces/ASE_interface/tests - chmod +x xtest.sh - ./xtest.sh + python -m unittest test_external_provenance -v + + - name: Run engine-owned integration tests + shell: bash -l {0} + run: | + mkdir -p "$RUNNER_TEMP/ase-engine-tests" + cp interfaces/ASE_interface/tests/*.py interfaces/ASE_interface/tests/xtest.sh "$RUNNER_TEMP/ase-engine-tests/" + cd "$RUNNER_TEMP/ase-engine-tests" + if [ "${{ matrix.package }}" = external ]; then + python verify_external.py --bundled-root "$GITHUB_WORKSPACE/interfaces/ASE_interface" --candidate-root "$GITHUB_WORKSPACE/candidate-plugin" + export ASE_ABACUS_SOCKET=1 + fi + timeout 30m bash ./xtest.sh - # When a test fails, every integration test script (scf/relax/md/band/ - # magnetic) preserves its whole ABACUS run directory under - # /tmp/abacus_ase_failure (see preserved_tmpdir in tests/common.py, - # overridable via ASE_ABACUS_KEEP_DIR). Upload it so remote-CI-only - # failures (e.g. issue #7794) can be root-caused without runner access. - name: Upload ASE failure artifacts if: failure() uses: actions/upload-artifact@v7 with: - name: ase-failure-${{ github.run_id }}-${{ github.run_attempt }} + name: ase-failure-${{ matrix.package }}-${{ github.run_id }}-${{ github.run_attempt }} path: /tmp/abacus_ase_failure/** if-no-files-found: ignore retention-days: 14 diff --git a/docs/advanced/interface/ase.md b/docs/advanced/interface/ase.md index b7b02d3a279..570906018f6 100644 --- a/docs/advanced/interface/ase.md +++ b/docs/advanced/interface/ase.md @@ -22,6 +22,16 @@ cd interfaces/ASE_interface pip install . ``` +For the independent candidate package, use a separate environment and build a +wheel from the reviewed `QuantumMisaka/abacuslite` commit +`136783baf4b6832c13d02a2542753439fbacc6df`. The ABACUS PR CI has +parallel `bundled` and `external` jobs; the latter installs this wheel and runs +the engine-owned tests from outside the bundled source tree. The local command +and PP/ORB asset location are selected with `ABACUS_COMMAND` and +`ASE_ABACUS_ASSETS`. Set `ASE_ABACUS_SOCKET=1` to include the bounded fixed-cell +socket case. See [the ASE interface README](../../../interfaces/ASE_interface/README.md) +for the exact candidate installation and test commands. + ## ABACUS Calculator Present calculator implementation requires a "profile" to act as an interface between the Python runtime and the file system. diff --git a/interfaces/ASE_interface/README.md b/interfaces/ASE_interface/README.md index 543d4774836..02a951e7ed6 100644 --- a/interfaces/ASE_interface/README.md +++ b/interfaces/ASE_interface/README.md @@ -21,6 +21,39 @@ cd interfaces/ASE_interface pip install . ``` +For coordinated testing of the independent candidate, build a wheel from the +reviewed [QuantumMisaka/abacuslite](https://github.com/QuantumMisaka/abacuslite) +commit and install it in a separate environment. The bundled installation above +remains available while the migration is reviewed: + +```bash +git clone https://github.com/QuantumMisaka/abacuslite.git /tmp/abacuslite-candidate +git -C /tmp/abacuslite-candidate checkout 136783baf4b6832c13d02a2542753439fbacc6df +python -m pip install build +python -m build --wheel --outdir /tmp/abacuslite-wheel /tmp/abacuslite-candidate +python -m pip install /tmp/abacuslite-wheel/*.whl +mkdir -p /tmp/abacus-ase-engine-tests +cp /path/to/abacus-develop/interfaces/ASE_interface/tests/*.py \ + /path/to/abacus-develop/interfaces/ASE_interface/tests/xtest.sh \ + /tmp/abacus-ase-engine-tests/ +cd /tmp/abacus-ase-engine-tests +python verify_external.py \ + --bundled-root /path/to/abacus-develop/interfaces/ASE_interface \ + --candidate-root /tmp/abacuslite-candidate +ABACUS_COMMAND='mpirun -np 2 /path/to/abacus' \ +ASE_ABACUS_ASSETS=/path/to/abacus-develop/tests/PP_ORB \ +ASE_ABACUS_SOCKET=1 \ +bash ./xtest.sh +``` + +Run the scripts from outside `interfaces/ASE_interface` so that the bundled +source cannot shadow the installed wheel. In CI the `external` job checks +distribution metadata and the imported `abacuslite.__file__`, records the engine +revision and `abacus --version`, and retains failed calculation directories. +`workflow_dispatch` accepts a reviewed `plugin_ref` for coordinated runs. +The scripts above require a built ABACUS executable and the curated PP/ORB +assets; ordinary Python checks do not launch the engine. + ## Usage Examples Please refer to the example scripts in the `examples` folder. Recommended learning path: diff --git a/interfaces/ASE_interface/tests/band.py b/interfaces/ASE_interface/tests/band.py index 1ba7799c83e..97cbfe6dbcf 100644 --- a/interfaces/ASE_interface/tests/band.py +++ b/interfaces/ASE_interface/tests/band.py @@ -1,11 +1,12 @@ import unittest +import numpy as np from pathlib import Path here = Path(__file__).parent from ase.build import bulk from abacuslite.io.generalio import load_pseudo, load_orbital from abacuslite import AbacusProfile, Abacus from abacuslite.utils.ksampling import kpathgen -from common import preserved_tmpdir +from common import preserved_tmpdir, test_profile_paths, assert_scf_facts class TestSCFFollowedNSCF(unittest.TestCase): @@ -18,11 +19,11 @@ def setUp(self): return super().setUp() def test(self): - pporb = here.parent.parent.parent / 'tests' / 'PP_ORB' + command, pporb = test_profile_paths(here) silicon = bulk('Si', 'diamond', a=5.43) aprof = AbacusProfile( - command='mpirun -np 2 abacus', + command=command, pseudo_dir=pporb, orbital_dir=pporb, omp_num_threads=1, @@ -43,6 +44,7 @@ def test(self): silicon.calc = abacus _ = silicon.get_potential_energy() + assert_scf_facts(self, abacus, tmpdir, len(silicon)) # ============================================================= # @@ -54,8 +56,15 @@ def test(self): silicon.calc = bscalc _ = silicon.get_potential_energy() - bscalc.band_structure().write(Path(tmpdir) / 'bandstructure.json') + bands = bscalc.band_structure() + values = np.asarray(bands.energies) + self.assertEqual(values.ndim, 3) + self.assertEqual(values.shape[1], len(bandpath.kpts)) + self.assertGreater(values.shape[2], 0) + self.assertTrue(np.all(np.isfinite(values))) + self.assertTrue(np.allclose(values[0], bscalc.results['eigenvalues'][0], atol=1e-5)) + bands.write(Path(tmpdir) / 'bandstructure.json') self.assertTrue((Path(tmpdir) / 'bandstructure.json').exists()) if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/interfaces/ASE_interface/tests/common.py b/interfaces/ASE_interface/tests/common.py index dab62b072ab..f37e48e7f52 100644 --- a/interfaces/ASE_interface/tests/common.py +++ b/interfaces/ASE_interface/tests/common.py @@ -2,10 +2,57 @@ import contextlib import os +import re import shutil import tempfile from pathlib import Path +import numpy as np +from ase.units import GPa +from ase.stress import full_3x3_to_voigt_6_stress + + +def test_profile_paths(here): + """Return the engine command and curated assets for a real ASE run.""" + assets = Path(os.environ.get('ASE_ABACUS_ASSETS', here.parent.parent.parent / 'tests' / 'PP_ORB')).resolve() + if not assets.is_dir(): + raise FileNotFoundError(f'ASE_ABACUS_ASSETS is not a directory: {assets}') + return os.environ.get('ABACUS_COMMAND', 'mpirun -np 2 abacus'), assets + + +def assert_scf_facts(testcase, calc, directory, natoms, *, forces=False, stress=False): + """Compare calculator facts with ABACUS' independently written SCF log.""" + log = Path(directory) / 'OUT.ABACUS' / 'running_scf.log' + content = log.read_text() + energies = re.findall(r'!FINAL_ETOT_IS\s+([-+\d.Ee]+)\s+eV', content) + testcase.assertTrue(energies, f'no final energy in {log}') + energy = float(calc.results['energy']) + testcase.assertTrue(np.isfinite(energy)) + testcase.assertAlmostEqual(energy, float(energies[-1]), delta=1e-4) + fermi = re.findall(r'^\s*E_Fermi\s+[-+\d.Ee]+\s+([-+\d.Ee]+)', content, re.M) + if fermi: + testcase.assertIn('fermi_level', calc.results) + testcase.assertAlmostEqual(float(calc.results['fermi_level']), float(fermi[-1]), delta=1e-4) + if forces: + values = np.asarray(calc.results['forces']) + testcase.assertEqual(values.shape, (natoms, 3)) + testcase.assertTrue(np.all(np.isfinite(values))) + blocks = re.findall(r'#TOTAL-FORCE \(eV/Angstrom\)#(.*?)(?=\n\s*#|\Z)', content, re.S) + testcase.assertTrue(blocks, f'no raw force table in {log}') + rows = re.findall(r'^\s*[A-Z][a-z]?\d+\s+([-+\d.Ee]+)\s+([-+\d.Ee]+)\s+([-+\d.Ee]+)\s*$', blocks[-1], re.M) + testcase.assertEqual(len(rows), natoms) + testcase.assertTrue(np.allclose(values, np.asarray(rows, dtype=float), rtol=0, atol=1e-4)) + if stress: + values = np.asarray(calc.results['stress']) + testcase.assertEqual(values.shape, (6,)) + testcase.assertTrue(np.all(np.isfinite(values))) + blocks = re.findall(r'#TOTAL-STRESS \(kbar\)#(.*?)(?=\n\s*#|\Z)', content, re.S) + testcase.assertTrue(blocks, f'no raw stress table in {log}') + rows = re.findall(r'^\s*([-+\d.Ee]+)\s+([-+\d.Ee]+)\s+([-+\d.Ee]+)\s*$', blocks[-1], re.M) + testcase.assertGreaterEqual(len(rows), 3) + tensor = -0.1 * np.asarray(rows[:3], dtype=float) * GPa + testcase.assertTrue(np.allclose(values, full_3x3_to_voigt_6_stress(tensor), rtol=0, atol=1e-4)) + @contextlib.contextmanager def preserved_tmpdir(prefix): diff --git a/interfaces/ASE_interface/tests/magnetic.py b/interfaces/ASE_interface/tests/magnetic.py index 080961c3e2e..9ef3f1aba3f 100644 --- a/interfaces/ASE_interface/tests/magnetic.py +++ b/interfaces/ASE_interface/tests/magnetic.py @@ -8,14 +8,14 @@ here = Path(__file__).parent from ase.atoms import Atoms from abacuslite import AbacusProfile, Abacus -from common import preserved_tmpdir +from common import preserved_tmpdir, test_profile_paths, assert_scf_facts class TestMagneticSCF(unittest.TestCase): def setUp(self): - pporb = here.parent.parent.parent / 'tests' / 'PP_ORB' + command, pporb = test_profile_paths(here) self.aprof = AbacusProfile( - command='mpirun -np 2 abacus', + command=command, pseudo_dir=pporb, orbital_dir=pporb, omp_num_threads=1, @@ -45,10 +45,13 @@ def test_antiferromagnetic(self): fe.calc = abacus print(f'Fe (AFM): {fe.get_potential_energy()} eV') + assert_scf_facts(self, abacus, tmpdir, len(fe)) + self.assertEqual(np.asarray(abacus.results['magmoms']).shape, (2,)) for ife, m in enumerate(fe.calc.results['magmoms']): print(f'Fe{ife}: {m:10.6f} uB') self.assertGreater(abs(m), 1e-3) self.assertTrue(np.allclose(np.sum(fe.calc.results['magmoms']), 0.0, atol=1e-6)) + self.assertLess(np.prod(fe.calc.results['magmoms']), 0.0) def test_ferromagnetic(self): @@ -75,10 +78,13 @@ def test_ferromagnetic(self): fe.calc = abacus print(f'Fe (FM): {fe.get_potential_energy()} eV') + assert_scf_facts(self, abacus, tmpdir, len(fe)) + self.assertEqual(np.asarray(abacus.results['magmoms']).shape, (2,)) for ife, m in enumerate(fe.calc.results['magmoms']): print(f'Fe{ife}: {m:10.6f} uB') self.assertGreater(abs(m), 1e-3) self.assertGreater(abs(np.sum(fe.calc.results['magmoms'])), 0.0) + self.assertGreater(np.prod(fe.calc.results['magmoms']), 0.0) # @unittest.skip('see ase/outputs.py:L154-155, the magmom cannot be set as the vector, so ' # 'we skip this test for now') @@ -121,9 +127,11 @@ def test_noncolinear(self): fe.calc = abacus print(f'Fe (non-colinear): {fe.get_potential_energy()} eV') + assert_scf_facts(self, abacus, tmpdir, len(fe)) + self.assertEqual(np.asarray(abacus.results['magmoms']).shape, (2, 3)) for ife, m in enumerate(fe.calc.results['magmoms']): print(f'Fe{ife}: ({",".join([f"{mi:10.6f}" for mi in m])}) uB') self.assertGreater(np.linalg.norm(m), 1e-3) if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/interfaces/ASE_interface/tests/md.py b/interfaces/ASE_interface/tests/md.py index f00b9e3c965..e119a586a49 100644 --- a/interfaces/ASE_interface/tests/md.py +++ b/interfaces/ASE_interface/tests/md.py @@ -1,4 +1,5 @@ import unittest +import numpy as np from pathlib import Path here = Path(__file__).parent from ase.build import bulk @@ -7,16 +8,16 @@ from ase.md.velocitydistribution import MaxwellBoltzmannDistribution from abacuslite.io.generalio import load_pseudo, load_orbital from abacuslite import AbacusProfile, Abacus -from common import preserved_tmpdir +from common import preserved_tmpdir, test_profile_paths, assert_scf_facts class TestLangevinMolecularDynamics(unittest.TestCase): def test(self): - pporb = here.parent.parent.parent / 'tests' / 'PP_ORB' + command, pporb = test_profile_paths(here) silicon = bulk('Si', 'diamond', a=5.43) aprof = AbacusProfile( - command='mpirun -np 2 abacus', + command=command, pseudo_dir=pporb, orbital_dir=pporb, omp_num_threads=1, @@ -38,13 +39,24 @@ def test(self): } ) - silicon.calc = abacus - MaxwellBoltzmannDistribution(silicon, temperature_K=300) - dyn = Langevin(silicon, - timestep=1.0 * fs, - temperature_K=300, - friction=0.01) - dyn.run(2) + silicon.calc = abacus + MaxwellBoltzmannDistribution(silicon, temperature_K=300) + positions = silicon.positions.copy() + frames = [] + dyn = Langevin(silicon, + timestep=1.0 * fs, + temperature_K=300, + friction=0.01) + dyn.attach(lambda: frames.append((silicon.positions.copy(), silicon.get_forces().copy()))) + dyn.run(2) + self.assertEqual(dyn.nsteps, 2) + self.assertGreaterEqual(len(frames), 3) + self.assertFalse(np.allclose(silicon.positions, positions)) + for frame_positions, frame_forces in frames: + self.assertEqual(frame_forces.shape, (len(silicon), 3)) + self.assertTrue(np.all(np.isfinite(frame_positions))) + self.assertTrue(np.all(np.isfinite(frame_forces))) + assert_scf_facts(self, abacus, tmpdir, len(silicon), forces=True, stress=True) if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/interfaces/ASE_interface/tests/relax.py b/interfaces/ASE_interface/tests/relax.py index 1b3c2a98751..5f370240120 100644 --- a/interfaces/ASE_interface/tests/relax.py +++ b/interfaces/ASE_interface/tests/relax.py @@ -1,20 +1,22 @@ import unittest +import numpy as np from pathlib import Path here = Path(__file__).parent from ase.build import bulk from ase.optimize import BFGS from abacuslite.io.generalio import load_pseudo, load_orbital from abacuslite import AbacusProfile, Abacus -from common import preserved_tmpdir +from common import preserved_tmpdir, test_profile_paths, assert_scf_facts class TestIonicRelaxationWithStress(unittest.TestCase): def test(self): - pporb = here.parent.parent.parent / 'tests' / 'PP_ORB' + command, pporb = test_profile_paths(here) silicon = bulk('Si', 'diamond', a=5.43) + silicon.positions[1, 0] += 0.1 aprof = AbacusProfile( - command='mpirun -np 2 abacus', + command=command, pseudo_dir=pporb, orbital_dir=pporb, omp_num_threads=1, @@ -35,13 +37,21 @@ def test(self): } ) - silicon.calc = abacus - e = silicon.get_potential_energy() - opt = BFGS(silicon) - opt.run(fmax=0.05, steps=1) - self.assertLessEqual(silicon.get_potential_energy(), e) - self.assertIsNotNone(silicon.calc.results['forces']) - self.assertIsNotNone(silicon.calc.results['stress']) + silicon.calc = abacus + initial_positions = silicon.positions.copy() + e = silicon.get_potential_energy() + initial_forces = silicon.get_forces() + self.assertTrue(np.isfinite(e)) + self.assertEqual(initial_forces.shape, (len(silicon), 3)) + self.assertGreater(np.max(np.linalg.norm(initial_forces, axis=1)), 0.05) + opt = BFGS(silicon, logfile=None) + opt.run(fmax=0.05, steps=1) + final_energy = silicon.get_potential_energy() + self.assertTrue(np.isfinite(final_energy)) + self.assertLessEqual(final_energy, e + 1e-4) + self.assertEqual(opt.nsteps, 1) + self.assertFalse(np.allclose(silicon.positions, initial_positions)) + assert_scf_facts(self, abacus, tmpdir, len(silicon), forces=True, stress=True) if __name__ == '__main__': - unittest.main() \ No newline at end of file + unittest.main() diff --git a/interfaces/ASE_interface/tests/scf.py b/interfaces/ASE_interface/tests/scf.py index d9f3e52e150..279f8a583ce 100644 --- a/interfaces/ASE_interface/tests/scf.py +++ b/interfaces/ASE_interface/tests/scf.py @@ -1,19 +1,20 @@ import unittest +import numpy as np from pathlib import Path here = Path(__file__).parent from ase.build import bulk from abacuslite.io.generalio import load_pseudo, load_orbital from abacuslite import AbacusProfile, Abacus -from common import preserved_tmpdir +from common import preserved_tmpdir, test_profile_paths, assert_scf_facts class TestSCF(unittest.TestCase): def test(self): - pporb = here.parent.parent.parent / 'tests' / 'PP_ORB' + command, pporb = test_profile_paths(here) silicon = bulk('Si', 'diamond', a=5.43) aprof = AbacusProfile( - command='mpirun -np 2 abacus', + command=command, pseudo_dir=pporb, orbital_dir=pporb, omp_num_threads=1, @@ -31,11 +32,18 @@ def test(self): 'basis_type': 'lcao', 'gamma_only': True, 'scf_thr': 1e-3, # fast for test, wrong for production + 'cal_force': True, + 'cal_stress': True, } ) silicon.calc = abacus - print('Silicon :', silicon.get_potential_energy()) + energy = silicon.get_potential_energy() + print('Silicon :', energy) + self.assertTrue(np.isfinite(energy)) + assert_scf_facts(self, abacus, tmpdir, len(silicon), + forces=True, stress=True) + self.assertEqual(abacus.results['natoms'], len(silicon)) if __name__ == '__main__': unittest.main() diff --git a/interfaces/ASE_interface/tests/socket_handoff.py b/interfaces/ASE_interface/tests/socket_handoff.py new file mode 100644 index 00000000000..dc12a4566a5 --- /dev/null +++ b/interfaces/ASE_interface/tests/socket_handoff.py @@ -0,0 +1,76 @@ +"""Bounded real fixed-cell socket handoff against raw FileIO calculations.""" + +import os +import signal +import unittest +from pathlib import Path + +import numpy as np +from ase import Atoms +from abacuslite import Abacus, AbacusProfile, AbacusSocketIO + +from common import assert_scf_facts, preserved_tmpdir, test_profile_paths + + +class TestSocketHandoff(unittest.TestCase): + def test_two_positions(self): + command, assets = test_profile_paths(Path(__file__).parent) + profile = AbacusProfile(command=command, pseudo_dir=assets, + orbital_dir=assets, omp_num_threads=1) + inputs = dict( + pseudopotentials={'Si': 'Si_ONCV_PBE-1.0.upf'}, + basissets={'Si': 'Si_gga_8au_100Ry_2s2p1d.orb'}, + inp={'calculation': 'scf', 'basis_type': 'lcao', 'gamma_only': True, + 'scf_thr': 1e-6, 'scf_nmax': 40, 'cal_force': 1, 'cal_stress': 1}, + ) + atoms = Atoms('Si2', positions=[[0, 0, 0], [1.25, 1.25, 1.25]], + cell=[5.43, 5.43, 5.43], pbc=True) + with preserved_tmpdir('abacus_ase_socket_') as tmpdir: + previous_handler = signal.getsignal(signal.SIGALRM) + def timed_out(*_): + raise TimeoutError('socket test exceeded 360 s') + signal.signal(signal.SIGALRM, timed_out) + signal.alarm(360) + try: + self._run_handoff(profile, inputs, atoms, tmpdir) + finally: + signal.alarm(0) + signal.signal(signal.SIGALRM, previous_handler) + + def _run_handoff(self, profile, inputs, atoms, tmpdir): + root = Path(tmpdir) + socket = AbacusSocketIO(profile=profile, directory=root / 'socket', + unixsocket=f'abacus_ci_{os.getpid()}', + timeout=120, **inputs) + client = None + with socket: + atoms.calc = socket + for index, displacement in enumerate((0.0, 0.03)): + atoms.positions[1] = [1.25 + displacement] * 3 + socket.calculate(atoms, properties=['energy', 'forces', 'stress']) + client = socket.server.proc + energy = atoms.get_potential_energy() + forces = atoms.get_forces().copy() + stress = atoms.get_stress().copy() + self.assertTrue(np.isfinite(energy)) + self.assertEqual(forces.shape, (2, 3)) + self.assertEqual(stress.shape, (6,)) + self.assertTrue(np.all(np.isfinite(forces))) + self.assertTrue(np.all(np.isfinite(stress))) + + reference_dir = root / f'fileio_{index}' + reference = Abacus(profile=profile, directory=reference_dir, **inputs) + check_atoms = atoms.copy() + check_atoms.calc = reference + self.assertAlmostEqual(energy, check_atoms.get_potential_energy(), delta=1e-4) + self.assertTrue(np.allclose(forces, check_atoms.get_forces(), atol=1e-4)) + self.assertTrue(np.allclose(stress, check_atoms.get_stress(), atol=1e-4)) + assert_scf_facts(self, reference, reference_dir, 2, + forces=True, stress=True) + self.assertIsNone(socket.server) + self.assertIsNotNone(client) + self.assertEqual(client.poll(), 0) + + +if __name__ == '__main__': + unittest.main() diff --git a/interfaces/ASE_interface/tests/test_external_provenance.py b/interfaces/ASE_interface/tests/test_external_provenance.py new file mode 100644 index 00000000000..3373bc5c37c --- /dev/null +++ b/interfaces/ASE_interface/tests/test_external_provenance.py @@ -0,0 +1,42 @@ +import tempfile +import unittest +from pathlib import Path + +from verify_external import verify_locations + + +class ExternalProvenanceTest(unittest.TestCase): + def test_installed_wheel_location(self): + with tempfile.TemporaryDirectory() as root: + root = Path(root) + site = root / 'venv' / 'site-packages' / 'abacuslite' / '__init__.py' + source = root / 'checkout' / 'interfaces' / 'ASE_interface' + site.parent.mkdir(parents=True) + source.mkdir(parents=True) + site.touch() + verify_locations(site, site, source) + + def test_source_shadow_is_rejected(self): + with tempfile.TemporaryDirectory() as root: + root = Path(root) + source = root / 'checkout' / 'interfaces' / 'ASE_interface' + shadow = source / 'abacuslite' / '__init__.py' + installed = root / 'venv' / 'site-packages' / 'abacuslite' / '__init__.py' + shadow.parent.mkdir(parents=True) + installed.parent.mkdir(parents=True) + shadow.touch() + installed.touch() + with self.assertRaisesRegex(AssertionError, 'source shadow'): + verify_locations(shadow, installed, source) + + def test_wrong_distribution_is_rejected(self): + with tempfile.TemporaryDirectory() as root: + root = Path(root) + imported = root / 'one' / 'abacuslite' / '__init__.py' + installed = root / 'two' / 'abacuslite' / '__init__.py' + imported.parent.mkdir(parents=True) + installed.parent.mkdir(parents=True) + imported.touch() + installed.touch() + with self.assertRaisesRegex(AssertionError, 'installed distribution'): + verify_locations(imported, installed, root / 'checkout') diff --git a/interfaces/ASE_interface/tests/verify_external.py b/interfaces/ASE_interface/tests/verify_external.py new file mode 100644 index 00000000000..abd128ef4e4 --- /dev/null +++ b/interfaces/ASE_interface/tests/verify_external.py @@ -0,0 +1,40 @@ +"""Prove the candidate lane imports the installed wheel, not a checkout.""" + +import argparse +from importlib import metadata +from pathlib import Path + + +def verify_locations(imported, installed, bundled): + imported = Path(imported).resolve() + installed = Path(installed).resolve() + bundled = Path(bundled).resolve() + if imported.is_relative_to(bundled): + raise AssertionError(f'source shadow: {imported} is under {bundled}') + if imported != installed: + raise AssertionError(f'import is not from installed distribution: {imported} != {installed}') + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--bundled-root', type=Path, required=True) + parser.add_argument('--candidate-root', type=Path, required=True) + args = parser.parse_args() + + import abacuslite + + distribution = metadata.distribution('abacuslite') + installed = distribution.locate_file('abacuslite/__init__.py') + imported = Path(abacuslite.__file__) + verify_locations(imported, installed, args.bundled_root) + if imported.resolve().is_relative_to(args.candidate_root.resolve()): + raise AssertionError(f'candidate source shadow: {imported}') + if not installed.is_file(): + raise AssertionError(f'installed distribution lacks abacuslite package: {installed}') + print(f'abacuslite distribution: {distribution.metadata["Name"]} {distribution.version}') + print(f'abacuslite import: {imported.resolve()}') + print(f'abacuslite distribution path: {installed.resolve()}') + + +if __name__ == '__main__': + main() diff --git a/interfaces/ASE_interface/tests/xtest.sh b/interfaces/ASE_interface/tests/xtest.sh index 645c729e7f9..4bd274eb850 100644 --- a/interfaces/ASE_interface/tests/xtest.sh +++ b/interfaces/ASE_interface/tests/xtest.sh @@ -7,3 +7,6 @@ python3 ./relax.py -v python3 ./md.py -v python3 ./band.py -v python3 ./magnetic.py -v +if [ "${ASE_ABACUS_SOCKET:-0}" = 1 ]; then + python3 ./socket_handoff.py -v +fi From da5e2c59c53372949b5659d7b924b0410737537e Mon Sep 17 00:00:00 2001 From: QuantumMisaka Date: Sat, 17 Oct 2026 00:34:12 +0800 Subject: [PATCH 02/12] test: verify raw band and atom facts with bounded socket cleanup --- .github/workflows/ase_plugin_test.yml | 2 +- interfaces/ASE_interface/tests/band.py | 5 +- interfaces/ASE_interface/tests/common.py | 88 +++++++++++++++++++ interfaces/ASE_interface/tests/magnetic.py | 6 +- .../ASE_interface/tests/socket_handoff.py | 9 +- .../ASE_interface/tests/test_harness_facts.py | 82 +++++++++++++++++ 6 files changed, 186 insertions(+), 6 deletions(-) create mode 100644 interfaces/ASE_interface/tests/test_harness_facts.py diff --git a/.github/workflows/ase_plugin_test.yml b/.github/workflows/ase_plugin_test.yml index ccbb1b63141..4b29dd34a3d 100644 --- a/.github/workflows/ase_plugin_test.yml +++ b/.github/workflows/ase_plugin_test.yml @@ -111,7 +111,7 @@ jobs: shell: bash -l {0} run: | cd interfaces/ASE_interface/tests - python -m unittest test_external_provenance -v + python -m unittest test_external_provenance test_harness_facts -v - name: Run engine-owned integration tests shell: bash -l {0} diff --git a/interfaces/ASE_interface/tests/band.py b/interfaces/ASE_interface/tests/band.py index 97cbfe6dbcf..95caff4808f 100644 --- a/interfaces/ASE_interface/tests/band.py +++ b/interfaces/ASE_interface/tests/band.py @@ -6,7 +6,8 @@ from abacuslite.io.generalio import load_pseudo, load_orbital from abacuslite import AbacusProfile, Abacus from abacuslite.utils.ksampling import kpathgen -from common import preserved_tmpdir, test_profile_paths, assert_scf_facts +from common import (preserved_tmpdir, test_profile_paths, assert_scf_facts, + assert_band_facts) class TestSCFFollowedNSCF(unittest.TestCase): @@ -62,7 +63,7 @@ def test(self): self.assertEqual(values.shape[1], len(bandpath.kpts)) self.assertGreater(values.shape[2], 0) self.assertTrue(np.all(np.isfinite(values))) - self.assertTrue(np.allclose(values[0], bscalc.results['eigenvalues'][0], atol=1e-5)) + assert_band_facts(self, bands, Path(tmpdir) / 'OUT.ABACUS' / 'eig_occ.txt') bands.write(Path(tmpdir) / 'bandstructure.json') self.assertTrue((Path(tmpdir) / 'bandstructure.json').exists()) diff --git a/interfaces/ASE_interface/tests/common.py b/interfaces/ASE_interface/tests/common.py index f37e48e7f52..2f1f5b54478 100644 --- a/interfaces/ASE_interface/tests/common.py +++ b/interfaces/ASE_interface/tests/common.py @@ -4,6 +4,7 @@ import os import re import shutil +import subprocess import tempfile from pathlib import Path @@ -20,6 +21,93 @@ def test_profile_paths(here): return os.environ.get('ABACUS_COMMAND', 'mpirun -np 2 abacus'), assets +def assert_band_facts(testcase, bands, eig_occ_path): + """Check ASE bands against the engine's raw eig_occ rows in eV.""" + values = np.asarray(bands.energies) + testcase.assertEqual(values.ndim, 3) + nspin, nk, nbands = values.shape + testcase.assertGreater(nbands, 0) + frames = [] + frame = {} + key = None + for line in Path(eig_occ_path).read_text().splitlines(): + if line.strip().endswith('# ionic step'): + if frame: + frames.append(frame) + frame = {} + key = None + continue + header = re.match(r'\s*spin=(\d+)\s+k-point=(\d+)/(\d+)\b', line) + if header: + spin, point, total = map(int, header.groups()) + testcase.assertEqual(total, nk) + key = (spin, point) + testcase.assertNotIn(key, frame) + frame[key] = [] + continue + row = re.fullmatch(r'\s*(\d+)\s+([-+\d.Ee]+)\s+([-+\d.Ee]+)\s*', line) + if row and key is not None: + testcase.assertEqual(int(row.group(1)), len(frame[key]) + 1) + frame[key].append(float(row.group(2))) + if frame: + frames.append(frame) + testcase.assertTrue(frames, f'no raw bands in {eig_occ_path}') + raw = frames[-1] + testcase.assertEqual(set(raw), {(spin, point) + for spin in range(1, nspin + 1) + for point in range(1, nk + 1)}) + for spin in range(nspin): + for point in range(nk): + row = raw[(spin + 1, point + 1)] + testcase.assertEqual(len(row), nbands) + testcase.assertTrue(np.allclose(values[spin, point], row, + rtol=0, atol=1e-5)) + + +def assert_collinear_magmoms(testcase, calc, log_path, natoms): + """Compare moments in atom order with the last raw ABACUS table (uB).""" + lines = Path(log_path).read_text().splitlines() + starts = [index for index, line in enumerate(lines) + if re.fullmatch(r'\s*Total Magnetism \(uB\)\s*', line, re.I)] + testcase.assertTrue(starts, f'no per-atom magnetic table in {log_path}') + rows = [] + for line in lines[starts[-1] + 1:]: + match = re.fullmatch(r'\s*([A-Z][a-z]?\d+)\s+([-+\d.Ee]+)\s*', line) + if match: + rows.append((match.group(1), float(match.group(2)))) + elif rows: + break + testcase.assertEqual(len(rows), natoms) + testcase.assertEqual(len({label for label, _ in rows}), natoms) + moments = np.asarray(calc.results['magmoms']) + testcase.assertEqual(moments.shape, (natoms,)) + testcase.assertTrue(np.allclose(moments, [value for _, value in rows], + rtol=0, atol=1e-4)) + + +def close_socket_bounded(calc, *, wait_seconds=5, terminate_seconds=5): + """Close ASE's server without its unbounded wait, then reap only its client.""" + server = getattr(calc, 'server', None) + client = getattr(server, 'proc', None) + if server is not None: + server.proc = None # ASE SocketServer.close otherwise calls wait() forever. + try: + calc.close() + finally: + if client is not None: + try: + returncode = client.wait(timeout=wait_seconds) + except subprocess.TimeoutExpired: + client.terminate() + try: + client.wait(timeout=terminate_seconds) + except subprocess.TimeoutExpired: + client.kill() + client.wait(timeout=terminate_seconds) + raise TimeoutError('socket client did not exit after server close') + return returncode if client is not None else None + + def assert_scf_facts(testcase, calc, directory, natoms, *, forces=False, stress=False): """Compare calculator facts with ABACUS' independently written SCF log.""" log = Path(directory) / 'OUT.ABACUS' / 'running_scf.log' diff --git a/interfaces/ASE_interface/tests/magnetic.py b/interfaces/ASE_interface/tests/magnetic.py index 9ef3f1aba3f..f539e70601e 100644 --- a/interfaces/ASE_interface/tests/magnetic.py +++ b/interfaces/ASE_interface/tests/magnetic.py @@ -8,7 +8,8 @@ here = Path(__file__).parent from ase.atoms import Atoms from abacuslite import AbacusProfile, Abacus -from common import preserved_tmpdir, test_profile_paths, assert_scf_facts +from common import (preserved_tmpdir, test_profile_paths, assert_scf_facts, + assert_collinear_magmoms) class TestMagneticSCF(unittest.TestCase): @@ -47,6 +48,9 @@ def test_antiferromagnetic(self): print(f'Fe (AFM): {fe.get_potential_energy()} eV') assert_scf_facts(self, abacus, tmpdir, len(fe)) self.assertEqual(np.asarray(abacus.results['magmoms']).shape, (2,)) + assert_collinear_magmoms(self, abacus, + Path(tmpdir) / 'OUT.ABACUS' / 'running_scf.log', + len(fe)) for ife, m in enumerate(fe.calc.results['magmoms']): print(f'Fe{ife}: {m:10.6f} uB') self.assertGreater(abs(m), 1e-3) diff --git a/interfaces/ASE_interface/tests/socket_handoff.py b/interfaces/ASE_interface/tests/socket_handoff.py index dc12a4566a5..01a62be2145 100644 --- a/interfaces/ASE_interface/tests/socket_handoff.py +++ b/interfaces/ASE_interface/tests/socket_handoff.py @@ -9,7 +9,8 @@ from ase import Atoms from abacuslite import Abacus, AbacusProfile, AbacusSocketIO -from common import assert_scf_facts, preserved_tmpdir, test_profile_paths +from common import (assert_scf_facts, close_socket_bounded, + preserved_tmpdir, test_profile_paths) class TestSocketHandoff(unittest.TestCase): @@ -43,7 +44,7 @@ def _run_handoff(self, profile, inputs, atoms, tmpdir): unixsocket=f'abacus_ci_{os.getpid()}', timeout=120, **inputs) client = None - with socket: + try: atoms.calc = socket for index, displacement in enumerate((0.0, 0.03)): atoms.positions[1] = [1.25 + displacement] * 3 @@ -67,8 +68,12 @@ def _run_handoff(self, profile, inputs, atoms, tmpdir): self.assertTrue(np.allclose(stress, check_atoms.get_stress(), atol=1e-4)) assert_scf_facts(self, reference, reference_dir, 2, forces=True, stress=True) + finally: + signal.alarm(0) + returncode = close_socket_bounded(socket) self.assertIsNone(socket.server) self.assertIsNotNone(client) + self.assertEqual(returncode, 0) self.assertEqual(client.poll(), 0) diff --git a/interfaces/ASE_interface/tests/test_harness_facts.py b/interfaces/ASE_interface/tests/test_harness_facts.py new file mode 100644 index 00000000000..348fea0c040 --- /dev/null +++ b/interfaces/ASE_interface/tests/test_harness_facts.py @@ -0,0 +1,82 @@ +"""Offline checks for independent engine facts and bounded socket cleanup.""" + +import os +import subprocess +import sys +import tempfile +import time +import unittest +from pathlib import Path +from types import SimpleNamespace +from unittest.mock import patch + +import numpy as np +from ase.calculators.socketio import SocketIOCalculator + +from common import (assert_band_facts, assert_collinear_magmoms, + close_socket_bounded, preserved_tmpdir) + + +class HarnessFactsTest(unittest.TestCase): + def test_band_eigenvalues_reject_mutated_reader_result(self): + raw = '''1 # ionic step +spin=1 k-point=1/2 Cartesian=0.0 0.0 0.0 (1 plane wave) +1 -2.5000 1.0 +2 1.2500 0.0 +spin=1 k-point=2/2 Cartesian=0.5 0.0 0.0 (1 plane wave) +1 -2.2500 1.0 +2 1.5000 0.0 +''' + with tempfile.TemporaryDirectory() as root: + path = Path(root) / 'eig_occ.txt' + path.write_text(raw) + bands = SimpleNamespace(energies=np.array([[[-2.5, 1.25], [-2.25, 1.5]]])) + assert_band_facts(self, bands, path) + bands.energies[0, 1, 1] += 0.1 + with self.assertRaises(AssertionError): + assert_band_facts(self, bands, path) + + def test_per_atom_magnetic_moment_rejects_swap(self): + raw = '''Total Magnetism (uB) +-------------------- + Fe1 1.2715948597 + Fe2 -1.2715948597 +-------------------- +''' + with tempfile.TemporaryDirectory() as root: + path = Path(root) / 'running_scf.log' + path.write_text(raw) + calc = SimpleNamespace(results={'magmoms': np.array([1.2715948597, -1.2715948597])}) + assert_collinear_magmoms(self, calc, path, 2) + calc.results['magmoms'] = calc.results['magmoms'][::-1] + with self.assertRaises(AssertionError): + assert_collinear_magmoms(self, calc, path, 2) + + def test_hung_socket_client_is_stopped_and_artifacts_retained(self): + with tempfile.TemporaryDirectory() as root: + keep = Path(root) / 'kept' + client = subprocess.Popen([sys.executable, '-c', 'import time; time.sleep(60)']) + unrelated = subprocess.Popen([sys.executable, '-c', 'import time; time.sleep(60)']) + try: + start = time.monotonic() + with patch.dict(os.environ, ASE_ABACUS_KEEP_DIR=str(keep)): + with self.assertRaisesRegex(TimeoutError, 'socket client'): + with preserved_tmpdir('abacus_ase_socket_') as run: + Path(run, 'sentinel.txt').write_text('diagnostic') + calc = SocketIOCalculator( + unixsocket=f'abacus_harness_{os.getpid()}', + timeout=1, launch_client=lambda *args, **kwargs: None) + calc.server = calc.launch_server() + calc.server.proc = client + close_socket_bounded(calc, wait_seconds=0.1, + terminate_seconds=0.1) + self.assertLess(time.monotonic() - start, 3.0) + self.assertIsNotNone(client.poll()) + self.assertIsNone(unrelated.poll()) + self.assertEqual([p.read_text() for p in keep.glob('*/sentinel.txt')], + ['diagnostic']) + finally: + for proc in (client, unrelated): + if proc.poll() is None: + proc.terminate() + proc.wait(timeout=3) From 68d23825ab7efe7a24d152b2db443b28a5b0dde7 Mon Sep 17 00:00:00 2001 From: QuantumMisaka Date: Sat, 17 Oct 2026 00:41:09 +0800 Subject: [PATCH 03/12] test: pass actual ASE changes to socket frames --- interfaces/ASE_interface/tests/common.py | 7 +++++ .../ASE_interface/tests/socket_handoff.py | 4 +-- .../ASE_interface/tests/test_harness_facts.py | 31 ++++++++++++++++++- 3 files changed, 39 insertions(+), 3 deletions(-) diff --git a/interfaces/ASE_interface/tests/common.py b/interfaces/ASE_interface/tests/common.py index 2f1f5b54478..448431893cb 100644 --- a/interfaces/ASE_interface/tests/common.py +++ b/interfaces/ASE_interface/tests/common.py @@ -21,6 +21,13 @@ def test_profile_paths(here): return os.environ.get('ABACUS_COMMAND', 'mpirun -np 2 abacus'), assets +def calculate_socket_frame(calc, atoms): + """Request all three facts while passing ASE's actual structure changes.""" + changes = calc.check_state(atoms) + calc.calculate(atoms, properties=['energy', 'forces', 'stress'], + system_changes=changes) + + def assert_band_facts(testcase, bands, eig_occ_path): """Check ASE bands against the engine's raw eig_occ rows in eV.""" values = np.asarray(bands.energies) diff --git a/interfaces/ASE_interface/tests/socket_handoff.py b/interfaces/ASE_interface/tests/socket_handoff.py index 01a62be2145..5cb5e5cdd70 100644 --- a/interfaces/ASE_interface/tests/socket_handoff.py +++ b/interfaces/ASE_interface/tests/socket_handoff.py @@ -9,7 +9,7 @@ from ase import Atoms from abacuslite import Abacus, AbacusProfile, AbacusSocketIO -from common import (assert_scf_facts, close_socket_bounded, +from common import (assert_scf_facts, calculate_socket_frame, close_socket_bounded, preserved_tmpdir, test_profile_paths) @@ -48,7 +48,7 @@ def _run_handoff(self, profile, inputs, atoms, tmpdir): atoms.calc = socket for index, displacement in enumerate((0.0, 0.03)): atoms.positions[1] = [1.25 + displacement] * 3 - socket.calculate(atoms, properties=['energy', 'forces', 'stress']) + calculate_socket_frame(socket, atoms) client = socket.server.proc energy = atoms.get_potential_energy() forces = atoms.get_forces().copy() diff --git a/interfaces/ASE_interface/tests/test_harness_facts.py b/interfaces/ASE_interface/tests/test_harness_facts.py index 348fea0c040..a15d0154ac6 100644 --- a/interfaces/ASE_interface/tests/test_harness_facts.py +++ b/interfaces/ASE_interface/tests/test_harness_facts.py @@ -11,13 +11,42 @@ from unittest.mock import patch import numpy as np +from ase import Atoms +from ase.calculators.calculator import Calculator, PropertyNotImplementedError from ase.calculators.socketio import SocketIOCalculator from common import (assert_band_facts, assert_collinear_magmoms, - close_socket_bounded, preserved_tmpdir) + calculate_socket_frame, close_socket_bounded, + preserved_tmpdir) class HarnessFactsTest(unittest.TestCase): + def test_socket_requests_only_actual_changes_after_first_frame(self): + class RecordingCalculator(Calculator): + implemented_properties = ['energy', 'forces', 'stress'] + + def __init__(self): + super().__init__() + self.changes = [] + + def calculate(self, atoms=None, properties=None, system_changes=None): + if self.atoms is not None and any( + name not in ('positions', 'cell') for name in system_changes): + raise PropertyNotImplementedError('immutable socket input changed') + super().calculate(atoms, properties, system_changes) + self.changes.append(list(system_changes)) + self.results = {'energy': 0.0, 'forces': np.zeros((2, 3)), + 'stress': np.zeros(6)} + + atoms = Atoms('Si2', positions=[[0, 0, 0], [1, 1, 1]], + cell=[5, 5, 5], pbc=True) + calc = RecordingCalculator() + calculate_socket_frame(calc, atoms) + atoms.positions[1, 0] += 0.03 + calculate_socket_frame(calc, atoms) + self.assertIn('numbers', calc.changes[0]) + self.assertEqual(calc.changes[1], ['positions']) + def test_band_eigenvalues_reject_mutated_reader_result(self): raw = '''1 # ionic step spin=1 k-point=1/2 Cartesian=0.0 0.0 0.0 (1 plane wave) From bca816410dcd8e380f2d84306c3e317e9d3b0716 Mon Sep 17 00:00:00 2001 From: QuantumMisaka Date: Sat, 17 Oct 2026 01:04:15 +0800 Subject: [PATCH 04/12] Validate native MD frames in both ASE plugin lanes --- .github/workflows/ase_plugin_test.yml | 6 +-- docs/advanced/interface/ase.md | 6 ++- interfaces/ASE_interface/README.md | 7 ++- .../ASE_interface/abacuslite/io/latestio.py | 13 +++-- interfaces/ASE_interface/tests/common.py | 33 +++++++++++++ interfaces/ASE_interface/tests/native_md.py | 48 +++++++++++++++++++ .../tests/test_native_md_fixture.py | 46 ++++++++++++++++++ interfaces/ASE_interface/tests/xtest.sh | 1 + 8 files changed, 149 insertions(+), 11 deletions(-) create mode 100644 interfaces/ASE_interface/tests/native_md.py create mode 100644 interfaces/ASE_interface/tests/test_native_md_fixture.py diff --git a/.github/workflows/ase_plugin_test.yml b/.github/workflows/ase_plugin_test.yml index 4b29dd34a3d..5c9afb7d4b5 100644 --- a/.github/workflows/ase_plugin_test.yml +++ b/.github/workflows/ase_plugin_test.yml @@ -7,7 +7,7 @@ on: plugin_ref: description: Reviewed QuantumMisaka/abacuslite commit or ref required: false - default: 136783baf4b6832c13d02a2542753439fbacc6df + default: dc020430bfa8c76edfc275b0f1eab2f12cc29d4d defaults: run: @@ -37,7 +37,7 @@ jobs: uses: actions/checkout@v7.0.1 with: repository: QuantumMisaka/abacuslite - ref: ${{ inputs.plugin_ref || '136783baf4b6832c13d02a2542753439fbacc6df' }} + ref: ${{ inputs.plugin_ref || 'dc020430bfa8c76edfc275b0f1eab2f12cc29d4d' }} path: candidate-plugin persist-credentials: false @@ -111,7 +111,7 @@ jobs: shell: bash -l {0} run: | cd interfaces/ASE_interface/tests - python -m unittest test_external_provenance test_harness_facts -v + python -m unittest test_external_provenance test_harness_facts test_native_md_fixture -v - name: Run engine-owned integration tests shell: bash -l {0} diff --git a/docs/advanced/interface/ase.md b/docs/advanced/interface/ase.md index 570906018f6..ba37c9ab06f 100644 --- a/docs/advanced/interface/ase.md +++ b/docs/advanced/interface/ase.md @@ -24,13 +24,15 @@ pip install . For the independent candidate package, use a separate environment and build a wheel from the reviewed `QuantumMisaka/abacuslite` commit -`136783baf4b6832c13d02a2542753439fbacc6df`. The ABACUS PR CI has +`dc020430bfa8c76edfc275b0f1eab2f12cc29d4d`. The ABACUS PR CI has parallel `bundled` and `external` jobs; the latter installs this wheel and runs the engine-owned tests from outside the bundled source tree. The local command and PP/ORB asset location are selected with `ABACUS_COMMAND` and `ASE_ABACUS_ASSETS`. Set `ASE_ABACUS_SOCKET=1` to include the bounded fixed-cell socket case. See [the ASE interface README](../../../interfaces/ASE_interface/README.md) -for the exact candidate installation and test commands. +for the exact candidate installation and test commands. Both lanes also run a +short ABACUS-native MD case and compare its periodic ASE trajectory with the +raw MD dump. ## ABACUS Calculator diff --git a/interfaces/ASE_interface/README.md b/interfaces/ASE_interface/README.md index 02a951e7ed6..f2d15effb75 100644 --- a/interfaces/ASE_interface/README.md +++ b/interfaces/ASE_interface/README.md @@ -28,7 +28,7 @@ remains available while the migration is reviewed: ```bash git clone https://github.com/QuantumMisaka/abacuslite.git /tmp/abacuslite-candidate -git -C /tmp/abacuslite-candidate checkout 136783baf4b6832c13d02a2542753439fbacc6df +git -C /tmp/abacuslite-candidate checkout dc020430bfa8c76edfc275b0f1eab2f12cc29d4d python -m pip install build python -m build --wheel --outdir /tmp/abacuslite-wheel /tmp/abacuslite-candidate python -m pip install /tmp/abacuslite-wheel/*.whl @@ -52,7 +52,10 @@ distribution metadata and the imported `abacuslite.__file__`, records the engine revision and `abacus --version`, and retains failed calculation directories. `workflow_dispatch` accepts a reviewed `plugin_ref` for coordinated runs. The scripts above require a built ABACUS executable and the curated PP/ORB -assets; ordinary Python checks do not launch the engine. +assets. Both lanes include a two-step ABACUS-native MD run that compares the +assembled periodic ASE frames with the raw `MD_dump` lattice, positions and +forces. The separate raw-output fixture regression is an offline Python check; +ordinary Python checks do not launch the engine. ## Usage Examples diff --git a/interfaces/ASE_interface/abacuslite/io/latestio.py b/interfaces/ASE_interface/abacuslite/io/latestio.py index 2b5e4ac39a9..294f93d619b 100644 --- a/interfaces/ASE_interface/abacuslite/io/latestio.py +++ b/interfaces/ASE_interface/abacuslite/io/latestio.py @@ -437,9 +437,9 @@ def read_abacus_out(fileobj, # read the structure, with the cell, elem, etc. (nframe) # if it is MD run, the trajectories will be in the file MD_dump, instead # of the running log - trajectory = read_traj_from_running_log(abacus_lines) \ - if fileobj.name != 'running_md.log' else \ - read_traj_from_md_dump(fileobj.parent / 'MD_dump') + is_md_dump = fileobj.name == 'running_md.log' + trajectory = read_traj_from_md_dump(fileobj.parent / 'MD_dump') \ + if is_md_dump else read_traj_from_running_log(abacus_lines) # read the eigenvalues (nframe, nk, nbnd) elecstate = read_band_from_eig_occ(fileobj.parent / 'eig_occ.txt') @@ -492,9 +492,14 @@ def read_abacus_out(fileobj, trajectory, elecstate, magmom, forces, stress, energies): # for each frame, a structure can be defined ind = ind or list(range(len(frame['elem']))) + # MD_dump lattice vectors are multiples of alat, while its Cartesian + # positions and the running-log structures are already in Angstrom. + cell = frame['cell'] * frame['alat_in_angstrom'] \ + if is_md_dump else frame['cell'] atoms = Atoms(symbols=np.array(frame['elem'])[ind].tolist(), positions=frame['coords'][ind], - cell=frame['cell'], + cell=cell, + pbc=is_md_dump, magmoms=mag[ind]) # from result, a calculator can be assembled # however, sometimes the force and stress is not calculated diff --git a/interfaces/ASE_interface/tests/common.py b/interfaces/ASE_interface/tests/common.py index 448431893cb..ae19231563b 100644 --- a/interfaces/ASE_interface/tests/common.py +++ b/interfaces/ASE_interface/tests/common.py @@ -92,6 +92,39 @@ def assert_collinear_magmoms(testcase, calc, log_path, natoms): rtol=0, atol=1e-4)) +def assert_native_md_facts(testcase, frames, dump_path, *, expected_frames): + """Compare ASE frames with the raw MD_dump lattice and Cartesian rows.""" + sections = re.split(r'(?=^MDSTEP:\s*\d+\s*$)', + Path(dump_path).read_text(), flags=re.M) + sections = [section for section in sections if section.startswith('MDSTEP:')] + testcase.assertEqual(len(sections), expected_frames) + testcase.assertEqual(len(frames), expected_frames) + for atoms, section in zip(frames, sections): + lines = section.splitlines() + alat = re.search(r'^LATTICE_CONSTANT:\s*([-+\d.Ee]+) Angstrom$', + section, re.M) + testcase.assertIsNotNone(alat) + lattice = lines.index('LATTICE_VECTORS') + raw_cell = np.array([[float(value) for value in row.split()] + for row in lines[lattice + 1:lattice + 4]]) + rows = [row.split() for row in lines if re.match(r'^\s*\d+\s+[A-Z][a-z]?\s+', row)] + testcase.assertEqual(len(rows), len(atoms)) + testcase.assertEqual(atoms.get_chemical_symbols(), [row[1] for row in rows]) + testcase.assertTrue(np.allclose(atoms.cell.array, raw_cell * float(alat.group(1)), + rtol=0, atol=1e-8)) + testcase.assertTrue(np.all(atoms.pbc)) + testcase.assertTrue(np.allclose(atoms.positions, + [[float(value) for value in row[2:5]] for row in rows], + rtol=0, atol=1e-8)) + testcase.assertTrue(np.isfinite(atoms.get_potential_energy())) + forces = atoms.get_forces() + testcase.assertEqual(forces.shape, (len(atoms), 3)) + testcase.assertTrue(np.all(np.isfinite(forces))) + testcase.assertTrue(np.allclose(forces, + [[float(value) for value in row[5:8]] for row in rows], + rtol=0, atol=1e-4)) + + def close_socket_bounded(calc, *, wait_seconds=5, terminate_seconds=5): """Close ASE's server without its unbounded wait, then reap only its client.""" server = getattr(calc, 'server', None) diff --git a/interfaces/ASE_interface/tests/native_md.py b/interfaces/ASE_interface/tests/native_md.py new file mode 100644 index 00000000000..20f4a3633dc --- /dev/null +++ b/interfaces/ASE_interface/tests/native_md.py @@ -0,0 +1,48 @@ +"""Run short ABACUS-native MD and check its ASE trajectory against MD_dump.""" + +import unittest +from pathlib import Path + +import numpy as np +from ase.build import bulk + +from abacuslite import Abacus, AbacusProfile +from abacuslite.io.generalio import load_orbital, load_pseudo +from abacuslite.io.latestio import read_abacus_out +from common import assert_native_md_facts, preserved_tmpdir, test_profile_paths + + +class TestNativeMD(unittest.TestCase): + def test_two_steps(self): + command, pporb = test_profile_paths(Path(__file__).parent) + silicon = bulk('Si', 'diamond', a=5.43) + profile = AbacusProfile(command=command, pseudo_dir=pporb, + orbital_dir=pporb, omp_num_threads=1) + with preserved_tmpdir('abacus_ase_native_md_') as run: + calculator = Abacus( + profile=profile, + directory=run, + pseudopotentials=load_pseudo(pporb), + basissets=load_orbital(pporb, efficiency=True), + inp={ + 'calculation': 'md', + 'basis_type': 'lcao', + 'gamma_only': True, + 'scf_thr': 1e-3, + 'cal_force': True, + 'md_nstep': 2, + 'md_tchain': 4, + 'md_tfirst': 10, + 'md_tfreq': 0.1, + 'init_vel': 1, + }, + ) + silicon.calc = calculator + self.assertTrue(np.isfinite(silicon.get_potential_energy())) + output = Path(run) / 'OUT.ABACUS' + frames = read_abacus_out(output / 'running_md.log') + assert_native_md_facts(self, frames, output / 'MD_dump', expected_frames=2) + + +if __name__ == '__main__': + unittest.main() diff --git a/interfaces/ASE_interface/tests/test_native_md_fixture.py b/interfaces/ASE_interface/tests/test_native_md_fixture.py new file mode 100644 index 00000000000..d665548d9ea --- /dev/null +++ b/interfaces/ASE_interface/tests/test_native_md_fixture.py @@ -0,0 +1,46 @@ +"""Offline regression for the real, two-frame latest-format ABACUS MD output.""" + +import unittest +import shutil +import tempfile +from pathlib import Path + +from abacuslite.io.latestio import read_abacus_out +from common import assert_native_md_facts + + +class NativeMDOutputTest(unittest.TestCase): + def setUp(self): + # Reuse ABACUS' existing two-frame raw output fixture as data only. + source = Path(__file__).resolve().parents[1] / 'abacuslite' / 'io' / 'testfiles' + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.fixture = Path(temporary.name) + for name, target in ( + ('pw-symm0-nspin4-gamma-md', 'running_md.log'), + ('nspin4-gamma-eigocc', 'eig_occ.txt'), + ('nspin4-gamma-mddump', 'MD_dump'), + ): + shutil.copyfile(source / name, self.fixture / target) + + def test_real_dump_cell_and_positions(self): + frames = read_abacus_out(self.fixture / 'running_md.log') + assert_native_md_facts(self, frames, self.fixture / 'MD_dump', expected_frames=2) + + def test_raw_dump_oracle_rejects_mutated_cell_pbc_and_position(self): + for mutation in ('cell', 'pbc', 'position'): + with self.subTest(mutation=mutation): + frames = read_abacus_out(self.fixture / 'running_md.log') + if mutation == 'cell': + frames[0].cell[0, 0] += 0.1 + elif mutation == 'pbc': + frames[0].pbc[0] = False + else: + frames[0].positions[0, 0] += 0.1 + with self.assertRaises(AssertionError): + assert_native_md_facts(self, frames, self.fixture / 'MD_dump', + expected_frames=2) + + +if __name__ == '__main__': + unittest.main() diff --git a/interfaces/ASE_interface/tests/xtest.sh b/interfaces/ASE_interface/tests/xtest.sh index 4bd274eb850..c2a8ef17195 100644 --- a/interfaces/ASE_interface/tests/xtest.sh +++ b/interfaces/ASE_interface/tests/xtest.sh @@ -5,6 +5,7 @@ set -e # Exit on error python3 ./scf.py -v python3 ./relax.py -v python3 ./md.py -v +python3 ./native_md.py -v python3 ./band.py -v python3 ./magnetic.py -v if [ "${ASE_ABACUS_SOCKET:-0}" = 1 ]; then From 4179b246731631767ca52ba3299a013ff26d9703 Mon Sep 17 00:00:00 2001 From: QuantumMisaka Date: Sun, 18 Oct 2026 02:13:00 +0800 Subject: [PATCH 05/12] Preserve ASE velocities in bundled STRU output --- .../ASE_interface/abacuslite/io/generalio.py | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/interfaces/ASE_interface/abacuslite/io/generalio.py b/interfaces/ASE_interface/abacuslite/io/generalio.py index c690964a5bb..d27dccc879a 100644 --- a/interfaces/ASE_interface/abacuslite/io/generalio.py +++ b/interfaces/ASE_interface/abacuslite/io/generalio.py @@ -234,7 +234,8 @@ def write_stru(stru: Atoms, ''' from ase.units import ( Bohr as __BOHR__, - Angstrom as __ANGSTROM__ + Angstrom as __ANGSTROM__, + AUT as __AUT__, ) assert isinstance(stru, Atoms) pp_file = pp_file or {} # can be None, for those non-ESolver_KS cases @@ -246,6 +247,10 @@ def write_stru(stru: Atoms, ind = species_group_indices(elem) coords = stru.get_positions()[ind] mobility = _constraint_mobility(stru)[ind] + # ASE Angstrom / ASE-time to ABACUS Bohr / atomic-time. Keep the same + # species ordering used for coordinates, mobility and magnetic moments. + velocities = (stru.get_velocities()[ind] * __AUT__ / __BOHR__ + if stru.has('momenta') else np.zeros((len(stru), 3))) elem = [elem[i] for i in ind] # handle the atomic magnetic moment (issue #6516) @@ -273,7 +278,7 @@ def write_stru(stru: Atoms, magmoms[j] | { 'coord': coords[j].tolist(), # coordinate 'm': mobility[j].tolist(), # mobility - 'v': [0.0, 0.0, 0.0], # velocity + 'v': velocities[j].tolist(), # atomic-unit velocity } for j in range(np.sum(nat[:i]), np.sum(nat[:i+1])) ] } @@ -781,6 +786,25 @@ def test_write_stru_preserves_first_occurrence_species_order(self): self.assertEqual([s['symbol'] for s in stru['species']], ['C', 'Pt', 'H']) self.assertEqual([s['natom'] for s in stru['species']], [2, 1, 2]) + def test_stru_velocities_keep_physical_units_and_species_order(self): + from ase.units import fs + + atoms = Atoms('HOH', positions=[[0, 0, 0], [1, 0, 0], [2, 0, 0]], + cell=[8, 8, 8], pbc=True) + atoms.set_velocities(np.array([[1, 0, 0], [0, 2, 0], [0, 0, -0.5]]) / fs) + # ABACUS writes MD_dump Angstrom/fs from STRU atomic-unit velocity. + au_per_angstrom_fs = 1.8897270 * 2.418884326505e-2 + with tempfile.TemporaryDirectory() as tmpdir: + path = write_stru(atoms, tmpdir, {'H': 'H.upf', 'O': 'O.upf'}) + species = read_stru(path)['species'] + self.assertEqual([entry['symbol'] for entry in species], ['H', 'O']) + self.assertEqual([entry['natom'] for entry in species], [2, 1]) + np.testing.assert_allclose([entry['v'] for entry in species[0]['atom']], + np.array([[1, 0, 0], [0, 0, -0.5]]) * au_per_angstrom_fs, + rtol=0, atol=1e-7) + np.testing.assert_allclose(species[1]['atom'][0]['v'], + [0, 2 * au_per_angstrom_fs, 0], rtol=0, atol=1e-7) + def test_write_stru_uses_ase_constraints_for_mobility(self): atoms = Atoms( symbols=['C', 'C', 'Pt', 'H'], From afe685495db3208b1313a56fc1dcfc1176d0586c Mon Sep 17 00:00:00 2001 From: QuantumMisaka Date: Sun, 18 Oct 2026 02:16:25 +0800 Subject: [PATCH 06/12] Add portable dual-format ASE engine acceptance harness --- .github/workflows/ase_plugin_test.yml | 67 +++-- docs/advanced/interface/ase.md | 20 +- interfaces/ASE_interface/README.md | 55 ++-- interfaces/ASE_interface/tests/band.py | 6 +- interfaces/ASE_interface/tests/common.py | 57 +++- .../ASE_interface/tests/legacy_facts.py | 96 +++++++ interfaces/ASE_interface/tests/native_md.py | 17 +- .../tests/native_md_generated.py | 60 ++++ .../tests/run_engine_compatibility.py | 263 ++++++++++++++++++ .../ASE_interface/tests/test_harness_facts.py | 18 ++ .../tests/test_portable_runner.py | 72 +++++ 11 files changed, 662 insertions(+), 69 deletions(-) create mode 100644 interfaces/ASE_interface/tests/legacy_facts.py create mode 100644 interfaces/ASE_interface/tests/native_md_generated.py create mode 100644 interfaces/ASE_interface/tests/run_engine_compatibility.py create mode 100644 interfaces/ASE_interface/tests/test_portable_runner.py diff --git a/.github/workflows/ase_plugin_test.yml b/.github/workflows/ase_plugin_test.yml index 5c9afb7d4b5..e18e8b079b7 100644 --- a/.github/workflows/ase_plugin_test.yml +++ b/.github/workflows/ase_plugin_test.yml @@ -5,9 +5,9 @@ on: workflow_dispatch: inputs: plugin_ref: - description: Reviewed QuantumMisaka/abacuslite commit or ref + description: Optional reviewed QuantumMisaka/abacuslite ref; empty uses published 1.1.0 required: false - default: dc020430bfa8c76edfc275b0f1eab2f12cc29d4d + default: '' defaults: run: @@ -27,17 +27,18 @@ jobs: env: ABACUS_COMMAND: mpirun -np 2 abacus ASE_ABACUS_ASSETS: ${{ github.workspace }}/tests/PP_ORB + ASE_ABACUS_SOCKET: '1' steps: - name: Checkout engine PR uses: actions/checkout@v7.0.1 - name: Checkout reviewed candidate - if: matrix.package == 'external' + if: matrix.package == 'external' && inputs.plugin_ref != '' uses: actions/checkout@v7.0.1 with: repository: QuantumMisaka/abacuslite - ref: ${{ inputs.plugin_ref || 'dc020430bfa8c76edfc275b0f1eab2f12cc29d4d' }} + ref: ${{ inputs.plugin_ref }} path: candidate-plugin persist-credentials: false @@ -58,18 +59,35 @@ jobs: if: matrix.package == 'bundled' shell: bash -l {0} run: | - cd interfaces/ASE_interface - pip install . + mkdir -p "$RUNNER_TEMP/bundled-wheel" + python -m pip wheel --no-deps --wheel-dir "$RUNNER_TEMP/bundled-wheel" \ + interfaces/ASE_interface + sha256sum "$RUNNER_TEMP"/bundled-wheel/*.whl + python -m pip install "$RUNNER_TEMP"/bundled-wheel/*.whl - name: Build and install candidate wheel if: matrix.package == 'external' shell: bash -l {0} + env: + PLUGIN_REF: ${{ inputs.plugin_ref }} run: | - python -m pip install 'build>=1.2' - python -m build --wheel --outdir "$RUNNER_TEMP/candidate-wheel" candidate-plugin - sha256sum "$RUNNER_TEMP"/candidate-wheel/*.whl - python -m pip install "$RUNNER_TEMP"/candidate-wheel/*.whl - git -C candidate-plugin rev-parse HEAD + mkdir -p "$RUNNER_TEMP/candidate-wheel" + if [ -n "$PLUGIN_REF" ]; then + python -m pip install 'build>=1.2' + python -m build --wheel --outdir "$RUNNER_TEMP/candidate-wheel" candidate-plugin + git -C candidate-plugin rev-parse HEAD + else + python -m pip download --only-binary=:all: --no-deps \ + --dest "$RUNNER_TEMP/candidate-wheel" 'abacuslite==1.1.0' + fi + wheels=("$RUNNER_TEMP"/candidate-wheel/*.whl) + test "${#wheels[@]}" -eq 1 + sha256sum "${wheels[0]}" + if [ -z "$PLUGIN_REF" ]; then + actual="$(sha256sum "${wheels[0]}")" + test "${actual%% *}" = c3e4504e9c9f5bceb69f70c79290e7843037ca72f625c5f66258e14d110f0ff3 + fi + python -m pip install "${wheels[0]}" python -m pip show abacuslite ase numpy seekpath - name: Install external tools from toolchain @@ -111,25 +129,34 @@ jobs: shell: bash -l {0} run: | cd interfaces/ASE_interface/tests - python -m unittest test_external_provenance test_harness_facts test_native_md_fixture -v + python -m unittest test_external_provenance test_harness_facts \ + test_native_md_fixture test_portable_runner -v - name: Run engine-owned integration tests shell: bash -l {0} run: | - mkdir -p "$RUNNER_TEMP/ase-engine-tests" - cp interfaces/ASE_interface/tests/*.py interfaces/ASE_interface/tests/xtest.sh "$RUNNER_TEMP/ase-engine-tests/" - cd "$RUNNER_TEMP/ase-engine-tests" + ENGINE_REF="$(git rev-parse HEAD)" if [ "${{ matrix.package }}" = external ]; then - python verify_external.py --bundled-root "$GITHUB_WORKSPACE/interfaces/ASE_interface" --candidate-root "$GITHUB_WORKSPACE/candidate-plugin" - export ASE_ABACUS_SOCKET=1 + WHEEL_DIR="$RUNNER_TEMP/candidate-wheel" + else + WHEEL_DIR="$RUNNER_TEMP/bundled-wheel" fi - timeout 30m bash ./xtest.sh + python interfaces/ASE_interface/tests/run_engine_compatibility.py \ + --engine /usr/local/bin/abacus \ + --mpi-command 'mpirun -np 2' \ + --assets "$GITHUB_WORKSPACE/tests/PP_ORB" \ + --format latest \ + --wheel "$WHEEL_DIR"/*.whl \ + --report-dir "$RUNNER_TEMP/ase-engine-tests" \ + --engine-ref "$ENGINE_REF" \ + --python "$(command -v python)" - name: Upload ASE failure artifacts - if: failure() + if: always() uses: actions/upload-artifact@v7 with: name: ase-failure-${{ matrix.package }}-${{ github.run_id }}-${{ github.run_attempt }} - path: /tmp/abacus_ase_failure/** + path: | + ${{ runner.temp }}/ase-engine-tests/** if-no-files-found: ignore retention-days: 14 diff --git a/docs/advanced/interface/ase.md b/docs/advanced/interface/ase.md index ba37c9ab06f..a10978b9aa3 100644 --- a/docs/advanced/interface/ase.md +++ b/docs/advanced/interface/ase.md @@ -22,17 +22,15 @@ cd interfaces/ASE_interface pip install . ``` -For the independent candidate package, use a separate environment and build a -wheel from the reviewed `QuantumMisaka/abacuslite` commit -`dc020430bfa8c76edfc275b0f1eab2f12cc29d4d`. The ABACUS PR CI has -parallel `bundled` and `external` jobs; the latter installs this wheel and runs -the engine-owned tests from outside the bundled source tree. The local command -and PP/ORB asset location are selected with `ABACUS_COMMAND` and -`ASE_ABACUS_ASSETS`. Set `ASE_ABACUS_SOCKET=1` to include the bounded fixed-cell -socket case. See [the ASE interface README](../../../interfaces/ASE_interface/README.md) -for the exact candidate installation and test commands. Both lanes also run a -short ABACUS-native MD case and compare its periodic ASE trajectory with the -raw MD dump. +For the independent package, install the published `abacuslite==1.1.0` wheel +in a separate environment. The ABACUS PR CI retains parallel `bundled` and +`external` jobs; the external lane verifies its installed modules against the +wheel and runs the engine-owned harness outside the bundled source tree. The +portable runner accepts an explicit executable, MPI launcher, curated PP/ORB +assets, output format, wheel and report directory. Its develop lane includes +the bounded fixed-cell socket case and native MD; LTS runs the supported cases +plus seeded native MD. See [the ASE interface README](../../../interfaces/ASE_interface/README.md) +for the exact command. Manual CI dispatch can select a reviewed source ref. ## ABACUS Calculator diff --git a/interfaces/ASE_interface/README.md b/interfaces/ASE_interface/README.md index f2d15effb75..d066747097e 100644 --- a/interfaces/ASE_interface/README.md +++ b/interfaces/ASE_interface/README.md @@ -21,41 +21,36 @@ cd interfaces/ASE_interface pip install . ``` -For coordinated testing of the independent candidate, build a wheel from the -reviewed [QuantumMisaka/abacuslite](https://github.com/QuantumMisaka/abacuslite) -commit and install it in a separate environment. The bundled installation above -remains available while the migration is reviewed: +The independent [abacuslite](https://pypi.org/project/abacuslite/1.1.0/) 1.1.0 +release can be installed in a separate environment. The engine compatibility +runner requires the same wheel file that was installed in that environment: ```bash -git clone https://github.com/QuantumMisaka/abacuslite.git /tmp/abacuslite-candidate -git -C /tmp/abacuslite-candidate checkout dc020430bfa8c76edfc275b0f1eab2f12cc29d4d -python -m pip install build -python -m build --wheel --outdir /tmp/abacuslite-wheel /tmp/abacuslite-candidate +python -m pip download --only-binary=:all: --no-deps \ + --dest /tmp/abacuslite-wheel 'abacuslite==1.1.0' python -m pip install /tmp/abacuslite-wheel/*.whl -mkdir -p /tmp/abacus-ase-engine-tests -cp /path/to/abacus-develop/interfaces/ASE_interface/tests/*.py \ - /path/to/abacus-develop/interfaces/ASE_interface/tests/xtest.sh \ - /tmp/abacus-ase-engine-tests/ -cd /tmp/abacus-ase-engine-tests -python verify_external.py \ - --bundled-root /path/to/abacus-develop/interfaces/ASE_interface \ - --candidate-root /tmp/abacuslite-candidate -ABACUS_COMMAND='mpirun -np 2 /path/to/abacus' \ -ASE_ABACUS_ASSETS=/path/to/abacus-develop/tests/PP_ORB \ -ASE_ABACUS_SOCKET=1 \ -bash ./xtest.sh +python /path/to/abacus-develop/interfaces/ASE_interface/tests/run_engine_compatibility.py \ + --engine /absolute/path/to/abacus --mpi-command 'mpirun -np 2' \ + --assets /path/to/abacus-develop/tests/PP_ORB --format latest \ + --wheel /tmp/abacuslite-wheel/abacuslite-1.1.0-py3-none-any.whl \ + --report-dir /tmp/abacus-ase-engine-tests --engine-ref FULL_ENGINE_COMMIT_SHA ``` -Run the scripts from outside `interfaces/ASE_interface` so that the bundled -source cannot shadow the installed wheel. In CI the `external` job checks -distribution metadata and the imported `abacuslite.__file__`, records the engine -revision and `abacus --version`, and retains failed calculation directories. -`workflow_dispatch` accepts a reviewed `plugin_ref` for coordinated runs. -The scripts above require a built ABACUS executable and the curated PP/ORB -assets. Both lanes include a two-step ABACUS-native MD run that compares the -assembled periodic ASE frames with the raw `MD_dump` lattice, positions and -forces. The separate raw-output fixture regression is an offline Python check; -ordinary Python checks do not launch the engine. +The runner copies the required scripts outside the checkout, verifies installed +runtime bytes against the supplied wheel, records the executable hash and +actual version/reader, and preserves every raw run and per-script log. It +rejects omitted or skipped required cases. The ordinary external CI lane pins +the published 1.1.0 wheel by SHA256 +`c3e4504e9c9f5bceb69f70c79290e7843037ca72f625c5f66258e14d110f0ff3`. +The bundled transition lane builds a wheel from this engine PR and runs the +same develop suite. Both develop lanes require all nine checks, including +fixed-cell socket and real native MD. Use `--format legacy` with an LTS +executable for eight interface checks plus the seeded initial-velocity case. +The LTS engine source has no bundled interface or socket driver; the shared +runner supplies its legacy-format oracle for plugin CI. +`workflow_dispatch` can select a reviewed plugin source ref instead of the +published release. Engine tests run only through this explicit runner or the +opt-in `xtest.sh`, not ordinary offline unit checks. ## Usage Examples diff --git a/interfaces/ASE_interface/tests/band.py b/interfaces/ASE_interface/tests/band.py index 95caff4808f..01a03a649a5 100644 --- a/interfaces/ASE_interface/tests/band.py +++ b/interfaces/ASE_interface/tests/band.py @@ -1,3 +1,4 @@ +import os import unittest import numpy as np from pathlib import Path @@ -63,7 +64,10 @@ def test(self): self.assertEqual(values.shape[1], len(bandpath.kpts)) self.assertGreater(values.shape[2], 0) self.assertTrue(np.all(np.isfinite(values))) - assert_band_facts(self, bands, Path(tmpdir) / 'OUT.ABACUS' / 'eig_occ.txt') + output = Path(tmpdir) / 'OUT.ABACUS' + raw_bands = output / ('running_nscf.log' if os.environ.get('ASE_ABACUS_FORMAT') == 'legacy' + else 'eig_occ.txt') + assert_band_facts(self, bands, raw_bands) bands.write(Path(tmpdir) / 'bandstructure.json') self.assertTrue((Path(tmpdir) / 'bandstructure.json').exists()) diff --git a/interfaces/ASE_interface/tests/common.py b/interfaces/ASE_interface/tests/common.py index ae19231563b..0706a64addd 100644 --- a/interfaces/ASE_interface/tests/common.py +++ b/interfaces/ASE_interface/tests/common.py @@ -9,7 +9,7 @@ from pathlib import Path import numpy as np -from ase.units import GPa +from ase.units import GPa, fs from ase.stress import full_3x3_to_voigt_6_stress @@ -30,6 +30,9 @@ def calculate_socket_frame(calc, atoms): def assert_band_facts(testcase, bands, eig_occ_path): """Check ASE bands against the engine's raw eig_occ rows in eV.""" + if os.environ.get('ASE_ABACUS_FORMAT', 'latest') == 'legacy': + from legacy_facts import assert_band_facts as assert_legacy_band_facts + return assert_legacy_band_facts(testcase, bands, eig_occ_path) values = np.asarray(bands.energies) testcase.assertEqual(values.ndim, 3) nspin, nk, nbands = values.shape @@ -94,12 +97,24 @@ def assert_collinear_magmoms(testcase, calc, log_path, natoms): def assert_native_md_facts(testcase, frames, dump_path, *, expected_frames): """Compare ASE frames with the raw MD_dump lattice and Cartesian rows.""" + raw_energies = {} + ion = None + for line in Path(dump_path).with_name('running_md.log').read_text().splitlines(): + header = (re.search(r'\bION=\s*(\d+)\b', line) or + re.search(r'#ION MOVE#\s*(\d+)\b', line)) + if header: + ion = int(header.group(1)) + energy = re.match(r'\s*E_KohnSham\s+[-+\d.Ee]+\s+([-+\d.Ee]+)', line) + if ion is not None and energy: + raw_energies[ion] = float(energy.group(1)) + testcase.assertEqual(set(raw_energies), set(range(1, expected_frames + 1))) sections = re.split(r'(?=^MDSTEP:\s*\d+\s*$)', Path(dump_path).read_text(), flags=re.M) sections = [section for section in sections if section.startswith('MDSTEP:')] testcase.assertEqual(len(sections), expected_frames) testcase.assertEqual(len(frames), expected_frames) - for atoms, section in zip(frames, sections): + raw_velocities = [] + for step, (atoms, section) in enumerate(zip(frames, sections), start=1): lines = section.splitlines() alat = re.search(r'^LATTICE_CONSTANT:\s*([-+\d.Ee]+) Angstrom$', section, re.M) @@ -108,6 +123,9 @@ def assert_native_md_facts(testcase, frames, dump_path, *, expected_frames): raw_cell = np.array([[float(value) for value in row.split()] for row in lines[lattice + 1:lattice + 4]]) rows = [row.split() for row in lines if re.match(r'^\s*\d+\s+[A-Z][a-z]?\s+', row)] + testcase.assertIn('VELOCITY (Angstrom/fs)', section) + raw_velocities.append(np.asarray([[float(value) for value in row[8:11]] + for row in rows])) testcase.assertEqual(len(rows), len(atoms)) testcase.assertEqual(atoms.get_chemical_symbols(), [row[1] for row in rows]) testcase.assertTrue(np.allclose(atoms.cell.array, raw_cell * float(alat.group(1)), @@ -117,12 +135,35 @@ def assert_native_md_facts(testcase, frames, dump_path, *, expected_frames): [[float(value) for value in row[2:5]] for row in rows], rtol=0, atol=1e-8)) testcase.assertTrue(np.isfinite(atoms.get_potential_energy())) + testcase.assertAlmostEqual(atoms.get_potential_energy(), raw_energies[step], + delta=1e-4) forces = atoms.get_forces() testcase.assertEqual(forces.shape, (len(atoms), 3)) testcase.assertTrue(np.all(np.isfinite(forces))) testcase.assertTrue(np.allclose(forces, [[float(value) for value in row[5:8]] for row in rows], rtol=0, atol=1e-4)) + testcase.assertTrue(np.all(np.isfinite(raw_velocities))) + testcase.assertGreater(np.max(np.abs(raw_velocities[0])), 1e-8, + 'MD starts with zero velocity') + cell = frames[0].cell.array + delta = frames[-1].positions - frames[0].positions + fractional = np.linalg.solve(cell.T, delta.T).T + fractional -= np.rint(fractional) + testcase.assertGreater(np.max(np.linalg.norm(fractional @ cell, axis=1)), 1e-6, + 'MD trajectory is static') + + +def assert_stru_velocity_facts(testcase, stru_path, ase_velocities): + """Check ASE Angstrom/ASE-time velocities converted to Bohr/atomic-time.""" + text = Path(stru_path).read_text() + tokens = re.findall(r'\bv\s+([-+\d.Ee]+)\s+([-+\d.Ee]+)\s+([-+\d.Ee]+)', text) + actual = np.asarray(tokens, dtype=float) + testcase.assertEqual(actual.shape, np.asarray(ase_velocities).shape) + expected = np.asarray(ase_velocities) * fs * 1.8897270 * 0.02418884326505 + testcase.assertGreater(np.max(np.abs(expected)), 1e-8) + testcase.assertTrue(np.allclose(actual, expected, rtol=2e-6, atol=1e-12), + f'STRU v unit mismatch: actual={actual}, expected={expected}') def close_socket_bounded(calc, *, wait_seconds=5, terminate_seconds=5): @@ -150,6 +191,10 @@ def close_socket_bounded(calc, *, wait_seconds=5, terminate_seconds=5): def assert_scf_facts(testcase, calc, directory, natoms, *, forces=False, stress=False): """Compare calculator facts with ABACUS' independently written SCF log.""" + if os.environ.get('ASE_ABACUS_FORMAT', 'latest') == 'legacy': + from legacy_facts import assert_scf_facts as assert_legacy_scf_facts + return assert_legacy_scf_facts(testcase, calc, directory, natoms, + forces=forces, stress=stress) log = Path(directory) / 'OUT.ABACUS' / 'running_scf.log' content = log.read_text() energies = re.findall(r'!FINAL_ETOT_IS\s+([-+\d.Ee]+)\s+eV', content) @@ -191,7 +236,10 @@ def preserved_tmpdir(prefix): /tmp/abacus_ase_failure) so remote-CI-only failures (e.g. issue #7794) can be root-caused from the uploaded artifact. """ - tmpdir = tempfile.mkdtemp(prefix=prefix) + raw_root = os.environ.get('ASE_ABACUS_RAW_DIR') + if raw_root: + Path(raw_root).mkdir(parents=True, exist_ok=True) + tmpdir = tempfile.mkdtemp(prefix=prefix, dir=raw_root) try: yield tmpdir except Exception: @@ -203,4 +251,5 @@ def preserved_tmpdir(prefix): print(f'[ase-test] failure, run dir preserved at {keep}') raise finally: - shutil.rmtree(tmpdir, ignore_errors=True) + if not raw_root: + shutil.rmtree(tmpdir, ignore_errors=True) diff --git a/interfaces/ASE_interface/tests/legacy_facts.py b/interfaces/ASE_interface/tests/legacy_facts.py new file mode 100644 index 00000000000..d15c3f65c64 --- /dev/null +++ b/interfaces/ASE_interface/tests/legacy_facts.py @@ -0,0 +1,96 @@ +"""Small raw-output oracles for ABACUS v3.10/LTS tables.""" + +import re +from pathlib import Path + +import numpy as np +from ase.units import GPa +from ase.stress import full_3x3_to_voigt_6_stress + + +def assert_band_facts(testcase, bands, running_nscf_path): + """Compare ASE bands with literal LTS running_nscf.log rows.""" + values = np.asarray(bands.energies) + testcase.assertEqual(values.ndim, 3) + nspin, nk, nbands = values.shape + testcase.assertGreater(nbands, 0) + testcase.assertEqual(nspin, 1, 'this silicon LTS band smoke is spin-unpolarized') + lines = Path(running_nscf_path).read_text().splitlines() + testcase.assertTrue(lines, running_nscf_path) + headings = [] + for i, line in enumerate(lines): + match = re.match(r'\s*(\d+)/(\d+)\s+kpoint\s+\(Cartesian\)\s*=.*\(\d+\s+pws\)', line) + if match: + headings.append((i, int(match.group(1)), int(match.group(2)))) + testcase.assertEqual(len(headings), nk) + raw = [] + for i, point, total in headings: + testcase.assertEqual(total, nk) + testcase.assertEqual(point, len(raw) + 1) + energies = [] + for line in lines[i + 1:]: + match = re.fullmatch(r'\s*(\d+)\s+([-+\d.Ee]+)\s+([-+\d.Ee]+)\s*', line) + if match: + testcase.assertEqual(int(match.group(1)), len(energies) + 1) + energies.append(float(match.group(2))) + if len(energies) == nbands: + break + elif energies: + break + testcase.assertEqual(len(energies), nbands) + raw.append(energies) + testcase.assertTrue(np.allclose(values[0], np.asarray(raw), rtol=0, atol=1e-5)) + + +def assert_scf_facts(testcase, calc, directory, natoms, *, forces=False, stress=False): + """Compare energy, optional EF, forces and stress with literal LTS log.""" + log = Path(directory) / 'OUT.ABACUS' / 'running_scf.log' + content = log.read_text() + energies = re.findall(r'!FINAL_ETOT_IS\s+([-+\d.Ee]+)\s+eV', content) + testcase.assertTrue(energies, f'no final energy in {log}') + energy = float(calc.results['energy']) + testcase.assertTrue(np.isfinite(energy)) + testcase.assertAlmostEqual(energy, float(energies[-1]), delta=1e-4) + fermi = re.findall(r'^\s*E_Fermi\s+[-+\d.Ee]+\s+([-+\d.Ee]+)', content, re.M) + if fermi: + testcase.assertIn('fermi_level', calc.results) + testcase.assertAlmostEqual(float(calc.results['fermi_level']), float(fermi[-1]), delta=1e-4) + lines = content.splitlines() + if forces: + values = np.asarray(calc.results['forces']) + testcase.assertEqual(values.shape, (natoms, 3)) + testcase.assertTrue(np.all(np.isfinite(values))) + starts = [i for i, line in enumerate(lines) if re.match( + r'^\s*TOTAL-FORCE\s*\(eV/Angstrom\)', line, re.I)] + testcase.assertTrue(starts, f'no LTS force table in {log}') + rows = [] + for line in lines[starts[-1] + 1:]: + match = re.fullmatch(r'\s*[A-Z][a-z]?\d+\s+([-+\d.Ee]+)\s+([-+\d.Ee]+)\s+([-+\d.Ee]+)\s*', line) + if match: + rows.append([float(value) for value in match.groups()]) + if len(rows) == natoms: + break + elif rows: + break + testcase.assertEqual(len(rows), natoms) + testcase.assertTrue(np.allclose(values, rows, rtol=0, atol=1e-4)) + if stress: + values = np.asarray(calc.results['stress']) + testcase.assertEqual(values.shape, (6,)) + testcase.assertTrue(np.all(np.isfinite(values))) + starts = [i for i, line in enumerate(lines) if re.match( + r'^\s*TOTAL-STRESS\s*\(KBAR\)', line, re.I)] + testcase.assertTrue(starts, f'no LTS stress table in {log}') + rows = [] + for line in lines[starts[-1] + 1:]: + match = re.fullmatch(r'\s*([-+\d.Ee]+)\s+([-+\d.Ee]+)\s+([-+\d.Ee]+)\s*', line) + if match: + rows.append([float(value) for value in match.groups()]) + if len(rows) == 3: + break + elif rows: + break + testcase.assertEqual(len(rows), 3) + tensor = -0.1 * np.asarray(rows, dtype=float) * GPa + testcase.assertTrue(np.allclose(values, full_3x3_to_voigt_6_stress(tensor), + rtol=0, atol=1e-4)) diff --git a/interfaces/ASE_interface/tests/native_md.py b/interfaces/ASE_interface/tests/native_md.py index 20f4a3633dc..d17ea111019 100644 --- a/interfaces/ASE_interface/tests/native_md.py +++ b/interfaces/ASE_interface/tests/native_md.py @@ -1,5 +1,6 @@ """Run short ABACUS-native MD and check its ASE trajectory against MD_dump.""" +import os import unittest from pathlib import Path @@ -8,14 +9,22 @@ from abacuslite import Abacus, AbacusProfile from abacuslite.io.generalio import load_orbital, load_pseudo -from abacuslite.io.latestio import read_abacus_out -from common import assert_native_md_facts, preserved_tmpdir, test_profile_paths +from common import (assert_native_md_facts, assert_stru_velocity_facts, + preserved_tmpdir, test_profile_paths) class TestNativeMD(unittest.TestCase): def test_two_steps(self): command, pporb = test_profile_paths(Path(__file__).parent) silicon = bulk('Si', 'diamond', a=5.43) + silicon.set_velocities(np.array([[0.01, 0.0, 0.0], + [-0.01, 0.0, 0.0]])) + initial_velocities = silicon.get_velocities().copy() + legacy = os.environ.get('ASE_ABACUS_FORMAT', 'latest') == 'legacy' + if legacy: + from abacuslite.io.legacyio import read_abacus_out + else: + from abacuslite.io.latestio import read_abacus_out profile = AbacusProfile(command=command, pseudo_dir=pporb, orbital_dir=pporb, omp_num_threads=1) with preserved_tmpdir('abacus_ase_native_md_') as run: @@ -39,9 +48,11 @@ def test_two_steps(self): ) silicon.calc = calculator self.assertTrue(np.isfinite(silicon.get_potential_energy())) + assert_stru_velocity_facts(self, Path(run) / 'STRU', initial_velocities) output = Path(run) / 'OUT.ABACUS' frames = read_abacus_out(output / 'running_md.log') - assert_native_md_facts(self, frames, output / 'MD_dump', expected_frames=2) + assert_native_md_facts(self, frames, output / 'MD_dump', + expected_frames=3 if legacy else 2) if __name__ == '__main__': diff --git a/interfaces/ASE_interface/tests/native_md_generated.py b/interfaces/ASE_interface/tests/native_md_generated.py new file mode 100644 index 00000000000..d9ee3d2f485 --- /dev/null +++ b/interfaces/ASE_interface/tests/native_md_generated.py @@ -0,0 +1,60 @@ +"""Run short ABACUS-native MD and check its ASE trajectory against MD_dump.""" + +import unittest +import re +from pathlib import Path + +import numpy as np +from ase.build import bulk + +from abacuslite import Abacus, AbacusProfile +from abacuslite.io.generalio import load_orbital, load_pseudo +from abacuslite.io.legacyio import read_abacus_out +from common import assert_native_md_facts, preserved_tmpdir, test_profile_paths + + +class TestNativeMD(unittest.TestCase): + def test_two_steps(self): + command, pporb = test_profile_paths(Path(__file__).parent) + silicon = bulk('Si', 'diamond', a=5.43) + # init_vel=0 must generate seeded engine velocities even though the + # ASE structure carries no initial momenta (STRU v remains zero). + self.assertFalse(silicon.has('momenta')) + profile = AbacusProfile(command=command, pseudo_dir=pporb, + orbital_dir=pporb, omp_num_threads=1) + with preserved_tmpdir('abacus_ase_native_md_') as run: + calculator = Abacus( + profile=profile, + directory=run, + pseudopotentials=load_pseudo(pporb), + basissets=load_orbital(pporb, efficiency=True), + inp={ + 'calculation': 'md', + 'basis_type': 'lcao', + 'gamma_only': True, + 'scf_thr': 1e-3, + 'cal_force': True, + 'md_nstep': 2, + 'md_tchain': 4, + 'md_tfirst': 10, + 'md_tfreq': 0.1, + 'init_vel': 0, + 'md_seed': 1729, + }, + ) + silicon.calc = calculator + self.assertTrue(np.isfinite(silicon.get_potential_energy())) + stru = (Path(run) / 'STRU').read_text() + raw_velocities = np.asarray(re.findall( + r'\bv\s+([-+\d.Ee]+)\s+([-+\d.Ee]+)\s+([-+\d.Ee]+)', stru), + dtype=float) + self.assertEqual(raw_velocities.shape, (len(silicon), 3)) + self.assertTrue(np.all(raw_velocities == 0)) + output = Path(run) / 'OUT.ABACUS' + frames = read_abacus_out(output / 'running_md.log') + # LTS run_md.cpp records MDSTEP 0, 1 and 2 for md_nstep=2. + assert_native_md_facts(self, frames, output / 'MD_dump', expected_frames=3) + + +if __name__ == '__main__': + unittest.main() diff --git a/interfaces/ASE_interface/tests/run_engine_compatibility.py b/interfaces/ASE_interface/tests/run_engine_compatibility.py new file mode 100644 index 00000000000..d45a6c08ad8 --- /dev/null +++ b/interfaces/ASE_interface/tests/run_engine_compatibility.py @@ -0,0 +1,263 @@ +#!/usr/bin/env python3 +"""Run the installed abacuslite wheel against one explicit ABACUS executable. + +The runner copies only this interface harness into a report directory outside +the ABACUS checkout. Each real-engine script then gets its own Python process. +""" + +import argparse +import hashlib +import json +import os +import re +import shlex +import shutil +import signal +import subprocess +import sys +import time +import zipfile +from email.parser import BytesParser +from pathlib import Path + + +HERE = Path(__file__).resolve().parent +REPO = HERE.parents[2] +CASES = { + 'latest': {'scf.py': 1, 'relax.py': 1, 'md.py': 1, 'native_md.py': 1, + 'band.py': 1, 'magnetic.py': 3, 'socket_handoff.py': 1}, + 'legacy': {'scf.py': 1, 'relax.py': 1, 'md.py': 1, 'native_md.py': 1, + 'band.py': 1, 'magnetic.py': 3, 'native_md_generated.py': 1}, +} +ASSETS = ('Si_ONCV_PBE-1.0.upf', 'Si_gga_8au_100Ry_2s2p1d.orb', + 'Fe_ONCV_PBE-1.0.upf', 'Fe.upf', 'Fe_gga_6au_100Ry_4s2p2d1f.orb') +PROBE = r'''import json, sys +from importlib import metadata +from pathlib import Path +import abacuslite +import abacuslite.core as core +from abacuslite import Abacus, AbacusProfile +d = metadata.distribution('abacuslite') +p = AbacusProfile(command=sys.argv[1]) +c = Abacus(profile=p) +print(json.dumps({'python': sys.executable, 'prefix': sys.prefix, + 'module': str(Path(abacuslite.__file__).resolve()), + 'installed': str(Path(d.locate_file('abacuslite/__init__.py')).resolve()), + 'version': d.version, 'engine_version': p.version(), + 'reader_selection': ('instance' if hasattr(c.template, '_legacyio') + else 'global'), + 'legacyio': getattr(c.template, '_legacyio', core.__LEGACYIO__)}))''' + + +def sha256(path): + digest = hashlib.sha256() + with Path(path).open('rb') as stream: + for block in iter(lambda: stream.read(1024 * 1024), b''): + digest.update(block) + return digest.hexdigest() + + +def validate_cases(fmt, results): + """Reject omitted, skipped or misreported required scripts.""" + expected = CASES[fmt] + if [item['script'] for item in results] != list(expected): + raise ValueError(f'incomplete {fmt} lane: expected {list(expected)}') + for item in results: + if (item['returncode'] != 0 or item['timed_out'] or + item['tests_reported'] != expected[item['script']] or + item['skipped']): + raise ValueError(f"required case failed: {item['script']}: {item}") + + +def capture(command, *, cwd, env, log, timeout): + start = time.monotonic() + timed_out = False + with Path(log).open('w') as stream: + process = subprocess.Popen(command, cwd=cwd, env=env, stdout=stream, + stderr=subprocess.STDOUT, start_new_session=True) + try: + process.wait(timeout=timeout) + except subprocess.TimeoutExpired: + timed_out = True + os.killpg(process.pid, signal.SIGTERM) + try: + process.wait(timeout=10) + except subprocess.TimeoutExpired: + os.killpg(process.pid, signal.SIGKILL) + process.wait(timeout=10) + return {'returncode': process.returncode, 'timed_out': timed_out, + 'seconds': round(time.monotonic() - start, 3)} + + +def wheel_runtime(wheel): + """Read wheel version and package runtime hashes without importing it.""" + with zipfile.ZipFile(wheel) as archive: + metadata_names = [name for name in archive.namelist() + if name.endswith('.dist-info/METADATA')] + if len(metadata_names) != 1: + raise ValueError('wheel has no unique METADATA') + metadata = BytesParser().parsebytes(archive.read(metadata_names[0])) + if metadata.get('Name', '').lower() != 'abacuslite': + raise ValueError('wheel is not abacuslite') + runtime = {name: hashlib.sha256(archive.read(name)).hexdigest() + for name in archive.namelist() + if name.startswith('abacuslite/') and name.endswith('.py')} + if 'abacuslite/__init__.py' not in runtime: + raise ValueError('wheel lacks abacuslite runtime') + return metadata['Version'], runtime + + +def harness_revision(repo): + """Record checkout identity separately from copied script hashes.""" + revision = subprocess.run(['git', '-C', str(repo), 'rev-parse', 'HEAD'], + capture_output=True, text=True, check=False) + status = subprocess.run(['git', '-C', str(repo), 'status', '--porcelain'], + capture_output=True, text=True, check=False) + return {'ref': revision.stdout.strip() if revision.returncode == 0 else None, + 'dirty': bool(status.stdout.strip()) if status.returncode == 0 else None} + + +def attest_install(probe, wheel, repo): + """Require every wheel runtime module to match installed site-packages.""" + version, runtime = wheel_runtime(wheel) + imported = Path(probe['module']).resolve() + installed = Path(probe['installed']).resolve() + prefix = Path(probe['prefix']).resolve() + if probe['version'] != version or imported != installed: + raise ValueError(f'installed wheel identity mismatch: {probe}') + if not imported.is_relative_to(prefix) or 'site-packages' not in imported.parts: + raise ValueError(f'not installed in interpreter site-packages: {imported}') + if imported.is_relative_to(repo): + raise ValueError(f'ABACUS checkout shadowed installed wheel: {imported}') + package = imported.parent.parent + for name, digest in runtime.items(): + path = package / name + if not path.is_file() or sha256(path) != digest: + raise ValueError(f'installed runtime differs from supplied wheel: {name}') + return {'version': version, 'runtime_sha256': runtime} + + +def required_path(value, *, file=False, directory=False, follow_symlinks=True): + path = Path(value) + if not path.is_absolute(): + raise ValueError(f'path must be absolute: {value}') + path = path.resolve() if follow_symlinks else path.absolute() + if file and not path.is_file(): + raise FileNotFoundError(path) + if directory and not path.is_dir(): + raise FileNotFoundError(path) + return path + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--engine', required=True) + parser.add_argument('--mpi-command', required=True) + parser.add_argument('--assets', required=True) + parser.add_argument('--format', choices=CASES, required=True) + parser.add_argument('--wheel', required=True) + parser.add_argument('--report-dir', required=True) + parser.add_argument('--engine-ref', required=True) + parser.add_argument('--python', default=sys.executable) + args = parser.parse_args(argv) + if not re.fullmatch(r'[0-9a-f]{40}', args.engine_ref): + parser.error('--engine-ref must be a full lowercase commit SHA') + engine = required_path(args.engine, file=True) + # Resolving a venv's python symlink would select its base interpreter. + python = required_path(args.python, file=True, follow_symlinks=False) + assets = required_path(args.assets, directory=True) + wheel = required_path(args.wheel, file=True) + report = required_path(args.report_dir) + if report.is_relative_to(REPO) or REPO.is_relative_to(report): + raise ValueError('report directory must be outside ABACUS checkout') + if report.exists() and any(report.iterdir()): + raise ValueError(f'report directory is not empty: {report}') + missing = [name for name in ASSETS if not (assets / name).is_file()] + if missing: + raise FileNotFoundError(f'missing curated assets: {missing}') + mpi = shlex.split(args.mpi_command) + if not mpi: + raise ValueError('empty MPI command') + report.mkdir(parents=True, exist_ok=True) + harness = report / 'harness' + harness.mkdir() + scripts = set(CASES[args.format]) | {'common.py', 'legacy_facts.py'} + for name in scripts: + shutil.copy2(HERE / name, harness / name) + env = os.environ.copy() + for key in ('PYTHONPATH', 'PYTHONHOME', 'ASE_ABACUS_SOCKET', 'ASE_ABACUS_RAW_DIR'): + env.pop(key, None) + env.update(ABACUS_COMMAND=shlex.join(mpi + [str(engine)]), + ASE_ABACUS_ASSETS=str(assets), ASE_ABACUS_FORMAT=args.format, + ASE_ABACUS_KEEP_DIR=str(report / 'failed-runs'), + OMP_NUM_THREADS='1', OPENBLAS_NUM_THREADS='1') + record = {'schema': 'abacuslite-engine-compatibility-v1', 'format': args.format, + 'engine': str(engine), 'engine_ref_asserted': args.engine_ref, + 'engine_sha256': sha256(engine), 'mpi_command': mpi, + 'harness_checkout': harness_revision(REPO), + 'assets': {name: sha256(assets / name) for name in ASSETS}, + 'wheel': str(wheel), 'wheel_sha256': sha256(wheel), + 'python': str(python), 'harness_sha256': sha256(__file__), + 'scripts_sha256': {name: sha256(harness / name) for name in sorted(scripts)}, + 'expected_cases': CASES[args.format], 'cases': [], 'all_pass': False} + output = report / 'evidence.json' + + def save(): + output.write_text(json.dumps(record, indent=2, sort_keys=True) + '\n') + + save() + try: + probe_result = capture([str(engine), '--version'], cwd=harness, env=env, + log=report / 'engine-version.log', timeout=30) + record['engine_probe'] = probe_result + record['engine_version_output'] = (report / 'engine-version.log').read_text() + save() + if probe_result['returncode'] != 0 or probe_result['timed_out']: + raise RuntimeError('engine --version failed') + probe_result = capture([str(python), '-c', PROBE, str(engine)], cwd=harness, + env=env, log=report / 'package-probe.log', timeout=45) + record['package_probe_result'] = probe_result + if probe_result['returncode'] != 0 or probe_result['timed_out']: + raise RuntimeError('installed package probe failed') + probe = json.loads((report / 'package-probe.log').read_text().splitlines()[-1]) + record['package_probe'] = probe + record['wheel_runtime'] = attest_install(probe, wheel, REPO) + if probe['legacyio'] is not (args.format == 'legacy'): + raise ValueError(f'wrong calculator reader for {args.format}: {probe}') + save() + for script, expected in CASES[args.format].items(): + name = script.removesuffix('.py') + raw = report / 'raw' / name + raw.mkdir(parents=True) + case_env = dict(env, ASE_ABACUS_RAW_DIR=str(raw)) + log = report / f'{name}.log' + result = capture([str(python), str(harness / script), '-v'], cwd=harness, + env=case_env, log=log, timeout=240) + content = log.read_text() + counts = [int(value) for value in re.findall(r'Ran (\d+) tests?', content)] + result.update(script=script, expected_tests=expected, + tests_reported=sum(counts), + skipped=bool(re.search(r'\bskipped=\d+\b', content)), + log=log.name) + record['cases'].append(result) + save() + print(f'{script}: {result}', flush=True) + validate_cases(args.format, record['cases']) + record['all_pass'] = True + except Exception as exc: + record['error'] = f'{type(exc).__name__}: {exc}' + raise + finally: + record['raw_sha256'] = {str(path.relative_to(report)): sha256(path) + for path in sorted((report / 'raw').rglob('*')) + if path.is_file()} if (report / 'raw').exists() else {} + record['dependency_freeze'] = None + freeze = capture([str(python), '-m', 'pip', 'freeze'], cwd=harness, env=env, + log=report / 'pip-freeze.log', timeout=45) + record['dependency_freeze'] = freeze + save() + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/interfaces/ASE_interface/tests/test_harness_facts.py b/interfaces/ASE_interface/tests/test_harness_facts.py index a15d0154ac6..398d3d86026 100644 --- a/interfaces/ASE_interface/tests/test_harness_facts.py +++ b/interfaces/ASE_interface/tests/test_harness_facts.py @@ -18,6 +18,7 @@ from common import (assert_band_facts, assert_collinear_magmoms, calculate_socket_frame, close_socket_bounded, preserved_tmpdir) +from legacy_facts import assert_band_facts as assert_legacy_band_facts class HarnessFactsTest(unittest.TestCase): @@ -65,6 +66,23 @@ def test_band_eigenvalues_reject_mutated_reader_result(self): with self.assertRaises(AssertionError): assert_band_facts(self, bands, path) + def test_legacy_band_oracle_rejects_mutated_reader_result(self): + raw = '''1/2 kpoint (Cartesian) = 0.0 0.0 0.0 (1 pws) +1 -2.5000 1.0 +2 1.2500 0.0 +2/2 kpoint (Cartesian) = 0.5 0.0 0.0 (1 pws) +1 -2.2500 1.0 +2 1.5000 0.0 +''' + with tempfile.TemporaryDirectory() as root: + path = Path(root) / 'running_nscf.log' + path.write_text(raw) + bands = SimpleNamespace(energies=np.array([[[-2.5, 1.25], [-2.25, 1.5]]])) + assert_legacy_band_facts(self, bands, path) + bands.energies[0, 1, 1] += 0.1 + with self.assertRaises(AssertionError): + assert_legacy_band_facts(self, bands, path) + def test_per_atom_magnetic_moment_rejects_swap(self): raw = '''Total Magnetism (uB) -------------------- diff --git a/interfaces/ASE_interface/tests/test_portable_runner.py b/interfaces/ASE_interface/tests/test_portable_runner.py new file mode 100644 index 00000000000..b7009fdee2f --- /dev/null +++ b/interfaces/ASE_interface/tests/test_portable_runner.py @@ -0,0 +1,72 @@ +"""Offline contract checks for the portable real-engine lane.""" + +import tempfile +import unittest +import zipfile +from pathlib import Path + +from run_engine_compatibility import (CASES, attest_install, capture, + required_path, validate_cases) + + +class PortableRunnerTest(unittest.TestCase): + def test_venv_python_symlink_is_not_resolved_to_base_interpreter(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + binary = root / 'base-python' + binary.touch() + venv_python = root / 'venv-python' + venv_python.symlink_to(binary) + self.assertEqual(required_path(str(venv_python), file=True, + follow_symlinks=False), venv_python) + + def test_required_case_set_rejects_missing_failure_skip_and_wrong_count(self): + cases = [{'script': name, 'returncode': 0, 'timed_out': False, + 'tests_reported': count, 'skipped': False} + for name, count in CASES['legacy'].items()] + validate_cases('legacy', cases) + for changed in (cases[:-1],): + with self.assertRaisesRegex(ValueError, 'incomplete'): + validate_cases('legacy', changed) + for change in ({'returncode': 1}, {'timed_out': True}, + {'tests_reported': 0}, {'skipped': True}): + altered = [dict(item) for item in cases] + altered[0].update(change) + with self.assertRaisesRegex(ValueError, 'required case failed'): + validate_cases('legacy', altered) + + def test_installed_runtime_bytes_must_match_supplied_wheel(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + site = root / 'venv' / 'lib' / 'python3.10' / 'site-packages' + package = site / 'abacuslite' + package.mkdir(parents=True) + imported = package / '__init__.py' + imported.write_text('original') + wheel = root / 'abacuslite-1.1.0-py3-none-any.whl' + with zipfile.ZipFile(wheel, 'w') as archive: + archive.writestr('abacuslite/__init__.py', 'original') + archive.writestr('abacuslite-1.1.0.dist-info/METADATA', + 'Metadata-Version: 2.1\nName: abacuslite\nVersion: 1.1.0\n') + probe = {'module': str(imported), 'installed': str(imported), + 'prefix': str(root / 'venv'), 'version': '1.1.0'} + attest_install(probe, wheel, root / 'checkout') + imported.write_text('mutated') + with self.assertRaisesRegex(ValueError, 'runtime differs'): + attest_install(probe, wheel, root / 'checkout') + imported.write_text('original') + with self.assertRaisesRegex(ValueError, 'shadowed'): + attest_install(probe, wheel, root) + + def test_hung_case_has_bounded_process_group_timeout(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + result = capture(['python3', '-c', 'import time; time.sleep(60)'], + cwd=root, env=None, log=root / 'hung.log', timeout=0.1) + self.assertTrue(result['timed_out']) + self.assertNotEqual(result['returncode'], 0) + self.assertLess(result['seconds'], 3) + + +if __name__ == '__main__': + unittest.main() From e446bda12f852a29d9cffd883ef29642e83cc6a6 Mon Sep 17 00:00:00 2001 From: QuantumMisaka Date: Sun, 18 Oct 2026 02:24:00 +0800 Subject: [PATCH 07/12] Attest built ABACUS and bound harness timeout cleanup --- .github/workflows/ase_plugin_test.yml | 22 +++++++-- .../tests/run_engine_compatibility.py | 14 +++++- .../tests/test_portable_runner.py | 48 +++++++++++++++++++ 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ase_plugin_test.yml b/.github/workflows/ase_plugin_test.yml index e18e8b079b7..66dcb22e1eb 100644 --- a/.github/workflows/ase_plugin_test.yml +++ b/.github/workflows/ase_plugin_test.yml @@ -52,6 +52,7 @@ jobs: - name: Install dependencies shell: bash -l {0} run: | + set -euo pipefail conda install -y pip pip install numpy scipy matplotlib @@ -59,6 +60,7 @@ jobs: if: matrix.package == 'bundled' shell: bash -l {0} run: | + set -euo pipefail mkdir -p "$RUNNER_TEMP/bundled-wheel" python -m pip wheel --no-deps --wheel-dir "$RUNNER_TEMP/bundled-wheel" \ interfaces/ASE_interface @@ -71,6 +73,7 @@ jobs: env: PLUGIN_REF: ${{ inputs.plugin_ref }} run: | + set -euo pipefail mkdir -p "$RUNNER_TEMP/candidate-wheel" if [ -n "$PLUGIN_REF" ]; then python -m pip install 'build>=1.2' @@ -107,12 +110,18 @@ jobs: cmake -B build -G Ninja cmake --build build -j2 - - name: Install and Soft Link ABACUS + - name: Install and attest the built ABACUS executable run: | + built="$GITHUB_WORKSPACE/build/abacus_basic_para" + test -x "$built" cmake --install build - if [ -f /usr/local/bin/abacus_2p ]; then - ln -sf /usr/local/bin/abacus_2p /usr/local/bin/abacus - fi + installed="$(command -v abacus)" + test -x "$installed" + built_sha="$(sha256sum "$built")" + installed_sha="$(sha256sum "$installed")" + test "${built_sha%% *}" = "${installed_sha%% *}" + printf 'Built ABACUS: %s %s\n' "$built" "${built_sha%% *}" + printf 'Installed ABACUS: %s %s\n' "$installed" "${installed_sha%% *}" git rev-parse HEAD abacus --version @@ -120,6 +129,7 @@ jobs: if: matrix.package == 'bundled' shell: bash -l {0} run: | + set -euo pipefail cd interfaces/ASE_interface/abacuslite chmod +x xtest.sh ./xtest.sh @@ -128,6 +138,7 @@ jobs: if: matrix.package == 'external' shell: bash -l {0} run: | + set -euo pipefail cd interfaces/ASE_interface/tests python -m unittest test_external_provenance test_harness_facts \ test_native_md_fixture test_portable_runner -v @@ -135,6 +146,7 @@ jobs: - name: Run engine-owned integration tests shell: bash -l {0} run: | + set -euo pipefail ENGINE_REF="$(git rev-parse HEAD)" if [ "${{ matrix.package }}" = external ]; then WHEEL_DIR="$RUNNER_TEMP/candidate-wheel" @@ -142,7 +154,7 @@ jobs: WHEEL_DIR="$RUNNER_TEMP/bundled-wheel" fi python interfaces/ASE_interface/tests/run_engine_compatibility.py \ - --engine /usr/local/bin/abacus \ + --engine "$GITHUB_WORKSPACE/build/abacus_basic_para" \ --mpi-command 'mpirun -np 2' \ --assets "$GITHUB_WORKSPACE/tests/PP_ORB" \ --format latest \ diff --git a/interfaces/ASE_interface/tests/run_engine_compatibility.py b/interfaces/ASE_interface/tests/run_engine_compatibility.py index d45a6c08ad8..4e707dc200d 100644 --- a/interfaces/ASE_interface/tests/run_engine_compatibility.py +++ b/interfaces/ASE_interface/tests/run_engine_compatibility.py @@ -79,12 +79,22 @@ def capture(command, *, cwd, env, log, timeout): process.wait(timeout=timeout) except subprocess.TimeoutExpired: timed_out = True - os.killpg(process.pid, signal.SIGTERM) + try: + os.killpg(process.pid, signal.SIGTERM) + except ProcessLookupError: + pass try: process.wait(timeout=10) except subprocess.TimeoutExpired: + pass + # The leader can exit on SIGTERM while a child ignores it. The + # process group remains alive, so always finish the timeout with + # SIGKILL rather than treating leader exit as group shutdown. + try: os.killpg(process.pid, signal.SIGKILL) - process.wait(timeout=10) + except ProcessLookupError: + pass + process.wait(timeout=10) return {'returncode': process.returncode, 'timed_out': timed_out, 'seconds': round(time.monotonic() - start, 3)} diff --git a/interfaces/ASE_interface/tests/test_portable_runner.py b/interfaces/ASE_interface/tests/test_portable_runner.py index b7009fdee2f..a06c84fd72d 100644 --- a/interfaces/ASE_interface/tests/test_portable_runner.py +++ b/interfaces/ASE_interface/tests/test_portable_runner.py @@ -1,6 +1,10 @@ """Offline contract checks for the portable real-engine lane.""" +import os +import signal +import sys import tempfile +import time import unittest import zipfile from pathlib import Path @@ -67,6 +71,50 @@ def test_hung_case_has_bounded_process_group_timeout(self): self.assertNotEqual(result['returncode'], 0) self.assertLess(result['seconds'], 3) + def test_timeout_kills_sigterm_ignoring_child_after_leader_exits(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + pid_file = root / 'child.pid' + child_code = ('import os,signal,sys,time; ' + 'signal.signal(signal.SIGTERM, signal.SIG_IGN); ' + 'open(sys.argv[1], "w").write(str(os.getpid())); ' + 'time.sleep(60)') + leader_code = ('import subprocess,sys,time; ' + 'subprocess.Popen([sys.executable, "-c", ' + 'sys.argv[1], sys.argv[2]]); time.sleep(60)') + child_pid = None + try: + result = capture([sys.executable, '-c', leader_code, + child_code, str(pid_file)], + cwd=root, env=None, log=root / 'hung-group.log', + timeout=1) + self.assertTrue(pid_file.is_file(), 'child probe did not start') + child_pid = int(pid_file.read_text()) + self.assertTrue(result['timed_out']) + self.assertLess(result['seconds'], 4) + for _ in range(50): + try: + state = Path(f'/proc/{child_pid}/stat').read_text().split()[2] + except FileNotFoundError: + break + if state == 'Z': + break + time.sleep(0.02) + else: + self.fail(f'timeout left SIGTERM-ignoring child {child_pid} alive') + finally: + if child_pid is None and pid_file.is_file(): + child_pid = int(pid_file.read_text()) + if child_pid is not None: + try: + child_group = os.getpgid(child_pid) + if child_group != os.getpgrp(): + os.killpg(child_group, signal.SIGKILL) + else: + os.kill(child_pid, signal.SIGKILL) + except ProcessLookupError: + pass + if __name__ == '__main__': unittest.main() From eeb1136372e9389b5712901698ad7faf15777cab Mon Sep 17 00:00:00 2001 From: QuantumMisaka Date: Sun, 18 Oct 2026 17:20:30 +0800 Subject: [PATCH 08/12] docs: document the socket protocol compatibility contract State the abacus.socket.properties.v1 schema, additive/breaking-change rule and the abacuslite adapter contract link in the ASE socket documentation. --- docs/advanced/interface/ase.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/advanced/interface/ase.md b/docs/advanced/interface/ase.md index a10978b9aa3..27ef9afaab0 100644 --- a/docs/advanced/interface/ase.md +++ b/docs/advanced/interface/ase.md @@ -203,6 +203,20 @@ Calling `atoms.get_potential_energy()` does not force a force or stress calculat The ABACUS metadata extension is required to expose force/stress presence safely. If a legacy client returns an empty extras field, the adapter accepts only an energy-only response and refuses to infer forces or stress from the fixed-wire padding. Generic i-PI/ASE clients that ignore ABACUS extras cannot distinguish mandatory padding from a computed zero; use `AbacusSocketIO` or another metadata-aware client when requesting optional properties. When launching ABACUS with a generic client, explicitly set `cal_force=1` for force-driven workflows and `cal_stress=1` for stress evaluation; an omitted switch defaults to disabled. Such clients also need their own policy for unconverged SCF results. +#### Socket protocol compatibility + +The socket metadata extension uses the schema `abacus.socket.properties.v1`. +The `present` list distinguishes computed properties from protocol padding, and +`scf_converged` reports the SCF state for the returned frame. Additive metadata +fields may be added within v1; a breaking change requires a new schema version +and a transition period in both ABACUS and abacuslite. + +The ASE-side adapter contract is documented in the abacuslite repository: +https://github.com/QuantumMisaka/abacuslite/blob/main/docs/socket-contract.md + +Engine and adapter changes that alter this contract should be linked and tested +together. + A socket calculator owns one ABACUS process initialized from one fixed `INPUT`/`STRU` setup. Reuse the same `AbacusSocketIO` instance only for position updates under the same electronic-structure settings and the same cell. Do not change `kpts`, `kspacing`, `nspin`, `basis_type`, `basissets`, pseudopotentials, species, atom count, cell, or other core `INPUT`/`STRU` parameters through an existing socket calculator; create a new `AbacusSocketIO` instance and a new ABACUS client process for those changes. `AbacusSocketIO` rejects cell changes before sending them to ABACUS, and the ABACUS socket driver also checks incoming POSDATA cells against the initial `STRU` cell and exits if they differ. In socket mode, ABACUS keeps one client process alive. All SCF evaluations produced by the same `AbacusSocketIO` instance are appended to the same `OUT.ABACUS/running_scf.log`, because the ABACUS calculation type remains `scf`. The authoritative per-step energy and force results are returned through the i-PI socket to ASE. Use ASE trajectory and optimizer log files, such as `BFGS(atoms, trajectory="opt.traj", logfile="opt.log")`, when each optimizer or MD step should be saved separately. Treat `running_scf.log` mainly as the ABACUS diagnostic log for the socket client, not as one independent FileIO result per structure. From 3b49e5ae62441ba030fc7eb24afdc90bebbf31aa Mon Sep 17 00:00:00 2001 From: QuantumMisaka Date: Mon, 19 Oct 2026 00:37:42 +0800 Subject: [PATCH 09/12] refactor(ase): consume external abacuslite plugin --- .github/pull_request_template.md | 1 + .github/workflows/ase_plugin_test.yml | 68 +- docs/advanced/interface/ase.md | 44 +- interfaces/ASE_interface/DEVELOP.md | 90 -- interfaces/ASE_interface/README.md | 139 +- .../ASE_interface/abacuslite/__init__.py | 13 - interfaces/ASE_interface/abacuslite/core.py | 1262 ----------------- .../ASE_interface/abacuslite/io/__init__.py | 0 .../ASE_interface/abacuslite/io/generalio.py | 965 ------------- .../ASE_interface/abacuslite/io/latestio.py | 717 ---------- .../ASE_interface/abacuslite/io/legacyio.py | 1261 ---------------- .../abacuslite/io/testfiles/input-script | 34 - .../lcao-symm0-nspin2-multik-cellrelax | 315 ---- .../lcao-symm0-nspin2-multik-cellrelax_ | 648 --------- .../testfiles/lcao-symm0-nspin2-multik-relax | 254 ---- .../testfiles/lcao-symm0-nspin2-multik-relax_ | 542 ------- .../io/testfiles/lcao-symm1-nspin1-multik-scf | 135 -- .../testfiles/lcao-symm1-nspin1-multik-scf_ | 259 ---- .../io/testfiles/pw-symm0-nspin4-gamma-md_ | 409 ------ .../abacuslite/utils/__init__.py | 0 .../abacuslite/utils/ksampling.py | 260 ---- interfaces/ASE_interface/abacuslite/xtest.sh | 10 - .../ASE_interface/examples/bandstructure.py | 82 -- .../ASE_interface/examples/cellrelax.py | 76 - .../ASE_interface/examples/constraintmd.py | 101 -- interfaces/ASE_interface/examples/dos.py | 65 - interfaces/ASE_interface/examples/md.py | 72 - .../ASE_interface/examples/metadynamics.py | 146 -- interfaces/ASE_interface/examples/neb.py | 111 -- interfaces/ASE_interface/examples/relax.py | 67 - interfaces/ASE_interface/examples/scf.py | 56 - interfaces/ASE_interface/examples/soc.py | 63 - interfaces/ASE_interface/examples/socketio.py | 157 -- interfaces/ASE_interface/pyproject.toml | 24 - .../fixtures/native_md/MD_dump} | 0 .../fixtures/native_md/eig_occ.txt} | 0 .../fixtures/native_md/running_md.log.txt} | 0 .../tests/test_external_provenance.py | 2 +- .../tests/test_native_md_fixture.py | 10 +- .../ASE_interface/tests/verify_external.py | 17 +- interfaces/README | 5 + source/source_relax/socket_utils.cpp | 2 +- source/source_relax/socket_utils.h | 1 + .../unittests/test_socket_driver.cpp | 12 + 44 files changed, 143 insertions(+), 8352 deletions(-) delete mode 100644 interfaces/ASE_interface/DEVELOP.md delete mode 100644 interfaces/ASE_interface/abacuslite/__init__.py delete mode 100644 interfaces/ASE_interface/abacuslite/core.py delete mode 100644 interfaces/ASE_interface/abacuslite/io/__init__.py delete mode 100644 interfaces/ASE_interface/abacuslite/io/generalio.py delete mode 100644 interfaces/ASE_interface/abacuslite/io/latestio.py delete mode 100644 interfaces/ASE_interface/abacuslite/io/legacyio.py delete mode 100644 interfaces/ASE_interface/abacuslite/io/testfiles/input-script delete mode 100644 interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm0-nspin2-multik-cellrelax delete mode 100644 interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm0-nspin2-multik-cellrelax_ delete mode 100644 interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm0-nspin2-multik-relax delete mode 100644 interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm0-nspin2-multik-relax_ delete mode 100644 interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm1-nspin1-multik-scf delete mode 100644 interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm1-nspin1-multik-scf_ delete mode 100644 interfaces/ASE_interface/abacuslite/io/testfiles/pw-symm0-nspin4-gamma-md_ delete mode 100644 interfaces/ASE_interface/abacuslite/utils/__init__.py delete mode 100644 interfaces/ASE_interface/abacuslite/utils/ksampling.py delete mode 100755 interfaces/ASE_interface/abacuslite/xtest.sh delete mode 100644 interfaces/ASE_interface/examples/bandstructure.py delete mode 100644 interfaces/ASE_interface/examples/cellrelax.py delete mode 100644 interfaces/ASE_interface/examples/constraintmd.py delete mode 100644 interfaces/ASE_interface/examples/dos.py delete mode 100644 interfaces/ASE_interface/examples/md.py delete mode 100644 interfaces/ASE_interface/examples/metadynamics.py delete mode 100644 interfaces/ASE_interface/examples/neb.py delete mode 100644 interfaces/ASE_interface/examples/relax.py delete mode 100644 interfaces/ASE_interface/examples/scf.py delete mode 100644 interfaces/ASE_interface/examples/soc.py delete mode 100644 interfaces/ASE_interface/examples/socketio.py delete mode 100644 interfaces/ASE_interface/pyproject.toml rename interfaces/ASE_interface/{abacuslite/io/testfiles/nspin4-gamma-mddump => tests/fixtures/native_md/MD_dump} (100%) rename interfaces/ASE_interface/{abacuslite/io/testfiles/nspin4-gamma-eigocc => tests/fixtures/native_md/eig_occ.txt} (100%) rename interfaces/ASE_interface/{abacuslite/io/testfiles/pw-symm0-nspin4-gamma-md => tests/fixtures/native_md/running_md.log.txt} (100%) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 83c986d168c..b99f4c6a35d 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -5,6 +5,7 @@ - [ ] I have listed the exact verification commands run and their results. - [ ] I have described user-visible behavior changes, including INPUT parameter changes. - [ ] I have explained core-module impact for ESolver, HSolver, ElecState, Hamilt, Operator, Psi, or other `source/` changes. +- [ ] If this changes `interfaces/ASE_interface/`, `source/source_relax/socket_*`, or the socket metadata schema, I linked the abacuslite counterpart and updated the compatibility contract. - [ ] I have requested any needed governance exception below. ### Linked Issue diff --git a/.github/workflows/ase_plugin_test.yml b/.github/workflows/ase_plugin_test.yml index 66dcb22e1eb..c493b5951c2 100644 --- a/.github/workflows/ase_plugin_test.yml +++ b/.github/workflows/ase_plugin_test.yml @@ -1,7 +1,9 @@ -name: Atomic Simulation Environment (ASE) Plugin Test +name: Atomic Simulation Environment (ASE) Interface Test on: pull_request: + schedule: + - cron: '41 3 * * 1' workflow_dispatch: inputs: plugin_ref: @@ -15,15 +17,11 @@ defaults: jobs: test: - name: abacuslite (${{ matrix.package }}) + name: abacuslite external runs-on: ubuntu-latest timeout-minutes: 120 container: image: ghcr.io/deepmodeling/abacus-gnu - strategy: - fail-fast: false - matrix: - package: [bundled, external] env: ABACUS_COMMAND: mpirun -np 2 abacus ASE_ABACUS_ASSETS: ${{ github.workspace }}/tests/PP_ORB @@ -33,8 +31,8 @@ jobs: - name: Checkout engine PR uses: actions/checkout@v7.0.1 - - name: Checkout reviewed candidate - if: matrix.package == 'external' && inputs.plugin_ref != '' + - name: Checkout reviewed plugin candidate + if: github.event_name == 'workflow_dispatch' && inputs.plugin_ref != '' uses: actions/checkout@v7.0.1 with: repository: QuantumMisaka/abacuslite @@ -49,41 +47,29 @@ jobs: activate-environment: abacuslite python-version: '3.10' - - name: Install dependencies + - name: Install Python dependencies shell: bash -l {0} run: | set -euo pipefail conda install -y pip pip install numpy scipy matplotlib - - name: Install bundled plugin - if: matrix.package == 'bundled' - shell: bash -l {0} - run: | - set -euo pipefail - mkdir -p "$RUNNER_TEMP/bundled-wheel" - python -m pip wheel --no-deps --wheel-dir "$RUNNER_TEMP/bundled-wheel" \ - interfaces/ASE_interface - sha256sum "$RUNNER_TEMP"/bundled-wheel/*.whl - python -m pip install "$RUNNER_TEMP"/bundled-wheel/*.whl - - - name: Build and install candidate wheel - if: matrix.package == 'external' + - name: Install abacuslite shell: bash -l {0} env: PLUGIN_REF: ${{ inputs.plugin_ref }} run: | set -euo pipefail - mkdir -p "$RUNNER_TEMP/candidate-wheel" + mkdir -p "$RUNNER_TEMP/plugin-wheel" if [ -n "$PLUGIN_REF" ]; then python -m pip install 'build>=1.2' - python -m build --wheel --outdir "$RUNNER_TEMP/candidate-wheel" candidate-plugin + python -m build --wheel --outdir "$RUNNER_TEMP/plugin-wheel" candidate-plugin git -C candidate-plugin rev-parse HEAD else python -m pip download --only-binary=:all: --no-deps \ - --dest "$RUNNER_TEMP/candidate-wheel" 'abacuslite==1.1.0' + --dest "$RUNNER_TEMP/plugin-wheel" 'abacuslite==1.1.0' fi - wheels=("$RUNNER_TEMP"/candidate-wheel/*.whl) + wheels=("$RUNNER_TEMP"/plugin-wheel/*.whl) test "${#wheels[@]}" -eq 1 sha256sum "${wheels[0]}" if [ -z "$PLUGIN_REF" ]; then @@ -92,6 +78,8 @@ jobs: fi python -m pip install "${wheels[0]}" python -m pip show abacuslite ase numpy seekpath + python interfaces/ASE_interface/tests/verify_external.py \ + --checkout-root "$GITHUB_WORKSPACE" - name: Install external tools from toolchain run: | @@ -101,9 +89,9 @@ jobs: ./scripts/stage4/install_stage4.sh cd .. - - name: Configure & Build ABACUS (GNU) + - name: Configure & build ABACUS (GNU) run: | - git config --global --add safe.directory `pwd` + git config --global --add safe.directory "$(pwd)" source toolchain/install/setup prepend_path CMAKE_PREFIX_PATH ${PEXSI32_ROOT}:${SUPERLU_DIST32_ROOT}:${PARMETIS32_ROOT}:${METIS32_ROOT}:${GKLIB_ROOT} rm -rf build @@ -125,17 +113,7 @@ jobs: git rev-parse HEAD abacus --version - - name: Run bundled unit tests - if: matrix.package == 'bundled' - shell: bash -l {0} - run: | - set -euo pipefail - cd interfaces/ASE_interface/abacuslite - chmod +x xtest.sh - ./xtest.sh - - - name: Run provenance regressions - if: matrix.package == 'external' + - name: Run offline harness contracts shell: bash -l {0} run: | set -euo pipefail @@ -147,27 +125,21 @@ jobs: shell: bash -l {0} run: | set -euo pipefail - ENGINE_REF="$(git rev-parse HEAD)" - if [ "${{ matrix.package }}" = external ]; then - WHEEL_DIR="$RUNNER_TEMP/candidate-wheel" - else - WHEEL_DIR="$RUNNER_TEMP/bundled-wheel" - fi python interfaces/ASE_interface/tests/run_engine_compatibility.py \ --engine "$GITHUB_WORKSPACE/build/abacus_basic_para" \ --mpi-command 'mpirun -np 2' \ --assets "$GITHUB_WORKSPACE/tests/PP_ORB" \ --format latest \ - --wheel "$WHEEL_DIR"/*.whl \ + --wheel "$RUNNER_TEMP"/plugin-wheel/*.whl \ --report-dir "$RUNNER_TEMP/ase-engine-tests" \ - --engine-ref "$ENGINE_REF" \ + --engine-ref "$(git rev-parse HEAD)" \ --python "$(command -v python)" - name: Upload ASE failure artifacts if: always() uses: actions/upload-artifact@v7 with: - name: ase-failure-${{ matrix.package }}-${{ github.run_id }}-${{ github.run_attempt }} + name: ase-failure-${{ github.run_id }}-${{ github.run_attempt }} path: | ${{ runner.temp }}/ase-engine-tests/** if-no-files-found: ignore diff --git a/docs/advanced/interface/ase.md b/docs/advanced/interface/ase.md index 27ef9afaab0..0755baf7e57 100644 --- a/docs/advanced/interface/ase.md +++ b/docs/advanced/interface/ase.md @@ -4,7 +4,11 @@ [ASE](https://wiki.fysik.dtu.dk/ase/) (Atomic Simulation Environment) performs as a powerful Pythonic platform for atomistic simulations, in which there are plenty of functionalties supported, such as various geometry optimization algorithms for finding both the minimum energy point and the transition states, including BFGS, BFGSLineSearch, FIRE, NEB, AUTO-NEB, etc, and various molecular dynamics techniques, including thermostats (Langevin, CSVR, Nose-Hoover Chain, etc) and metadynamics (via the interface with Plumed). -Due to the growing number of softwares and machine-learning forcefields, we turn to maintain the interface with ASE by our own, while a legacy version of ASE interface can still be found at [our GitLab repository of ase-abacus](https://gitlab.com/1041176461/ase-abacus ). +Due to the growing number of softwares and machine-learning forcefields, the +ABACUS ASE interface is maintained as the independent +[`abacuslite`](https://github.com/QuantumMisaka/abacuslite) package. A legacy +version of the ASE interface can still be found at +[our GitLab repository of ase-abacus](https://gitlab.com/1041176461/ase-abacus). ## Installation @@ -15,22 +19,21 @@ conda create -n abacus python=3.10 conda activate abacus ``` -Then, install the ASE interface by: +Then, install the independently maintained ASE plugin: ```bash -cd interfaces/ASE_interface -pip install . +python -m pip install abacuslite ``` -For the independent package, install the published `abacuslite==1.1.0` wheel -in a separate environment. The ABACUS PR CI retains parallel `bundled` and -`external` jobs; the external lane verifies its installed modules against the -wheel and runs the engine-owned harness outside the bundled source tree. The -portable runner accepts an explicit executable, MPI launcher, curated PP/ORB -assets, output format, wheel and report directory. Its develop lane includes -the bounded fixed-cell socket case and native MD; LTS runs the supported cases -plus seeded native MD. See [the ASE interface README](../../../interfaces/ASE_interface/README.md) -for the exact command. Manual CI dispatch can select a reviewed source ref. +`abacuslite` owns the Python calculator, ABACUS input/output readers and user +examples. The ABACUS repository keeps only the engine-side compatibility +harness in [`interfaces/ASE_interface/tests`](../../../interfaces/ASE_interface/README.md). +ABACUS CI installs the published plugin, or a reviewed candidate ref, and runs +that harness against the executable built from the current source. Manual CI +dispatch can select the candidate ref. + +The plugin is not vendored and is not a git submodule, so GitHub source +archives remain complete. ## ABACUS Calculator @@ -111,7 +114,8 @@ with tempfile.TemporaryDirectory() as tmpdir: In the new implementation, we limit the range of functionalties supported to mainly include the necessary ones, such as the SCF calculation, the energy and force/stress evaluation. The other features, such as starting the molecule dynamics directly in ABACUS from Python, is not supported anymore. Instead, it is encouraged to use the ASE tools to perform the molecule dynamics. -Please read the examples in `interfaces/ASE_interface/examples/` for more details. +See the [abacuslite examples](https://github.com/QuantumMisaka/abacuslite/tree/main/examples) +for complete scripts. ### Socket I/O with ASE @@ -158,11 +162,10 @@ With the ABACUS toolchain workflow, build the normal ABACUS executable with LCAO For PW calculations on CUDA/ROCm with multiple MPI ranks, use a k-point layout compatible with ABACUS' GPU parallelization. In practice, make sure each k-point pool contains one MPI rank; for example, a 4-rank PW GPU socket calculation should use at least four k-points so the default GPU `kpar` adjustment can assign one rank per pool. A one-k-point PW GPU job with several MPI ranks can fail in the PW GPU transform path; reduce the rank count or use a denser k-point mesh such as a smaller `kspacing`. -The ASE interface can be installed from this repository with: +The ASE interface is installed from PyPI: ```bash -cd interfaces/ASE_interface -pip install . +python -m pip install abacuslite ``` A minimal socket calculator setup is: @@ -211,8 +214,8 @@ The `present` list distinguishes computed properties from protocol padding, and fields may be added within v1; a breaking change requires a new schema version and a transition period in both ABACUS and abacuslite. -The ASE-side adapter contract is documented in the abacuslite repository: -https://github.com/QuantumMisaka/abacuslite/blob/main/docs/socket-contract.md +The ASE-side adapter contract is documented in the +[abacuslite socket contract](https://github.com/QuantumMisaka/abacuslite/blob/main/docs/socket-contract.md). Engine and adapter changes that alter this contract should be linked and tested together. @@ -223,7 +226,8 @@ In socket mode, ABACUS keeps one client process alive. All SCF evaluations produ The i-PI protocol does not transmit element symbols. `AbacusSocketIO` therefore sorts the internal socket atoms with the same first-occurrence species grouping used when writing `STRU`, and maps returned forces back to the original ASE `Atoms` order. This avoids silent force/atom mismatches when structures are read from CIF, extxyz, POSCAR, or other formats whose atom order is not already grouped for ABACUS. Users should not manually reorder atoms for socket I/O; pass the physical ASE `Atoms` object directly to the calculator. -A complete fixed-cell validation and benchmark example is available in `interfaces/ASE_interface/examples/socketio.py`. +A complete fixed-cell validation example is available in +[`abacuslite/examples/socketio.py`](https://github.com/QuantumMisaka/abacuslite/blob/main/examples/socketio.py). ## SPAP Analysis diff --git a/interfaces/ASE_interface/DEVELOP.md b/interfaces/ASE_interface/DEVELOP.md deleted file mode 100644 index acb1f1384b4..00000000000 --- a/interfaces/ASE_interface/DEVELOP.md +++ /dev/null @@ -1,90 +0,0 @@ -# Developer Guide - -Abacuslite has the following file structure: -``` -. -├── __init__.py -├── core.py -├── io -│ ├── __init__.py -│ ├── generalio.py -│ ├── latestio.py -│ ├── legacyio.py -│ └── testfiles -├── utils -│ ├── __init__.py -│ └── ksampling.py -└── xtest.sh -``` - -## core.py - -This file contains the implementation of the Atomic Simulation Environment (ASE) calculator. - -## io - -This directory contains the input/output (I/O) functions for extracting information from ABACUS dumped files. A summary is here: - -### Long-term-support (LTS) version - -|Item|Regular expression|Example| -|----|----|----| -|esolver_type|'The esolver type has been set to : (\S+)'|`The esolver type has been set to : ksdft_pw`| -|nspin|'nspin\s+=\s+(\d+)'|`nspin = 4`| -|number of bands|'NBANDS\s+=\s+(\d+)'|`NBANDS = 40`| -|number of atoms|'TOTAL ATOM NUMBER\s*=\s*(\d+)'|`TOTAL ATOM NUMBER = 2`| -|lattice constant|'lattice constant \(Angstrom\)\s*=\s*(-?\d+(\.\d+)?)'|`lattice constant (Angstrom) = 0.529177`| -|lattice vectors|'^Lattice vectors: \(Cartesian coordinate: in unit of a_0\)$'|` Lattice vectors: (Cartesian coordinate: in unit of a_0)`| -|coordinate system|'^(DIRECT\|CARTESIAN) COORDINATES'|`DIRECT COORDINATES`| -|atomic positions|'^tau[c\|d]_([A-Z][a-z]?)\d+\s+(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)*'|`taud_As1 0.2500000000 0.2500000000 0.2500000000 1.7321 0.0000000000 0.0000000000 0.0000000000`| -|eigenvalues|'\d+/\d+\s+kpoint\s+\(Cartesian\)\s+=\s+(-?\d(\.\d+)?(e-\d+)?)\s+(-?\d(\.\d+)?(e-\d+)?)\s+(-?\d(\.\d+)?(e-\d\|+)?)\s+\(\d+\s+pws\)'|` 1/1 kpoint (Cartesian) = 0.0000 0.0000 0.0000 (230 pws)`| -|atomic forces|'\s\*TOTAL\-FORCE\s\*\(eV\s*/Angstrom\)'|` TOTAL-FORCE (eV/Angstrom) `| -|total stress|'\s\*TOTAL\-STRESS\s\*\(KBAR\)'|` TOTAL-STRESS (KBAR) `| -|kpoints and weights|'\s\*(IBZ\|KPOINTS)\s+(DIRECT\|CARTESIAN)_X\s+(DIRECT\|CARTESIAN)_Y\s+(DIRECT\|CARTESIAN)_Z\s+WEIGHT(\s+ibz2bz)?'|` KPOINTS DIRECT_X DIRECT_Y DIRECT_Z WEIGHT`| -|energy|'\s*ENERGY\s+Rydberg\s+eV'|` Energy Rydberg eV `| -|total magnetism|'\s\*Total\sMagnetism\s\(uB\)(\s+x\s+y\s+z)?\s\*'|` Total Magnetism (uB) `| - -### Latest version - -|Item|Regular expression|Example|Notes| -|----|----|----|----| -|esolver_type|'#ENERGY SOLVER#\s+(\S+)'|` #ENERGY SOLVER# ksdft_pw`|| -|nspin|'nspin\s+=\s+(\d+)'|`nspin = 4`|| -|number of bands|||has been removed| -|number of atoms|'TOTAL ATOM NUMBER\s*=\s*(\d+)'|`TOTAL ATOM NUMBER = 2`|| -|lattice constant|'lattice constant \(Angstrom\)\s*=\s*(-?\d+(\.\d+)?)'|`lattice constant (Angstrom) = 0.529177`|| -|lattice vectors|'^Lattice vectors: \(Cartesian coordinate: in unit of a_0\)$'|` Lattice vectors: (Cartesian coordinate: in unit of a_0)`|| -|coordinate system|'^(DIRECT\|CARTESIAN) COORDINATES'|`DIRECT COORDINATES`|| -|atomic positions|'atom\s+x\s+y\s+z\s+mag‘|` atom x y z mag`|"tauc/taud" suffix has been removed, therefore the way to read the coordinates changes to find the head of the table, then read the following `number of atoms` lines. On the other hand, if the `calculation` is set to `md`, ABACUS will not dump the atomic positions to running log anymore, instead, will read from the MD_dump file| -|eigenvalues|'spin=(\d)\s+k-point=(\d+)/(\d+)\s+Cartesian=\s*(-?\d(\.\d+)?(e-\d+)?)\s+(-?\d(\.\d+)?(e-\d+)?)\s+(-?\d(\.\d\|+)?(e-\d+)?)\s+\(\d+\s+plane wave\)'| spin=1 k-point=1/1 Cartesian=0.0000000 0.0000000 0.0000000 (1837 plane wave)|eigenvalues information has been removed from the running log, the file istate.info is renamed as eig_occ.txt, where the eigenvalues are read| -|atomic forces|'#\s\*TOTAL\-FORCE\s*\(eV\s*/Angstrom\)\s\*#'|` #TOTAL-FORCE (eV/Angstrom)#`|| -|total stress|'#\s\*TOTAL\-STRESS\s*\(kbar\)\s\*#'|` #TOTAL-STRESS (kbar)# `|| -|kpoints and weights|'\s*(IBZ\|KPOINTS)\s+(DIRECT\|CARTESIAN)_X\s+(DIRECT\|CARTESIAN)_Y\s+(DIRECT\|CARTESIAN)_Z\s+WEIGHT(\s+ibz2bz)?'|` KPOINTS DIRECT_X DIRECT_Y DIRECT_Z WEIGHT`|| -|energy|'\s*ENERGY\s+Rydberg\s+eV'|` Energy Rydberg eV `|| -|total magnetism|'\s\*Total\sMagnetism\s\(uB\)(\s+x\s+y\s+z)?\s\*'|` Total Magnetism (uB) `|| - -Please look at detailed implementations in the following files. - -### generalio.py - -This module contains the general I/O functions for ABACUS. - -### latestio.py - -This module contains the I/O functions for the latest version of ABACUS. - -### legacyio.py - -This module contains the I/O functions for the Long-Term-Support (LTS) version of ABACUS. - -## utils - -This directory contains the utility modules for Abacuslite. - -### ksampling.py - -This module contains the wrapper of k-point sampling functions and helper functions. - -## xtest.sh - -This script can run all the unittests that programmed in all Python source files. \ No newline at end of file diff --git a/interfaces/ASE_interface/README.md b/interfaces/ASE_interface/README.md index d066747097e..4d903273cb8 100644 --- a/interfaces/ASE_interface/README.md +++ b/interfaces/ASE_interface/README.md @@ -1,92 +1,85 @@ -# abacuslite +# ABACUS ASE interface -## Introduction +[`abacuslite`](https://github.com/QuantumMisaka/abacuslite) is the +independently installed ASE calculator plugin for ABACUS. Install it from +PyPI: -abacuslite is a lightweight plugin for ABACUS (Atomic-orbital Based Ab-initio Computation at UStc), implementing the ASE (Atomic Simulation Environment) calculator interface. +```bash +python -m pip install abacuslite +``` + +The plugin owns the Python calculator, ABACUS input/output readers, examples +and user documentation. This directory contains only the engine-side +compatibility harness used by ABACUS CI; it does not ship a second copy of the +plugin. + +`abacuslite` is not vendored and is not a git submodule. Downloading the ABACUS +source archive therefore keeps its engine tree complete; the plugin is resolved +as a normal Python dependency. -### Key Features +## What this harness checks -- **Lightweight Design**: Implemented as a plugin, no need to modify ASE core code -- **Version Compatibility**: Supports ASE versions satisfying the package requirement `ase>=3.22` -- **ASE Integration**: Uses ASE as the running platform, making ABACUS a callable calculator within it -- **Function Support**: Provides SCF-based energy, force, and stress evaluations through ASE. ASE can use these evaluations for relaxation, molecular dynamics, NEB, band-structure, and density-of-states workflows. -- **Socket Support**: `AbacusSocketIO` provides fixed-cell i-PI socket calculations, with energy always available and forces/stress enabled independently when requested. +The harness builds one explicit ABACUS executable and runs the installed +`abacuslite` package against it. Required checks cover SCF, ASE relaxation and +MD, native MD, band structure, magnetic moments and fixed-cell socket handoff. +The develop lane uses the latest output format; the same runner accepts a +legacy-format executable for LTS compatibility. -## Installation +Every case runs in a separate Python process and preserves its raw ABACUS +output. The runner verifies that imports come from the installed distribution, +that runtime files match the supplied wheel, and that no required case was +skipped or omitted. -Install the plugin from the ASE interface directory: +## Run locally + +Build ABACUS, install a wheel in the Python environment that will run the +harness, and pass both explicitly: ```bash -cd interfaces/ASE_interface -pip install . +python -m pip install /path/to/abacuslite-*.whl + +python interfaces/ASE_interface/tests/run_engine_compatibility.py \ + --engine /path/to/abacus \ + --mpi-command 'mpirun -np 2' \ + --assets /path/to/abacus-develop/tests/PP_ORB \ + --format latest \ + --wheel /path/to/abacuslite-*.whl \ + --report-dir /tmp/abacus-ase-engine-tests \ + --engine-ref "$(git rev-parse HEAD)" ``` -The independent [abacuslite](https://pypi.org/project/abacuslite/1.1.0/) 1.1.0 -release can be installed in a separate environment. The engine compatibility -runner requires the same wheel file that was installed in that environment: +Use `--format legacy` with the supported 3.10.x executable. The LTS engine has +no socket driver; that is a declared capability difference, not a skipped +required case. + +For the offline harness contracts: ```bash -python -m pip download --only-binary=:all: --no-deps \ - --dest /tmp/abacuslite-wheel 'abacuslite==1.1.0' -python -m pip install /tmp/abacuslite-wheel/*.whl -python /path/to/abacus-develop/interfaces/ASE_interface/tests/run_engine_compatibility.py \ - --engine /absolute/path/to/abacus --mpi-command 'mpirun -np 2' \ - --assets /path/to/abacus-develop/tests/PP_ORB --format latest \ - --wheel /tmp/abacuslite-wheel/abacuslite-1.1.0-py3-none-any.whl \ - --report-dir /tmp/abacus-ase-engine-tests --engine-ref FULL_ENGINE_COMMIT_SHA +cd interfaces/ASE_interface/tests +python -m unittest test_external_provenance test_harness_facts \ + test_native_md_fixture test_portable_runner -v ``` -The runner copies the required scripts outside the checkout, verifies installed -runtime bytes against the supplied wheel, records the executable hash and -actual version/reader, and preserves every raw run and per-script log. It -rejects omitted or skipped required cases. The ordinary external CI lane pins -the published 1.1.0 wheel by SHA256 -`c3e4504e9c9f5bceb69f70c79290e7843037ca72f625c5f66258e14d110f0ff3`. -The bundled transition lane builds a wheel from this engine PR and runs the -same develop suite. Both develop lanes require all nine checks, including -fixed-cell socket and real native MD. Use `--format legacy` with an LTS -executable for eight interface checks plus the seeded initial-velocity case. -The LTS engine source has no bundled interface or socket driver; the shared -runner supplies its legacy-format oracle for plugin CI. -`workflow_dispatch` can select a reviewed plugin source ref instead of the -published release. Engine tests run only through this explicit runner or the -opt-in `xtest.sh`, not ordinary offline unit checks. - -## Usage Examples - -Please refer to the example scripts in the `examples` folder. Recommended learning path: - -1. **scf.py** - Basic SCF calculation example -2. **relax.py** - Atomic position relaxation calculation -3. **cellrelax.py** - Cell parameter relaxation calculation -4. **bandstructure.py** - Band structure calculation -5. **dos.py** - Density of states calculation -6. **md.py** - Molecular dynamics simulation -7. **constraintmd.py** - Constrained molecular dynamics simulation -8. **metadynamics.py** - Metadynamics simulation -9. **neb.py** - Nudged Elastic Band (NEB) calculation -10. **soc.py** - Noncollinear spin-orbit coupling calculation -11. **socketio.py** - Fixed-cell ASE optimization with `AbacusSocketIO`, running ABACUS as an i-PI socket client - -The regular `Abacus` calculator runs one ABACUS calculation for each ASE property evaluation. ASE controls the relaxation, molecular-dynamics, and other workflow steps. The socket calculator reuses one ABACUS process for position updates, while the cell and electronic-structure settings remain fixed for that calculator instance. - -## Authors - -- Yuyang Ji -- Zhenxiong Shen -- Yike Huang -- Zhaoqing Liu - -## Acknowledgments - -Thanks to the ABACUS development team for their support and contributions. +## CI and cross-repository changes -## License +The ASE workflow installs the published, tested `abacuslite` wheel (currently +1.1.0) by default. A manual `workflow_dispatch` run can build a reviewed +`abacuslite` ref for a coordinated change. The `abacuslite` repository +separately builds pinned ABACUS develop/LTS snapshots and runs the same engine +harness on a schedule. -The applicable license terms are provided in the repository [LICENSE](../../LICENSE). +ABACUS owns the engine, output formats and socket wire protocol. `abacuslite` +owns the Python adapter and parser. Parser or calculator fixes belong in +`abacuslite`; engine format changes belong here and require a linked adapter +change. -## Contact +The socket metadata schema is `abacus.socket.properties.v1`. Additive fields +may be added within v1. A breaking change requires a new schema version and a +transition period in both repositories. See +[the socket contract](https://github.com/QuantumMisaka/abacuslite/blob/main/docs/socket-contract.md) +and [ABACUS ASE documentation](../../docs/advanced/interface/ase.md). -If you have any questions or suggestions, please contact us through: +## License -- GitHub: [deepmodeling/abacus-develop](https://github.com/deepmodeling/abacus-develop) +The applicable license terms are provided in the repository +[LICENSE](../../LICENSE). diff --git a/interfaces/ASE_interface/abacuslite/__init__.py b/interfaces/ASE_interface/abacuslite/__init__.py deleted file mode 100644 index fe361f81bad..00000000000 --- a/interfaces/ASE_interface/abacuslite/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -''' -interfaces to Atomic-orbital Based Ab-initio Computation at UStc (ABACUS), -for more information about this DFT calculator, -please refer to the Github official repository: -https://github.com/deepmodeling/abacus-develop -and online-manual: -https://abacus.deepmodeling.com/en/latest/index.html - -For a more complete ABACUS pre-/post-processing workflow package, -please refer to the ABACUSTest package: -https://github.com/pxlxingliang/abacus-test -''' -from .core import * \ No newline at end of file diff --git a/interfaces/ASE_interface/abacuslite/core.py b/interfaces/ASE_interface/abacuslite/core.py deleted file mode 100644 index 9d23159ea25..00000000000 --- a/interfaces/ASE_interface/abacuslite/core.py +++ /dev/null @@ -1,1262 +0,0 @@ -# fmt: off - -''' -here the ase-abacus implementation is pasted and modified. -Source: -https://gitlab.com/1041176461/ase-abacus/-/blob/master/ase/calculators/abacus.py - -This module defines an ASE interface to ABACUS. -Created on Fri Jun 8 16:33:38 2018 - -ABACUS (Atomic-orbital Based Ab-initio Computation at UStc) is an open-source -package based on density functional theory (DFT). The package utilizes both plane -wave and numerical atomic basis sets with the usage of pseudopotentials to describe -the interactions between nuclear ions and valence electrons. ABACUS supports LDA, -GGA, meta-GGA, and hybrid functionals. Apart from single-point calculations, -the package allows geometry optimizations and ab-initio molecular dynamics with -various ensembles. The package also provides a variety of advanced functionalities -for simulating materials, including the DFT+U, VdW corrections, and implicit solvation -model, etc. In addition, ABACUS strives to provide a general infrastructure to -facilitate the developments and applications of novel machine-learning-assisted -DFT methods (DeePKS, DP-GEN, DeepH, DeePTB etc.) in molecular and material simulations. - -Modified on Wed Jun 20 15:00:00 2018 -@author: Shen Zhen-Xiong - -Modified on Wed Jun 03 23:00:00 2022 -@author: Ji Yu-yang - -Refactored from Sun Dec 07 21:41 2025 -@author: Huang Yi-ke -''' - -import json -import os -import re -import shutil -import tempfile -import unittest -from pathlib import Path -from typing import Dict, Optional, List - -import numpy as np -from ase.calculators.genericfileio import ( - BaseProfile, - CalculatorTemplate, - GenericFileIOCalculator, - read_stdout -) -from ase.calculators.socketio import SocketIOCalculator -from ase.atoms import Atoms -from ase.dft.kpoints import BandPath -from ase.io import read - -from abacuslite.io.generalio import ( - file_safe_backup, - read_input, - read_stru, - read_kpt, - species_group_indices, - write_input, - write_stru, - write_kpt -) - -__LEGACYIO__ = True -def switch_io_backend_version(version: str) -> bool: - '''determine if the i/o is in legacy format by the version number, - for detailed discussion, see issue #7260 - ''' - global __LEGACYIO__ - m = re.match(r'^v(\d+)\.(\d+)\.(\d+)(\.\d+|\-(alpha|beta|rc)\.\d+|\-(alpha|beta|rc)\d+)?$', version) - assert m, f'Invalid format of version number, please check file version.h' - assert int(m.group(1)) >= 3, f'ABACUS v2.x is not supported' - if int(m.group(2)) >= 11: - __LEGACYIO__ = False - elif int(m.group(2)) == 9: - if m.group(4) is not None: - # it is also possible to divide the version more carefully, - # but because 3.9.0.x are all on the develop branch, up to - # now, there is no user submit issue to request such a careful - # division - __LEGACYIO__ = False - else: - __LEGACYIO__ = True - return __LEGACYIO__ - -class AbacusProfile(BaseProfile): - '''AbacusProfile for interacting the ASE with ABACUS that installed in - the practical system''' - configvars = {'pseudo_dir', 'orbital_dir'} - - def __init__(self, - command: str, - pseudo_dir: Optional[str | Path] = None, - orbital_dir: Optional[str | Path] = None, - omp_num_threads: Optional[int] = None, - **kwargs): - '''Initialize ABACUS profile. - - Parameters - ---------- - command : str - The command to run ABACUS. NOTE: there may be the case for some - sophisticated ABACUS user they call ABACUS with command like - `OMP_NUM_THREADS=1 mpirun -np X abacus`. Here please do not set - the number of omp threads in `command`, instead, use `nomp=1`. - pseudo_dir : str or Path, optional - The directory containing pseudopotential files. - orbital_dir : str or Path, optional - The directory containing orbital basis files. This is only necessary - for an ABACUS-LCAO calculation - omp_num_threads : int, optional - The number of omp threads to use. - ''' - assert isinstance(command, str) - # further validation on the command will be in the __init__ of - # the base class - super().__init__(command, **kwargs) - self.pseudo_dir = pseudo_dir - self.orbital_dir = orbital_dir - - if omp_num_threads is not None: - # set the number of omp threads for the present process - assert isinstance(omp_num_threads, int) - os.environ['OMP_NUM_THREADS'] = str(omp_num_threads) - - @staticmethod - def parse_version(stdout) -> str: - # MPI launchers may add informational lines before ABACUS output. - match = re.search(r'ABACUS version (\S+)', stdout or '') - if match is None: - raise RuntimeError( - 'Could not parse ABACUS version from command output. ' - 'Expected a line like "ABACUS version vX.Y.Z".' - ) - return match.group(1) - - def get_calculator_command(self, inputfile) -> List[str]: - # because ABACUS run in the folder where there are INPUT files, so the - # additional inputfile argument is not used. - return [] - - def socketio_argv_inet(self, port: Optional[int] = None) -> List[str]: - port = 31415 if port is None else port - return [ - 'env', - f'ABACUS_SOCKET_ADDRESS=localhost:{port}', - *self._split_command, - ] - - def socketio_argv_unix(self, socket: str) -> List[str]: - return [ - 'env', - f'ABACUS_SOCKET_ADDRESS=/tmp/ipi_{socket}:UNIX', - *self._split_command, - ] - - def version(self) -> str: - '''get the abacus version information''' - cmd_ = [*self._split_command, '--version'] - return AbacusProfile.parse_version(read_stdout(cmd_)) - -class AbacusTemplate(CalculatorTemplate): - - implemented_properties = [ - 'energy', 'forces', 'stress', 'free_energy', 'magmom' - ] - _label = 'abacus' - - def __init__(self): - super().__init__( - 'abacus', - self.implemented_properties - ) - self.non_convergence_ok = False - # the redirect stdout and stderr - self.inputname = 'INPUT' # hard-coded - self.outputname = f'{self._label}.out' - self.errorname = f'{self._label}.err' - - # fix: inconsistent atoms order may induce bugs, here a list - # is kept to swap the order of atoms - self.atomorder = None - - '''because it may be not one-to-one mapping between the property - desired to calculate and the keywords used in the calculation, - in the following a series of functions for mapping the property - calculation to the keywords settings are implemented''' - @staticmethod - def get_energy_keywords(self) -> Dict[str, str]: - return {} - - @staticmethod - def get_forces_keywords(self) -> Dict[str, str]: - return {'cal_force': '1'} - - @staticmethod - def get_stress_keywords(self) -> Dict[str, str]: - return {'cal_stress': '1'} - - @staticmethod - def get_free_energy_keywords(self) -> Dict[str, str]: - return {} - - @staticmethod - def get_magmom_keywords(self) -> Dict[str, str]: - return {'nspin': '2'} - - def get_property_keywords(self, - parameters: Dict[str, str], - properties: List[str]) -> Dict[str, str]: - '''Connect the relationship between the properties calculation and - the ABACUS keywords. May be more complicated in the future, therefore - it is better to have a separate mapping function instead of - implementing in some other functions. - - Parameters - ---------- - parameters : dict - The parameters used to perform the calculation. - properties : list of str - The list of properties to calculate - ''' - def keyword_compare_value(value): - if isinstance(value, bool): - return '1' if value else '0' - if isinstance(value, (list, tuple, set)): - return ' '.join(str(i) for i in value) - return str(value) - - param_cache_ = { - key: keyword_compare_value(value) - for key, value in parameters.items() - if value is not None - } - - def counter(param_new: Dict[str, str]) -> Dict[str, str]: - info = 'desired properties or explicit parameters required contradictory keywords' - staged = {} - for k, v in param_new.items(): - if v is None: - continue - normalized_value = keyword_compare_value(v) - if k in param_cache_ and param_cache_[k] != normalized_value: - raise ValueError(f'{info}: {k}={v} (now), {param_cache_[k]} (before)') - staged[k] = normalized_value - param_cache_.update(staged) - return param_new - - # update the parameters with the keywords for the properties - for p in properties: - assert p in self.implemented_properties - parameters.update(counter(getattr(self, f'get_{p}_keywords')(parameters))) - - # from the parameters, get the file path - self.suffix = parameters.get('suffix', 'ABACUS') - self.calculation = parameters.get('calculation', 'scf') - # with the above two, the running log file can be positioned. - return parameters - - def write_input(self, - profile: AbacusProfile, - directory: Path | str, - atoms: Atoms, - parameters: Dict[str, str], - properties: List[str]) -> None: - '''Write the input files for the calculation. This function connects - the calculation in ASE language (atoms, properties, assisted by the - parameters) to the input files of ABACUS. - - Parameters - ---------- - profile : AbacusProfile - The profile used to perform the calculation. - directory : Path - The working directory to store the input files. - atoms : Atoms - The atoms object to perform the calculation on. Because - parameters: dict - The parameters used to perform the calculation. - properties: list of str - The list of properties to calculate - ''' - # directory - directory = Path(directory) - directory.mkdir(exist_ok=True, parents=True) - - # copy the `parameters` because later we will modify it - parameters = parameters.copy() - - # STRU - _ = file_safe_backup(directory / parameters.get('stru_file', 'STRU')) - # group atoms by first-occurrence species order. Keep the reverse map - # so that we will recover the order in function read_results() - ind = species_group_indices(atoms.get_chemical_symbols()) - self.atomorder = sorted(range(len(atoms)), key=lambda i: ind[i]) # revmap - # then we write - _ = write_stru(atoms[ind], - outdir=directory, - pp_file=parameters.get('pseudopotentials'), - orb_file=parameters.get('basissets'), - fname=parameters.get('stru_file', 'STRU')) - - # KPT, if needed - if 'kpts' in parameters: - _ = file_safe_backup(directory / parameters.get('kpoint_file', 'KPT')) - _ = write_kpt(parameters['kpts'], - directory / parameters.get('kpoint_file', 'KPT')) - # should this function be responsible for checking the integrity - # of information provided by the user? There may be the case that - # user provides incomplete information, such that the ABACUS cannot - # run with parameters. - - # INPUT - # after writing the KPT and STRU, delete them from the parameters - _ = parameters.pop('kpts', None) - - _ = parameters.pop('pseudopotentials', None) - parameters.update({'pseudo_dir': profile.pseudo_dir}) - - _ = parameters.pop('basissets', None) - parameters.update({'orbital_dir': profile.orbital_dir}) - # update the parameters respect to the properties desired - parameters = self.get_property_keywords(parameters, properties) - # postprocess on the parameters: convert the key and values - # from any to string. For the case where the value is a - # array, convert to the string spaced by whitespace - for k, v in parameters.items(): - # if the v is iterable, convert to the string spaced by whitespace - if isinstance(v, (list, tuple, set)): - parameters[k] = ' '.join(str(i) for i in v) - dst = directory / self.inputname - _ = file_safe_backup(dst) - # remove possible key-value pairs whose value is None - parameters = {k: v for k, v in parameters.items() if v is not None} - - # FIXME: only support the ksdft esolver_type presently - if parameters.get('esolver_type', 'ksdft') != 'ksdft': - raise NotImplementedError( - 'ABACUS Lite only supports the ksdft esolver_type presently, ' - 'which means the ABACUS should always be used as a DFT ' - 'calculator. For other forcefields that ABACUS supports ' - 'such as the LJ, DP, etc., please either use the ABACUS ' - 'directly, or the implementation of interfaces to ASE ' - 'directly.' - ) - - # write the INPUT file to the target directory - _ = write_input(parameters, dst) - - def execute(self, - directory: Path | str, - profile: AbacusProfile): - '''Execute the ABACUS Lite calculation. - - Parameters - ---------- - directory : Path or str - The working directory to store the input files. - profile : AbacusProfile - The profile used to perform the calculation. - - Raises - ------ - SubprocessError - If the ABACUS Lite calculation fails. - ''' - from subprocess import SubprocessError - try: - profile.run(directory=directory, - inputfile=None, - outputfile=self.outputname, - errorfile=self.errorname) - except SubprocessError: - # Surface only the tails of each log: abacus.out already contains - # the SCF iteration table, so full stdout/stderr plus 200 lines of - # running log would make the exception message unreadably long. - # The run dir is preserved separately by the test harness - # (ASE_ABACUS_KEEP_DIR) when post-mortem of the full logs is - # needed. - def tail(p: Path, n: int) -> str: - if not p.exists(): - return '' - lines = p.read_text(errors='replace').splitlines() - header = f'=== {p.name} (last {min(n, len(lines))} lines) ===' - return header + '\n' + '\n'.join(lines[-n:]) - - directory = Path(directory) - message = ['ABACUS Lite calculation failed'] - for fn, n in ((self.outputname, 200), - (self.errorname, 200)): - chunk = tail(directory / fn, n) - if chunk: - message.append(chunk) - # The tail of the SCF running log holds the per-step drho - # history, usually the key evidence for issues like #7794 - # ("Can't find even an electron"). - scf_log = (directory / f'OUT.{self.suffix}' - / f'running_{self.calculation}.log') - chunk = tail(scf_log, 100) - if chunk: - message.append(chunk) - raise SubprocessError('\n'.join(message)) - - def read_results(self, directory) -> Dict: - '''the function that returns the desired properties in dict''' - read_abacus_out = lambda fn: None - global __LEGACYIO__ - if __LEGACYIO__: - from abacuslite.io.legacyio import read_abacus_out - else: - from abacuslite.io.latestio import read_abacus_out - - outdir = directory / f'OUT.{self.suffix}' - # only the last frame - atoms: Optional[Atoms] = read_abacus_out( - outdir / f'running_{self.calculation}.log', - sort_atoms_with=self.atomorder)[-1] - assert atoms is not None - - return dict(atoms.calc.properties()) - - def load_profile(self, cfg, **kwargs): - return AbacusProfile.from_config(cfg, self.name, **kwargs) - -class Abacus(GenericFileIOCalculator): - def __init__(self, - profile=None, - directory='.', - **kwargs): - '''Construct the ABACUS calculator. - - The keyword arguments (kwargs) can be one of the ASE standard - keywords: 'xc', 'kpts' or any of ABACUS' - native keywords. - - Parameters - ---------- - profile: AbacusProfile - the interface that interacts with the ABACUS executable. - directory: str or Path - the working directory to store the input files. - pseudopotentials: dict - A mapping from the element to the pseudopotential file name, - e.g. ``{'O': 'O_ONCV_PBE-1.0.upf', 'H': 'H.upf'}``. - baisssets: dict, optional - A mapping from the element to the ABACUS numerical atomic - orbital file name. This is necessary only when it is an - ABACUS-LCAO (Linear-Combination-of-Atomic-Orbitals) calculation - e.g. ``{'O': 'O_gga_10au_100Ry_2s2p1d.orb', - 'H': 'H_gga_10au_100Ry_2s1p.orb'}``. - kpts: dict - The k-points sampling should be given as a dict. For there - are many modes of k-sampling supported, the content may differ - in cases. A `mode` key should be used to specify the ksampling, - allowed modes are: `mp-sampling`, `line` and `point`. For - `mp-sampling` mode, `gamma-centered`, `nk` and `kshift` should - present. `gamma-centered` is a boolean, `nk` and `kshift` should - be lists of three integers. ... TBD - inp: dict - parameters setting in INPUT of ABACUS. NOTE: if there are settings - on the `pseudo_dir` and `orbital_dir`, these will overwrite the - value in the profile. If you do not expect this, please only use - the profile, because the profile stands for interfacing with the - ASE calculator instance with the computational environment. - - **kwargs: - Other parameters to be passed to the ABACUS calculator. - ''' - # not recommended :( - profile = AbacusProfile('abacus') if profile is None else profile - - # to be compatible with both the legacy and latest format of i/o, the - # switch is needed. - _ = switch_io_backend_version(profile.version()) - - # because ABACUS run job in folders, based on the assumption that - # there is only one job in the folder. Therefore once there are already - # files in the folder, will try to create a new one...(seriously?) - inp = kwargs.pop('inp', {}) - - super().__init__( - template=AbacusTemplate(), - profile=profile, - parameters=kwargs | inp, - directory=directory, - ) - - def write_input(self, atoms, properties=None, system_changes=None): - if properties is None: - properties = self.template.implemented_properties - self.template.write_input( - profile=self.profile, - directory=Path(self.directory), - atoms=atoms, - parameters=self.parameters, - properties=properties, - ) - - @classmethod - def restart(cls, profile=None, directory='.', **kwargs): - '''instantiate one ABACUS calculator from an existing job directory, - optionally overwrite some keywords''' - directory = Path(directory) - inp_read = read_input(directory / 'INPUT') - - pporb_read = read_stru(directory / 'STRU')['species'] - pseudopotentials = kwargs.get( - 'pseudopotentials', - {pporb['symbol']: pporb['pp_file'] for pporb in pporb_read} - ) - if 'pseudopotentials' in kwargs: - del kwargs['pseudopotentials'] - - basissets = kwargs.get( - 'basissets', - {pporb['symbol']: pporb.get('orb_file') for pporb in pporb_read} - ) - if 'basissets' in kwargs: - del kwargs['basissets'] - if all([forb is None for forb in basissets.values()]): - basissets = {} - assert all([forb is not None for forb in basissets.values()]) - - kpts = kwargs.get('kpts', read_kpt(directory / inp_read.get('kpoint_file', 'KPT'))) - if 'kpts' in kwargs: - del kwargs['kpts'] - - inp = inp_read | kwargs.get('inp', {}) - if 'inp' in kwargs: - del kwargs['inp'] - - return cls(profile=profile, - directory=directory, - pseudopotentials=pseudopotentials, - basissets=basissets, - kpts=kpts, - inp=inp, - **kwargs) - - def fixed_density(self, - kpts: BandPath | Dict[str, str | int | List[float]], - symmetry: str = 'off', - profile=None, - **kwargs) -> 'Abacus': - '''spawn a new ABACUS calculator with fixed density, based on the present - instance. This funcionality is mostly only useful when perform the - non-self-consistent calculations like band structure. - This interface is referred from the ASE document at: - https://ase-lib.org/gettingstarted/tut04_bulk/bulk.html#band-structure - , however, we also note that it is from the implementation of the - GPAW python, not the ASE official. - To make less development burden as possible, we use the same interface - as the GPAW python. - - Parameters - ---------- - kpts : BandPath | Dict[str, str | int | List[float]] - The k-point path to be calculated. Can be either a BandPath object - or a dictionary that contains the k-point information. For the latter - case, see tbgen/calculators/abacus/generalio.py::write_kpt for more - details. - symmetry : str, optional - The symmetry mode to be used. Default is 'off'. Now only the `off` - mode is supported. - profile : AbacusProfile, optional - The profile to be used. Default is None. If None, the profile of - the present instance will be used. - **kwargs : dict - Other parameters to be passed to the ABACUS calculator. - - Returns - ------- - Abacus - The new ABACUS calculator instance that can perform the nscf calculation - tasks - ''' - # we should overwrite the 'calculation' to 'nscf', and 'init_chg' to 'file' - assert symmetry == 'off' - - kwargs.setdefault('inp', {}).update({'calculation': 'nscf', - 'init_chg': 'file', - 'symmetry': 0, - 'out_band': 1, - 'kspacing': 0.0, # overwrite - 'gamma_only': False, - 'read_file_dir': 'OUT.ABACUS'}) # overwrite - - profile = self.profile if profile is None else profile - - # get the kpoint coordinates - if isinstance(kpts, BandPath): - kwargs['kpts'] = { - 'mode': 'point', - 'nk': len(kpts.kpts), - 'nkinterpl': np.ones(len(kpts.kpts), dtype=int).tolist(), - 'coordinate': 'direct', - 'kpoints': kpts.kpts.tolist(), - } - else: - assert isinstance(kpts, dict) - kwargs['kpts'] = kpts - - # return - return Abacus.restart(profile=profile, - directory=self.directory, - **kwargs) - - def band_structure(self, efermi=None): - '''get the band structure from ABACUS. - (now not only GPAW can calculate the band structure ;) )''' - from ase.spectrum.band_structure import get_band_structure - return get_band_structure(calc=self, reference=efermi) - -class AbacusSocketIO(SocketIOCalculator): - """ASE socket I/O calculator that launches ABACUS as an i-PI client. - - A socket calculator owns one ABACUS process with one fixed INPUT/STRU - setup. The i-PI protocol can update positions, but electronic-structure - parameters such as k-points, spin, basis, pseudopotentials, and species - require a new calculator instance. Energy, forces, and stress are - independently controlled by ABACUS INPUT. The fixed-layout i-PI response - uses zero padding for absent fields and an extras metadata record so - padding is never exposed as a computed property. - """ - - def __init__(self, - profile=None, - directory='.', - port=None, - unixsocket=None, - timeout=None, - log=None, - **kwargs): - inp = dict(kwargs.pop('inp', {})) - self._property_constraints = {} - for keyword, property_name in (('cal_force', 'forces'), - ('cal_stress', 'stress')): - if keyword in inp: - self._property_constraints[property_name] = self._input_bool( - inp[keyword], keyword) - self.implemented_properties = [ - 'energy', 'free_energy', 'forces', 'stress'] - self._active_properties = None - self._last_socket_metadata = None - self.last_scf_converged = None - inp = self._socket_inp(inp) - self.abacus = Abacus( - profile=profile, - directory=directory, - inp=inp, - **kwargs, - ) - self._reference_cell = None - super().__init__( - port=port, - unixsocket=unixsocket, - timeout=timeout, - log=log, - launch_client=self._launch_client, - ) - - def calculate(self, atoms=None, properties=None, system_changes=None): - from ase.calculators.calculator import ( - PropertyNotImplementedError, - all_changes, - ) - from ase.stress import full_3x3_to_voigt_6_stress - - # A failed new request must not expose results or convergence from the - # previous geometry. Publish only a completely validated response. - self.results = {} - self.last_scf_converged = None - self._last_socket_metadata = None - if system_changes is None: - system_changes = all_changes - if atoms is None: - atoms = self.atoms - if atoms is None: - raise ValueError('AbacusSocketIO.calculate requires atoms') - - requested = self._normalize_socket_properties(properties) - self._check_requested_properties(requested) - - bad = [change for change in system_changes - if change not in self.supported_changes] - if self.atoms is not None and any(bad): - raise PropertyNotImplementedError( - 'Cannot change {} through IPI protocol. ' - 'Please create new socket calculator.' - .format(bad if len(bad) > 1 else bad[0])) - - desired = set(requested) - desired.discard('free_energy') - desired.add('energy') - for property_name, enabled in self._property_constraints.items(): - if enabled: - desired.add(property_name) - active = set(self._active_properties or ()) - if not active: - active.update(desired) - elif not desired.issubset(active): - active.update(desired) - if self.server is not None: - self._close_socket_session() - self._active_properties = tuple( - name for name in ('energy', 'forces', 'stress') if name in active) - - self._check_fixed_cell(atoms) - order = self._socket_sort_indices(atoms) - socket_atoms = atoms[order] - self.atoms = atoms.copy() - - if self.server is None: - self.server = self.launch_server() - proc = self.launch_client(socket_atoms, list(self._active_properties), - port=self._port, - unixsocket=self._unixsocket) - self.server.proc = proc - - raw_results = self.server.calculate(socket_atoms) - if not isinstance(raw_results, dict): - raise ValueError('ABACUS socket server returned a non-mapping result') - results = dict(raw_results) - metadata = self._decode_socket_metadata(results.pop('morebytes', None)) - if metadata is None: - if set(self._active_properties) != {'energy'}: - raise ValueError( - 'ABACUS socket response omitted property metadata; refusing ' - 'to infer forces or stress from fixed-wire padding') - present = {'energy'} - converged = None - else: - present = set(metadata['present']) - converged = metadata['scf_converged'] - - if 'energy' not in present or 'energy' not in results: - raise ValueError('ABACUS socket response did not provide energy') - energy = float(results['energy']) - if not np.isfinite(energy): - raise ValueError('ABACUS socket energy is not finite') - free_energy = float(results.get('free_energy', energy)) - if not np.isfinite(free_energy): - raise ValueError('ABACUS socket free energy is not finite') - current = {'energy': energy, 'free_energy': free_energy} - - if 'forces' in present: - if 'forces' not in results: - raise ValueError( - 'ABACUS socket metadata advertises forces, but wire response omitted them') - forces = np.asarray(results['forces'], dtype=np.float64) - expected_shape = (len(socket_atoms), 3) - if forces.shape != expected_shape or not np.all(np.isfinite(forces)): - raise ValueError('ABACUS socket forces have invalid shape or values') - current['forces'] = self._forces_to_input_order(forces, order) - - if 'stress' in present: - virial = results.get('virial') - if virial is None: - raise ValueError( - 'ABACUS socket metadata advertises stress, but wire response omitted virial') - if self.atoms.cell.rank != 3 or not any(self.atoms.pbc): - raise PropertyNotImplementedError( - 'ABACUS socket stress requires a periodic rank-3 cell') - virial = np.asarray(virial, dtype=np.float64) - if virial.shape != (3, 3) or not np.all(np.isfinite(virial)): - raise ValueError('ABACUS socket virial is not a finite 3x3 matrix') - vol = float(atoms.get_volume()) - if not np.isfinite(vol) or vol <= 0.0: - raise ValueError('ABACUS socket stress requires a positive cell volume') - current['stress'] = -full_3x3_to_voigt_6_stress(virial) / vol - - missing = [name for name in requested if name not in current] - if missing: - raise PropertyNotImplementedError( - 'ABACUS socket response did not provide requested {}'.format( - ', '.join(missing))) - self.results = current - self._last_socket_metadata = metadata - self.last_scf_converged = converged - - def _check_fixed_cell(self, atoms): - from ase.calculators.calculator import PropertyNotImplementedError - - cell = atoms.cell.array.copy() - if self._reference_cell is None: - self._reference_cell = cell - return - max_delta = np.max(np.abs(cell - self._reference_cell)) - if max_delta > 1.0e-10: - raise PropertyNotImplementedError( - 'AbacusSocketIO is fixed-cell only; create a new socket ' - 'calculator for a changed cell, or use the normal Abacus ' - 'FileIO calculator for variable-cell workflows.' - ) - - def set(self, **kwargs): - if kwargs: - raise ValueError( - 'AbacusSocketIO input parameters are fixed after construction; ' - 'create a new AbacusSocketIO calculator to change k-points, ' - 'spin, basis, pseudopotentials, species, or other INPUT/STRU ' - 'settings.' - ) - return super().set(**kwargs) - - def _check_requested_properties(self, requested): - from ase.calculators.calculator import PropertyNotImplementedError - - constraints = self._property_constraints - keywords = {'forces': 'cal_force', 'stress': 'cal_stress'} - for property_name, keyword in keywords.items(): - if property_name in requested and constraints.get(property_name) is False: - raise PropertyNotImplementedError( - '{}=0 disables requested {}'.format(keyword, property_name)) - - @staticmethod - def _normalize_socket_properties(properties): - from ase.calculators.calculator import PropertyNotImplementedError - - if properties is None: - names = ['energy'] - elif isinstance(properties, str): - names = [properties] - else: - names = list(properties) - if not names: - names = ['energy'] - allowed = {'energy', 'free_energy', 'forces', 'stress'} - unknown = [name for name in names if name not in allowed] - if unknown: - raise PropertyNotImplementedError( - 'ABACUS socket does not implement {}'.format(', '.join(unknown))) - return tuple(dict.fromkeys(names)) - - def _close_socket_session(self): - server = getattr(self, 'server', None) - if server is not None: - close = getattr(server, 'close', None) - if callable(close): - close() - self.server = None - self.results = {} - - @staticmethod - def _decode_socket_metadata(raw): - if raw is None: - return None - if isinstance(raw, str): - payload = raw.encode('utf-8') - elif isinstance(raw, (bytes, bytearray, memoryview)): - payload = bytes(raw) - else: - payload = np.asarray(raw, dtype=np.uint8).tobytes() - if not payload: - return None - try: - metadata = json.loads(payload.decode('utf-8')) - except (UnicodeDecodeError, ValueError) as error: - raise ValueError('ABACUS socket extras are not valid UTF-8 JSON') from error - if not isinstance(metadata, dict): - raise ValueError('ABACUS socket extras must be a JSON object') - if metadata.get('schema') != 'abacus.socket.properties.v1': - raise ValueError('unsupported ABACUS socket extras schema') - present = metadata.get('present') - if not isinstance(present, list): - raise ValueError('ABACUS socket extras present must be a list') - allowed = {'energy', 'forces', 'stress'} - if any(not isinstance(name, str) or name not in allowed for name in present): - raise ValueError('ABACUS socket extras contain an unknown property') - if 'energy' not in present: - raise ValueError('ABACUS socket extras must include energy') - scf_converged = metadata.get('scf_converged') - if not isinstance(scf_converged, bool): - raise ValueError('ABACUS socket extras scf_converged must be Boolean') - return { - 'present': tuple(dict.fromkeys(present)), - 'scf_converged': scf_converged, - } - - def _launch_client(self, atoms, properties=None, port=None, unixsocket=None): - from subprocess import Popen - - if properties is None: - properties = list(self._active_properties or ('energy',)) - properties = set(properties) - properties.discard('free_energy') - properties.add('energy') - # The i-PI response has fixed force/virial fields, but ABACUS must be - # told explicitly which expensive quantities to evaluate. Keep these - # switches synchronized with the session mask before writing INPUT. - self.abacus.parameters['cal_force'] = int('forces' in properties) - self.abacus.parameters['cal_stress'] = int('stress' in properties) - properties = [name for name in ('energy', 'forces', 'stress') - if name in properties] - - directory = Path(self.abacus.directory) - directory.mkdir(exist_ok=True, parents=True) - - if hasattr(self.abacus, 'write_inputfiles'): - self.abacus.write_inputfiles(atoms, properties) - else: - self.abacus.write_input(atoms, properties=properties) - - if unixsocket is not None: - argv = self.abacus.profile.socketio_argv_unix(socket=unixsocket) - else: - argv = self.abacus.profile.socketio_argv_inet(port=port) - - stdout = open(directory / self.abacus.template.outputname, 'w') - stderr = open(directory / self.abacus.template.errorname, 'w') - try: - return Popen(argv, cwd=directory, env=os.environ, - stdout=stdout, stderr=stderr) - finally: - stdout.close() - stderr.close() - - @staticmethod - def _socket_inp(inp): - inp = dict(inp) - calculation = inp.get('calculation', 'scf') - if calculation != 'scf': - raise ValueError('ABACUS socket I/O requires calculation="scf"') - for keyword in ('cal_force', 'cal_stress'): - if keyword in inp: - inp[keyword] = int(AbacusSocketIO._input_bool(inp[keyword], keyword)) - inp.update({ - 'calculation': 'scf', - 'socket_driver': 1, - }) - return inp - - @staticmethod - def _input_bool(value, name): - if isinstance(value, bool): - return value - if isinstance(value, int) and value in (0, 1): - return bool(value) - if isinstance(value, str): - normalized = value.strip().lower() - if normalized in ('true', '1'): - return True - if normalized in ('false', '0'): - return False - raise ValueError('{} must be one of true, false, 1, or 0'.format(name)) - - @staticmethod - def _socket_sort_indices(atoms): - return species_group_indices(atoms.get_chemical_symbols()) - - @staticmethod - def _forces_to_input_order(forces, order): - reordered = np.empty_like(forces) - for sorted_index, original_index in enumerate(order): - reordered[original_index] = forces[sorted_index] - return reordered - - -class TestAbacusCalculator(unittest.TestCase): - - here = Path(__file__).parent - pporb = here.parent.parent.parent / 'tests' / 'PP_ORB' - - def test_socketio_species_order_mapping(self): - atoms = Atoms(symbols=['Si', 'O', 'C', 'Si', 'O', 'C']) - order = AbacusSocketIO._socket_sort_indices(atoms) - self.assertEqual(order, [0, 3, 1, 4, 2, 5]) - - socket_forces = np.arange(18).reshape(6, 3) - input_forces = AbacusSocketIO._forces_to_input_order( - socket_forces, order) - - expected = np.empty_like(socket_forces) - for sorted_index, original_index in enumerate(order): - expected[original_index] = socket_forces[sorted_index] - np.testing.assert_array_equal(input_forces, expected) - - def test_socketio_rejects_parameter_changes(self): - calc = object.__new__(AbacusSocketIO) - with self.assertRaisesRegex(ValueError, 'fixed after construction'): - calc.set(kpts={'mode': 'mp-sampling', 'nk': [2, 2, 2]}) - - def test_socketio_rejects_cell_changes(self): - from ase.calculators.calculator import PropertyNotImplementedError - - calc = object.__new__(AbacusSocketIO) - calc.atoms = Atoms('Si', cell=[5.0, 5.0, 5.0], pbc=True) - calc._reference_cell = calc.atoms.cell.array.copy() - - changed = calc.atoms.copy() - changed.cell[0, 0] = 5.1 - with self.assertRaisesRegex(PropertyNotImplementedError, 'fixed-cell'): - calc._check_fixed_cell(changed) - - def test_socketio_input_keeps_independent_property_switches(self): - self.assertEqual( - AbacusSocketIO._socket_inp({'cal_force': 0, 'cal_stress': 1}), - {'calculation': 'scf', 'socket_driver': 1, - 'cal_force': 0, 'cal_stress': 1}) - - def test_socketio_boolean_parser_rejects_ambiguous_values(self): - for value in ('yes', 'no', 'on', 'off', ''): - with self.assertRaisesRegex(ValueError, 'cal_force'): - AbacusSocketIO._input_bool(value, 'cal_force') - - def test_socketio_metadata_rejects_unknown_property(self): - metadata = json.dumps({ - 'schema': 'abacus.socket.properties.v1', - 'present': ['energy', 'charges'], - 'scf_converged': True, - }).encode('utf-8') - with self.assertRaisesRegex(ValueError, 'unknown property'): - AbacusSocketIO._decode_socket_metadata( - np.frombuffer(metadata, dtype=np.int8)) - - def test_socketio_metadata_marks_padding_absent(self): - metadata = json.dumps({ - 'schema': 'abacus.socket.properties.v1', - 'present': ['energy'], - 'scf_converged': False, - }).encode('utf-8') - decoded = AbacusSocketIO._decode_socket_metadata( - np.frombuffer(metadata, dtype=np.int8)) - self.assertEqual(decoded['present'], ('energy',)) - self.assertFalse(decoded['scf_converged']) - - def test_socketio_legacy_response_cannot_infer_force_from_padding(self): - class LegacyServer: - def calculate(self, atoms): - return { - 'energy': 1.0, - 'forces': np.zeros((len(atoms), 3)), - 'virial': np.zeros((3, 3)), - 'morebytes': b'', - } - - calc = object.__new__(AbacusSocketIO) - calc.variable_cell = False - calc._property_constraints = {} - calc._active_properties = ('energy', 'forces') - calc._reference_cell = None - calc.atoms = None - calc.server = LegacyServer() - atoms = Atoms('Si') - with self.assertRaisesRegex(ValueError, 'refusing to infer forces'): - calc.calculate(atoms=atoms, properties=('forces',), system_changes=()) - - def test_socketio_failed_response_clears_previous_frame(self): - class BrokenServer: - def calculate(self, atoms): - raise EOFError('incomplete frame') - - calc = object.__new__(AbacusSocketIO) - calc._property_constraints = {} - calc._active_properties = ('energy',) - calc._reference_cell = None - calc.atoms = None - calc.results = {'energy': 123.0} - calc.last_scf_converged = True - calc._last_socket_metadata = {'scf_converged': True} - calc.server = BrokenServer() - with self.assertRaises(EOFError): - calc.calculate(Atoms('Si'), properties=('energy',), system_changes=()) - self.assertEqual(calc.results, {}) - self.assertIsNone(calc.last_scf_converged) - self.assertIsNone(calc._last_socket_metadata) - - def test_socketio_requested_disabled_property_is_rejected(self): - calc = object.__new__(AbacusSocketIO) - calc._property_constraints = {'forces': False, 'stress': False} - from ase.calculators.calculator import PropertyNotImplementedError - with self.assertRaises(PropertyNotImplementedError): - calc._check_requested_properties(('forces',)) - - def test_parse_version_allows_launcher_noise(self): - stdout = 'launcher info\nABACUS version v3.11.0-beta6\n' - self.assertEqual(AbacusProfile.parse_version(stdout), 'v3.11.0-beta6') - - def test_parse_version_rejects_missing_version(self): - with self.assertRaisesRegex(RuntimeError, 'ABACUS version'): - AbacusProfile.parse_version('launcher failed before abacus started') - - def test_calculator_results(self): - from ase.build.bulk import bulk - silicon = bulk('Si', crystalstructure='diamond', a=5.43) - aprof = AbacusProfile( - command='mpirun -np 2 abacus', - pseudo_dir=self.pporb, - orbital_dir=self.pporb, - omp_num_threads=1 - ) - with tempfile.TemporaryDirectory() as tmpdir: - calculator = Abacus(aprof, - directory=tmpdir, - pseudopotentials={'Si': 'Si_ONCV_PBE-1.0.upf'}, - basissets={'Si': 'Si_gga_6au_100Ry_2s2p1d.orb'}, - inp={'calculation': 'scf', - 'basis_type': 'lcao', - 'ks_solver': 'genelpa', - 'ecutwfc': 40, - 'symmetry': 1, - 'nspin': 1, - 'gamma_only': True, - 'cal_force': 1, - 'cal_stress': 1}) - silicon.calc = calculator - e = silicon.get_potential_energy() - - # check! - self.assertAlmostEqual(e, -194.953050937) - self.assertIsNotNone(calculator.results) - self.assertIsInstance(calculator.results, dict) - for k in ['nspins', 'nkpts', 'nbands', 'eigenvalues', 'occupations', - 'fermi_level', 'kpoint_weights', 'ibz_kpoints', 'energy', - 'free_energy', 'natoms', 'forces', 'stress', 'magmoms']: - self.assertIn(k, calculator.results) - self.assertEqual(calculator.results['nspins'], 1) - self.assertEqual(calculator.results['nkpts'], 1) - self.assertEqual(calculator.results['nbands'], 14) - self.assertEqual(calculator.results['energy'], e) - self.assertEqual(calculator.results['free_energy'], e) - self.assertEqual(calculator.results['natoms'], 2) - - for k in ['eigenvalues', 'occupations', 'ibz_kpoints', 'forces', 'stress', 'magmoms']: - self.assertIsInstance(calculator.results[k], np.ndarray) - - self.assertEqual(calculator.results['eigenvalues'].shape, (1, 1, 14)) - ekb = [-4.82194, 7.62727, 7.62727, 7.62737, 10.2436 , 10.2436 , - 10.2436 , 10.9884 , 16.057 , 16.057 , 23.8353 , 25.421 , - 25.421 , 25.4212 ] - self.assertTrue(np.allclose(calculator.results['eigenvalues'][0, 0, :], np.array(ekb))) - - self.assertEqual(calculator.results['occupations'].shape, (1, 1, 14)) - occ = [2., 2., 2., 2., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.] - self.assertTrue(np.allclose(calculator.results['occupations'][0, 0, :], np.array(occ))) - - self.assertEqual(calculator.results['ibz_kpoints'].shape, (1, 3)) - self.assertTrue(np.allclose(calculator.results['ibz_kpoints'][0, :], np.array([0,0,0]))) - - self.assertEqual(calculator.results['forces'].shape, (2, 3)) - self.assertTrue(np.allclose(calculator.results['forces'], np.zeros((2, 3)))) - - self.assertEqual(calculator.results['stress'].shape, (6,)) - stress = [-0.19327923, -0.19327923, -0.19327923, -0. , 0. , 0. ] - self.assertTrue(np.allclose(calculator.results['stress'], np.array(stress))) - - self.assertEqual(calculator.results['magmoms'].shape, (2,)) - self.assertTrue(np.allclose(calculator.results['magmoms'], np.zeros(2))) - - def test_restart(self): - from ase.build.bulk import bulk - silicon = bulk('Si', crystalstructure='diamond', a=5.43) - aprof = AbacusProfile( - command='mpirun -np 2 abacus', - pseudo_dir=self.pporb, - orbital_dir=self.pporb, - omp_num_threads=1 - ) - with tempfile.TemporaryDirectory() as tmpdir: - calculator = Abacus(aprof, - directory=tmpdir, - pseudopotentials={'Si': 'Si_ONCV_PBE-1.0.upf'}, - basissets={'Si': 'Si_gga_6au_100Ry_2s2p1d.orb'}, - inp={'calculation': 'scf', - 'basis_type': 'lcao', - 'ks_solver': 'genelpa', - 'ecutwfc': 40, - 'symmetry': 1, - 'nspin': 1, - 'gamma_only': True, - 'cal_force': 1, - 'cal_stress': 1}) - silicon.calc = calculator - e = silicon.get_potential_energy() - - # restart - silicon.calc = Abacus.restart(aprof, directory=tmpdir) - e2 = silicon.get_potential_energy() - self.assertAlmostEqual(e2, e) - - def test_version_number_check(self): - # not a version number - with self.assertRaises(AssertionError): - switch_io_backend_version('not-a-version-number') - # too old version - with self.assertRaises(AssertionError): - switch_io_backend_version('v2.2.2') - self.assertTrue(switch_io_backend_version('v3.8.4')) - self.assertFalse(switch_io_backend_version('v3.9.0.25')) - self.assertTrue(switch_io_backend_version('v3.10.0')) - self.assertFalse(switch_io_backend_version('v3.11.0-beta.2')) - self.assertFalse(switch_io_backend_version('v3.11.0')) - - def test_property_keywords_reject_conflicting_user_parameters(self): - template = AbacusTemplate() - with self.assertRaises(ValueError): - template.get_property_keywords({'nspin': 1}, ['magmom']) - - parameters = template.get_property_keywords({'nspin': 2}, ['magmom']) - self.assertEqual(str(parameters['nspin']), '2') - - def test_property_keywords_accept_equivalent_boolean_user_parameters(self): - template = AbacusTemplate() - - parameters = template.get_property_keywords( - {'cal_force': True, 'cal_stress': True}, - ['forces', 'stress'] - ) - - self.assertEqual(str(parameters['cal_force']), '1') - self.assertEqual(str(parameters['cal_stress']), '1') - - def test_property_keywords_reject_conflicting_boolean_user_parameters(self): - template = AbacusTemplate() - - with self.assertRaises(ValueError): - template.get_property_keywords({'cal_force': False}, ['forces']) - - with self.assertRaises(ValueError): - template.get_property_keywords({'cal_stress': False}, ['stress']) - - def test_property_keywords_treat_string_values_as_scalars(self): - template = AbacusTemplate() - template.implemented_properties = ['probe'] - template.get_probe_keywords = lambda parameters: {'custom_switch': 'true'} - - parameters = template.get_property_keywords( - {'custom_switch': 'true'}, ['probe'] - ) - - self.assertEqual(parameters['custom_switch'], 'true') - - def test_property_keywords_compare_iterables_like_input_writer(self): - template = AbacusTemplate() - template.implemented_properties = ['probe'] - template.get_probe_keywords = lambda parameters: { - 'custom_vector': [1, 'true'] - } - - with self.assertRaises(ValueError): - template.get_property_keywords( - {'custom_vector': [True, 'true']}, ['probe'] - ) - - def test_property_keywords_reject_conflicting_properties(self): - template = AbacusTemplate() - template.implemented_properties = ['prop_a', 'prop_b'] - template.get_prop_a_keywords = lambda parameters: {'calculation': 'scf'} - template.get_prop_b_keywords = lambda parameters: {'calculation': 'md'} - - with self.assertRaises(ValueError): - template.get_property_keywords({}, ['prop_a', 'prop_b']) - - def test_dipole_property_is_not_implemented(self): - template = AbacusTemplate() - self.assertNotIn('dipole', template.implemented_properties) - with self.assertRaises(AssertionError): - template.get_property_keywords({}, ['dipole']) - -if __name__ == '__main__': - unittest.main() diff --git a/interfaces/ASE_interface/abacuslite/io/__init__.py b/interfaces/ASE_interface/abacuslite/io/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/interfaces/ASE_interface/abacuslite/io/generalio.py b/interfaces/ASE_interface/abacuslite/io/generalio.py deleted file mode 100644 index d27dccc879a..00000000000 --- a/interfaces/ASE_interface/abacuslite/io/generalio.py +++ /dev/null @@ -1,965 +0,0 @@ -'''public functions that can be used by both the LTS and the latest -version of ABACUS''' -import re -import os -import tempfile -import unittest -from pathlib import Path -from io import TextIOWrapper -from typing import Optional, Dict, List, Any - -import numpy as np -from ase.atoms import Atoms -from ase.build import bulk -from ase.constraints import FixAtoms, FixCartesian -from ase.data import chemical_symbols, atomic_masses -ATOM_MASS = dict(zip(chemical_symbols, atomic_masses.tolist())) - -def load_pseudo(pseudo_dir: str) -> Dict[str, str]: - '''load the pseudopotential mapping from the pseudo_dir''' - pseudo_dir = Path(pseudo_dir) - UPFPAT_ = r'^([A-Z][a-z]?)([^a-zA-Z]+\S+)?\.(upf|UPF)$' - # the file name is assumed to have the pattern: - # [element symbol][delimeter][other things...][.upf|UPF] - return {re.match(UPFPAT_, f.name).group(1): f.name - for f in pseudo_dir.glob('*') - if re.match(UPFPAT_, f.name)} - -def load_orbital(orbital_dir: str, efficiency: bool=True) -> Dict[str, str]: - '''load the ABACUS numerical atomic orbital mapping from the - orbital dir. There may be the case that there is not only one - orbitals find for one element, in this case, the second parameter - `efficiency` will take its effect: if true, use the "smaller" - one, otherwise, use the "larger" one - - Parameters - ---------- - orbital_dir : str - The directory containing the orbital basis files. - efficiency : bool, optional - Whether to use the "smaller" one, default is True. - - Returns - ------- - Dict[str, str] - The mapping from element symbol to orbital basis file name. - ''' - # a small helper function - def _basis_size(basis: List[str]) -> int: - SPECTRUM = 'spdfghi' - num, symb = zip(*[re.match(r'(\d+)([spdfghi])', b).groups() for b in basis]) - return sum((2*SPECTRUM.index(s) + 1) * int(n) for n, s in zip(num, symb)) - - orbital_dir = Path(orbital_dir) - ORBPAT_ = r'^([A-Z][a-z]?)_gga_(\d+)au_(\d+(\.\d+)?)Ry_(\w+)\.orb$' - # a correct parse would yield: - # elem, rcut, ecut, components = re.match(ORBPAT_, f.name) - tosort = {} - for f in orbital_dir.glob('*'): - m = re.match(ORBPAT_, f.name) - if m: - elem, rcut, ecut, _, components = m.groups() - components = re.findall(r'\d+[spdfghi]', components) - tosort.setdefault(elem, []).append( - (f.name, int(rcut), float(ecut), _basis_size(components)) - ) - # sort the basis files by their size: first by components, then by rcut, then by ecut - ib = 0 if efficiency else -1 - return {elem: sorted(basis, key=lambda b: (b[3], b[1], b[2]))[ib][0] - for elem, basis in tosort.items()} - -def file_safe_backup(fn: Path, suffix: str = 'bak'): - '''for the case where there are already files with the same name, - add a suffix to the file name, like `STRU.bak.0`. If there are - already `STRU.bak.0`, rename the elder to `STRU.bak.1` and let - the latest one be `STRU.bak.0`. - - Parameters - ---------- - fn : Path - The path to the file to backup. Note: it must be provided - as the Path object so that its folder is accessible by this - function. - suffix : str, optional - The suffix to add to the file name. Default is 'bak' - ''' - assert isinstance(fn, Path) - where = fn.parent - prefix = f'{fn.name}.{suffix}.' - indexed_backups = [] - - for backup in where.glob(f'{fn.name}.{suffix}.*'): - index_text = backup.name.removeprefix(prefix) - if not re.fullmatch(r'0|[1-9][0-9]*', index_text): - continue - indexed_backups.append((int(index_text), backup)) - - for backup_index, backup in sorted(indexed_backups, key=lambda item: item[0], reverse=True): - backup.rename(backup.parent / f'{fn.name}.{suffix}.{backup_index + 1}') - - # backup the latest file, if there is one - if fn.exists(): - fn.rename(fn.parent / f'{fn.name}.{suffix}.0') - -def _write_stru(job_dir, stru, fname='STRU'): - ''' - Generates a ABACUS STRU file from a STRU dict. - - Parameters - ---------- - job_dir: str - Directory in which the STRU file is generated. - stru : dict - Parameters to generate the STRU file. - See the docstring at the beginning of this script for details. - fname : str - Name of the STRU file. - - ''' - with open(Path(job_dir) / fname, 'w') as f: - - #============ ATOMIC_SPECIES ============ - f.write('ATOMIC_SPECIES\n') - width = {key + '_w' : max([len(str(s[key])) for s in stru['species']]) - for key in ['symbol', 'mass', 'pp_file']} - for s in stru['species']: - f.write('{symbol:<{symbol_w}} {mass:>{mass_w}} {pp_file:>{pp_file_w}}'.format(**s, **width)) - if 'pp_type' in s: - f.write(f" {s['pp_type']}") - f.write('\n') - - #============ NUMERICAL_ORBITAL ============ - if 'orb_file' in stru['species'][0]: - f.write('\nNUMERICAL_ORBITAL\n') - for s in stru['species']: - f.write(f"{s['orb_file']}\n") - - #============ LATTICE_CONSTANT/PARAMETER/VECTORS ============ - f.write('\nLATTICE_CONSTANT\n') - f.write(f"{stru['lat']['const']}\n") - - if 'vec' in stru['lat']: - f.write('\nLATTICE_VECTORS\n') - for v in stru['lat']['vec']: - f.write(f'{v[0]} {v[1]} {v[2]}\n') - - if 'param' in stru['lat']: - f.write('\nLATTICE_PARAMETER\n') - for param in stru['lat']['param']: - f.write(f'{param} ') - f.write('\n') - - #============ ATOMIC_POSITIONS ============ - f.write('\nATOMIC_POSITIONS\n') - f.write(f"{stru['coord_type']}\n") - - for s in stru['species']: - f.write(f"\n{s['symbol']}\n") - f.write(f"{s['mag_each']}\n") - f.write(f"{s['natom']}\n") - - for atom in s['atom']: - f.write(' '.join(f'{x}' for x in atom['coord'])) - - for key in ['m', 'v']: # frozen atom / initial velocity - if key in atom: - f.write(f' {key}' + - ''.join(f' {x}' for x in atom[key])) - - if 'mag' in atom: - if not isinstance(atom['mag'], tuple): # collinear - f.write(f" mag {atom['mag']}") - else: # non-collinear - mag_coord_type, mag = atom['mag'] - assert mag_coord_type in ['Cartesian', 'Spherical'] - if mag_coord_type == 'Cartesian': - f.write(f' mag {mag[0]} {mag[1]} {mag[2]}') - else: - f.write(f' mag {mag[0]} angle1 {mag[1]} angle2 {mag[2]}') - - f.write('\n') - -def species_group_indices(symbols: List[str]) -> List[int]: - """Return indices grouped by first-occurrence species order.""" - species_order = list(dict.fromkeys(symbols)) - return [i for species in species_order for i, symbol in enumerate(symbols) if symbol == species] - - -def _constraint_mobility(atoms: Atoms) -> np.ndarray: - """Return ABACUS mobility flags derived from ASE constraints.""" - mobility = np.ones((len(atoms), 3), dtype=int) - for constraint in atoms.constraints: - if isinstance(constraint, FixAtoms): - mobility[constraint.get_indices()] = 0 - elif isinstance(constraint, FixCartesian): - indices = np.asarray(constraint.get_indices(), dtype=int) - mask = np.asarray(constraint.mask, dtype=bool) - if mask.ndim == 1: - mobility[np.ix_(indices, np.where(mask)[0])] = 0 - else: - for atom_index, atom_mask in zip(indices, mask): - mobility[atom_index, atom_mask] = 0 - return mobility - - -def write_stru(stru: Atoms, - outdir: str, - pp_file: Optional[Dict[str, str]], - orb_file: Optional[Dict[str, str]] = None, - fname: Optional[str] = 'STRU') -> str: - ''' - write the ABACUS STRU file from an Atoms object into outdir. - - Parameters - ---------- - stru : Atoms - The Atoms object to write. - outdir : str - The directory to write the STRU file. - pp_file : dict - A dictionary mapping element symbols to pseudopotential file names. - The keys are the element symbols, and the values are the paths to - the pseudopotential files. - orb_file : dict, optional - A dictionary mapping element symbols to orbital file names. - The keys are the element symbols, and the values are the paths to - the orbital files. If None, no orbital files will be written. - fname : str, optional - The name of the STRU file to write. Default is 'STRU'. - - Returns - ------- - str - The path to the written STRU file. - ''' - from ase.units import ( - Bohr as __BOHR__, - Angstrom as __ANGSTROM__, - AUT as __AUT__, - ) - assert isinstance(stru, Atoms) - pp_file = pp_file or {} # can be None, for those non-ESolver_KS cases - - elem = stru.get_chemical_symbols() - # ABACUS requires the atoms ranged species-by-species, therefore - # we need to group atoms by species. Preserve first-occurrence species - # order from ASE instead of forcing alphabetical order. - ind = species_group_indices(elem) - coords = stru.get_positions()[ind] - mobility = _constraint_mobility(stru)[ind] - # ASE Angstrom / ASE-time to ABACUS Bohr / atomic-time. Keep the same - # species ordering used for coordinates, mobility and magnetic moments. - velocities = (stru.get_velocities()[ind] * __AUT__ / __BOHR__ - if stru.has('momenta') else np.zeros((len(stru), 3))) - elem = [elem[i] for i in ind] - - # handle the atomic magnetic moment (issue #6516) - magmoms = np.array([stru[i].magmom for i in ind]).reshape(len(stru), -1) # ncol in [1, 3] - magmoms = [{} if abs(np.linalg.norm(m)) <= 1e-10 - else {'mag': m[0] if len(m) == 1 else ('Cartesian', m.tolist())} - for m in magmoms] - elem_uniq = list(dict.fromkeys(elem)) - nat = np.array([elem.count(e) for e in elem_uniq]) - stru_dict = { - 'coord_type': 'Cartesian', - 'lat': { - 'const': __ANGSTROM__ / __BOHR__, - 'vec': np.array(stru.get_cell()).tolist() - }, - 'species': [ - { - 'symbol': e, - 'mass': ATOM_MASS[e], - 'pp_file': pp_file.get(e, ''), - 'pp_type': '', - 'natom': n, - 'mag_each': 0.0, - 'atom': [ - magmoms[j] | { - 'coord': coords[j].tolist(), # coordinate - 'm': mobility[j].tolist(), # mobility - 'v': velocities[j].tolist(), # atomic-unit velocity - } for j in range(np.sum(nat[:i]), np.sum(nat[:i+1])) - ] - } - for i, (n, e) in enumerate(zip(nat, elem_uniq)) - ] - } - if orb_file is not None: - for s in stru_dict['species']: - s['orb_file'] = orb_file[s['symbol']] - - _write_stru(outdir, stru_dict, fname) - - return (Path(outdir) / fname).resolve().as_posix() - -def write_input(data: Dict[str, Any], fn: str) -> str: - ''' - write the ABACUS INPUT file from a Dict[str, Any] object - - Parameters - ---------- - data : dict - The data to write to the INPUT file. - The keys are the parameters and the values are the values of the parameters. - fn : str - The path to the ABACUS INPUT file. - - Returns - ------- - str - the absolute path of the written INPUT file - ''' - with open(fn, 'w') as f: - f.write('INPUT_PARAMETERS\n') - for key, value in data.items(): - f.write(f'{key} {value}\n') - f.write('\n') # add a newline at the end - - return str(Path(fn).resolve()) - -def _write_kline(data: Dict[str, Any], f: TextIOWrapper): - ''' - write the k-point file whose mode is "Line" - ''' - f.write('K_POINTS\n') - f.write(f"{data['nk']}\n") - f.write('Line_Cartesian\n' if data['coordinate'].lower() == 'cartesian' - else 'Line\n') - for k, nk in zip(data['kpoints'], data['nkinterpl']): - f.write(f"{k[0]} {k[1]} {k[2]} {nk}\n") - -def _write_ksampl_mp(data: Dict[str, Any], f: TextIOWrapper): - ''' - write the k-point file whose mode is automatically sampling with Monkhorst-Pack - ''' - f.write('K_POINTS\n') - f.write('0\n') - f.write("Gamma\n" if data['gamma-centered'] else "MP\n") - f.write(f"{data['nk'][0]} {data['nk'][1]} {data['nk'][2]} " - f"{data['kshift'][0]} {data['kshift'][1]} {data['kshift'][2]}\n") - -def _write_kpoint(data: Dict[str, Any], f: TextIOWrapper): - ''' - write the k-point file whose mode is specifying kpoints one-by-one - ''' - f.write('K_POINTS\n') - f.write(f"{data['nk']}\n") - f.write(f"{data['coordinate'].capitalize()}\n") - nkinterpl = data.get('nkinterpl', [1 for _ in data['kpoints']]) - for k, nk in zip(data['kpoints'], data.get('nkinterpl', nkinterpl)): - f.write(f"{k[0]} {k[1]} {k[2]} {nk}\n") - -def read_kpoint_from_running_log(fn: str) -> Dict[str, Any]: - '''Read the k-point information from the ABACUS running log file.''' - raise NotImplementedError('read_kpoint_from_running_log is not implemented yet.') - -def write_kpt(data: Dict[str, Any], fn: str) -> str: - ''' - write the ABACUS KPT file from a Dict[str, Any] object - - Parameters - ---------- - data : dict - The data to write to the KPT file. - The keys are the parameters and the values are the values of the parameters. - fn : str - The path to the ABACUS KPT file. - - Returns - ------- - str - the absolute path of the written KPT file - ''' - assert 'mode' in data, 'KPT data must contain "mode" key' - mywriter = { - # 'line': _write_kline, - 'mp-sampling': _write_ksampl_mp, - 'point': _write_kpoint - } - assert data['mode'] in mywriter, \ - f'Invalid KPT mode {data["mode"]}, must be one of ' \ - f'{list(mywriter.keys())}' - with open(fn, 'w') as f: - mywriter[data['mode']](data, f) - - return str(Path(fn).resolve()) - -def _parse_coord_line(line): - ''' - Parses a coordinate line (which may include extra parameters) - in the ATOMIC_POSITIONS block. - - A coordinate line always contains the x, y, z coordinates of an atom, - and may also include - - whether an atom is frozen in MD or relaxation - - initial velocity of an atom in MD or relaxation - - magnetic moment of an atom - - Reference - --------- - https://abacus.deepmodeling.com/en/latest/advanced/input_files/stru.html - - (see section "More Key Words" for details) - - ''' - fields = line.split() - result = { 'coord' : [float(x) for x in fields[0:3]] } - - idx = 3 - while idx < len(fields): - if fields[idx].isdigit(): # no keyword, 0/1 -> frozen atom - result['m'] = [int(x) for x in fields[idx:idx+3]] - idx += 3 - elif fields[idx] == 'm': # frozen atom - result['m'] = [int(x) for x in fields[idx+1:idx+4]] - idx += 4 - elif fields[idx] in ['v', 'vel', 'velocity']: # initial velocity - result['v'] = [float(x) for x in fields[idx+1:idx+4]] - idx += 4 - elif fields[idx] in ['mag', 'magmom']: - ''' - here we assume that frozen atom info cannot be placed after - a collinear mag info without a keyword - i.e., the following coordinate line - 0.0 0.0 0.0 mag 1.0 0 0 0 - is not allowed; one must explicitly specify 'm' in this case: - 0.0 0.0 0.0 mag 1.0 m 0 0 0 - - ''' - if idx + 2 < len(fields) and fields[idx+2] == 'angle1': - result['mag'] = ('Spherical', - list(map(float, fields[idx+1:idx+6:2]))) - idx += 6 - elif idx + 2 < len(fields) and fields[idx+2][0].isdigit(): - result['mag'] = ('Cartesian', - list(map(float, fields[idx+1:idx+4]))) - idx += 4 - else: # collinear - result['mag'] = float(fields[idx+1]) - idx += 2 - else: - raise ValueError('Error: unknown keyword %s'%fields[idx]) - - return result - -def _atomic_positions_gen(lines): - ''' - Iteratively generates info per species from the ATOMIC_POSITIONS block. - - ''' - natom = int(lines[2]) - yield {'symbol': lines[0], 'mag_each': float(lines[1]), 'natom': natom, - 'atom': [_parse_coord_line(line) for line in lines[3:3+natom]]} - if len(lines) > 3 + natom: - yield from _atomic_positions_gen(lines[3+natom:]) - -def read_stru(fn: str) -> Dict[str, Any]: - ''' - read the ABACUS STRU file and return a comprehensive Dict[str, Any] - object. This function is implemented by @jinzx10 in ABACUS-CSW-NAO - project. - - Parameters - ---------- - fn : str - The path to the ABACUS STRU file. - - Returns - ------- - A dict containing the following keys-value pairs: - 'species' : list of dict - List of atomic species. Each dict contains 'symbol', 'mass', - 'pp_file', and optionally 'pp_type'. - ''' - block_title = ['ATOMIC_SPECIES', - 'NUMERICAL_ORBITAL', - 'LATTICE_CONSTANT', - 'LATTICE_PARAMETER', - 'LATTICE_VECTORS', - 'ATOMIC_POSITIONS'] - - def _trim(line): - return line.split('#')[0].split('//')[0].strip(' \t\n') - - with open(fn, 'r') as f: - lines = [_trim(line).replace('\t', ' ') - for line in f.readlines() if len(_trim(line)) > 0] - - # break the content into blocks - delim = [i for i, line in enumerate(lines) if line in block_title] \ - + [len(lines)] - blocks = {lines[delim[i]] : lines[delim[i]+1:delim[i+1]] - for i in range(len(delim) - 1)} - - stru = {} - #============ LATTICE_CONSTANT/PARAMETER/VECTORS ============ - stru['lat'] = {'const': float(blocks['LATTICE_CONSTANT'][0])} - if 'LATTICE_VECTORS' in blocks: - stru['lat']['vec'] = [[float(x) for x in line.split()] - for line in blocks['LATTICE_VECTORS']] - elif 'LATTICE_PARAMETER' in blocks: - stru['lat']['param'] = [float(x) - for x in blocks['LATTICE_PARAMETER'][0].split()] - - #============ ATOMIC_SPECIES ============ - stru['species'] = [dict(zip(['symbol', 'mass', 'pp_file', 'pp_type'], - line.split())) - for line in blocks['ATOMIC_SPECIES']] - for s in stru['species']: - s['mass'] = float(s['mass']) - - #============ NUMERICAL_ORBITAL ============ - if 'NUMERICAL_ORBITAL' in blocks: - for i, s in enumerate(stru['species']): - s['orb_file'] = blocks['NUMERICAL_ORBITAL'][i] - - #============ ATOMIC_POSITIONS ============ - stru['coord_type'] = blocks['ATOMIC_POSITIONS'][0] - index = { s['symbol'] : i for i, s in enumerate(stru['species']) } - for ap in _atomic_positions_gen(blocks['ATOMIC_POSITIONS'][1:]): - stru['species'][index[ap['symbol']]].update(ap) - - return stru - -def read_input(fn: str) -> Dict[str, Any]: - ''' - read the ABACUS INPUT file and return a comprehensive Dict[str, Any] - - Parameters - ---------- - fn : str - The path to the ABACUS INPUT file. - - Returns - ------- - dict - ''' - with open(fn) as f: - raw = [l.strip() for l in f.readlines()] - raw = [l for l in raw - if l and not re.match(r'^(INPUT_PARAMETERS|#|//|!)', l)] - raw = [re.split(r'#|//|!', l)[0].strip() for l in raw] - raw = [re.split(r'\s+', l) for l in raw] - - return dict([(l[0], ' '.join(l[1:])) for l in raw if len(l) > 0]) - -def _read_kline(raw: List[str]) -> Dict[str, Any]: - ''' - read the k-point file whose mode is "Line", and return the parsed result - ''' - assert len(raw) >= 4, \ - f'Invalid KPT file, expected at least 4 lines, got {len(raw)}' - assert re.match(r'^\d+$', raw[1]), \ - f'Invalid KPT file, expected the second line to be an integer, ' \ - f'got {raw[1]}' - assert int(raw[1]) == len(raw) - 3, \ - f'Invalid KPT file, expected the second line to be the number of ' \ - f'k-points, got {raw[1]}, but found {len(raw)} k-points' - assert raw[2].lower().startswith('line'), \ - f'Invalid KPT file, expected "Line" in the third line, got {raw[2]}' - mymatch = [re.match(r'^(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)\s+' - r'(\d+).*', l) - for l in raw[3:]] - assert all(m for m in mymatch), \ - 'Invalid KPT file, expected the k-points to be in the format ' \ - '"x y z n # comment"' - return { - 'mode': 'line', - 'coordinate': 'Cartesian' if raw[2].lower().endswith('cartesian') else 'Direct', - 'nk': int(raw[1]), - 'kpoints': [tuple([float(x) for x in m.groups()[:3]]) for m in mymatch], - 'nkinterpl': [int(m.groups()[6]) for m in mymatch] - } - -def _read_ksampl_mp(raw: List[str]) -> Dict[str, Any]: - ''' - read the k-point file whose mode is automatically sampling with Monkhorst-Pack - scheme, and return the parsed result - ''' - assert len(raw) == 4, \ - f'Invalid KPT file, expected 4 lines, got {len(raw)}' - assert raw[2].lower() in ['gamma', 'mp'], \ - f'Invalid KPT file, expected "Gamma" or "MP" in the third line, ' \ - f'got {raw[2]}' - # the following may raise the ValueError if unpacking is not correct - nk1, nk2, nk3, kshift1, kshift2, kshift3 = map(int, raw[3].split()) - return { - 'mode': 'mp-sampling', - 'gamma-centered': bool(raw[2].lower() == 'gamma'), - 'nk': (nk1, nk2, nk3), - 'kshift': (kshift1, kshift2, kshift3) - } - -def _read_kpoint(raw: List[str]) -> Dict[str, Any]: - ''' - read the k-point file whose mode is specifying kpoints one-by-one, return - the parsed result. - ''' - assert len(raw) >= 4, \ - f'Invalid KPT file, expected at least 4 lines, got {len(raw)}' - assert re.match(r'^\d+$', raw[1]), \ - f'Invalid KPT file, expected the second line to be an integer, ' \ - f'got {raw[1]}' - assert int(raw[1]) == len(raw) - 3, \ - f'Invalid KPT file, expected the second line to be the number of ' \ - f'k-points, got {raw[1]}, but found {len(raw)} k-points' - assert raw[2].lower() in ['direct', 'cartesian'], \ - f'Invalid KPT file, expected "Direct" or "Cartesian" in the third line, ' \ - f'got {raw[2]}' - mymatch = [re.match(r'^(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)\s*' - r'(\d+).*', l) - for l in raw[3:]] - assert all(m for m in mymatch), \ - 'Invalid KPT file, expected the k-points to be in the format ' \ - '"x y z n # comment"' - - return { - 'mode': 'point', - 'coordinate': 'Cartesian' if raw[2].lower() == 'cartesian' else 'Direct', - 'nk': int(raw[1]), - 'kpoints': [tuple(map(float, m.groups()[:3])) for m in mymatch], - 'weights': [int(m.groups()[6]) for m in mymatch] - } - -def read_kpt(fn: str) -> Dict[str, Any]: - ''' - read the ABACUS KPT file and return a comprehensive Dict[str, Any] - - Parameters - ---------- - fn : str - The path to the ABACUS KPT file. - - Returns - ------- - dict - A dictionary containing the k-point information. - ''' - with open(fn) as f: - raw = [l.strip() for l in f.readlines()] - raw = [l for l in raw if l] - parser_switch = {'line': _read_kline, - 'gamma': _read_ksampl_mp, 'mp': _read_ksampl_mp, - 'direct': _read_kpoint, 'cartesian': _read_kpoint} - assert raw[0] == 'K_POINTS', \ - f'Invalid KPT file {fn}, first line must be "K_POINTS"' - # because there are modes like "Line_Cartesian", we need to - # split the second line by '_' - return parser_switch[raw[2].lower().split('_')[0]](raw) - -class TestAbacusCalculatorIOUtil(unittest.TestCase): - - def setUp(self): - here = Path(__file__).parent - self.testfiles = here / 'testfiles' - - def test_input_io(self): - fn = self.testfiles / 'input-script' - data = read_input(fn) - with tempfile.NamedTemporaryFile(mode='w') as f: - fn_ = Path(f.name) - write_input(data, fn_.as_posix()) - data_ = read_input(fn_.as_posix()) - self.assertDictEqual(data, data_) - # will automatically delete the file after the context manager - - def test_file_safe_backup_rotates_numbered_backups(self): - with tempfile.TemporaryDirectory() as tmpdir: - workdir = Path(tmpdir) - live = workdir / 'STRU' - live.write_text('live') - (workdir / 'STRU.bak.0').write_text('bak0') - (workdir / 'STRU.bak.1').write_text('bak1') - (workdir / 'STRU.bak.01').write_text('bak01') - (workdir / 'STRU.bak.note').write_text('note') - - file_safe_backup(live) - - self.assertFalse(live.exists()) - self.assertEqual((workdir / 'STRU.bak.0').read_text(), 'live') - self.assertEqual((workdir / 'STRU.bak.1').read_text(), 'bak0') - self.assertEqual((workdir / 'STRU.bak.2').read_text(), 'bak1') - self.assertEqual((workdir / 'STRU.bak.01').read_text(), 'bak01') - self.assertEqual((workdir / 'STRU.bak.note').read_text(), 'note') - - def test_stru_io(self): - from ase.units import Bohr, Angstrom - nacl = bulk('NaCl', 'rocksalt', a=5.64) - write_stru( - nacl, - outdir=self.testfiles, - pp_file={ - 'Na': 'Na.pz-bhs.UPF', - 'Cl': 'Cl.pz-bhs.UPF' - }, - orb_file={ - 'Na': 'Na_gga_6au_100Ry_2s2p1d.orb', - 'Cl': 'Cl_gga_6au_100Ry_2s2p1d.orb', - }, - ) - stru_ = read_stru(self.testfiles / 'STRU') - (self.testfiles / 'STRU').unlink() - - self.assertIsInstance(stru_, dict) - for k in ['lat', 'species', 'coord_type']: - self.assertIn(k, stru_) - for k in ['const', 'vec']: - self.assertIn(k, stru_['lat']) - for k in ['symbol', 'mass', 'pp_file', 'orb_file', 'mag_each', 'natom', 'atom']: - for s in stru_['species']: - self.assertIn(k, s) - self.assertEqual(stru_['coord_type'], 'Cartesian') - - self.assertEqual(stru_['lat']['const'], Angstrom / Bohr) - self.assertTrue(np.allclose(stru_['lat']['vec'], np.array(nacl.get_cell()))) - self.assertIsInstance(stru_['species'], list) - - for s in stru_['species']: - self.assertIsInstance(s, dict) - self.assertEqual(s['natom'], 1) - self.assertIsInstance(s['atom'], list) - self.assertEqual(s['natom'], len(s['atom'])) - for a in s['atom']: - self.assertIsInstance(a, dict) - for k in ['coord', 'm', 'v']: - self.assertIn(k, a) - self.assertEqual(a['m'], [1, 1, 1]) - self.assertEqual(a['v'], [0.0, 0.0, 0.0]) - - self.assertEqual(stru_['species'][0]['symbol'], 'Na') - self.assertEqual(stru_['species'][1]['symbol'], 'Cl') - self.assertEqual(stru_['species'][0]['mass'], ATOM_MASS['Na']) - self.assertEqual(stru_['species'][1]['mass'], ATOM_MASS['Cl']) - self.assertEqual(stru_['species'][0]['pp_file'], 'Na.pz-bhs.UPF') - self.assertEqual(stru_['species'][1]['pp_file'], 'Cl.pz-bhs.UPF') - self.assertEqual(stru_['species'][0]['orb_file'], 'Na_gga_6au_100Ry_2s2p1d.orb') - self.assertEqual(stru_['species'][1]['orb_file'], 'Cl_gga_6au_100Ry_2s2p1d.orb') - - def test_read_stru_with_lattice_parameter(self): - # STRU that uses LATTICE_PARAMETER instead of LATTICE_VECTORS - # (issue #7555: previously raised KeyError on 'LATTICE_PARAMETERS') - content = ( - "ATOMIC_SPECIES\n" - "Si 28.085 Si.upf\n" - "\n" - "LATTICE_CONSTANT\n" - "10.2\n" - "\n" - "LATTICE_PARAMETER\n" - "5.43 5.43 5.43 90 90 90\n" - "\n" - "ATOMIC_POSITIONS\n" - "Direct\n" - "\n" - "Si\n" - "0.0\n" - "2\n" - "0.0 0.0 0.0 1 1 1\n" - "0.25 0.25 0.25 1 1 1\n" - ) - with tempfile.TemporaryDirectory() as tmpdir: - fn = Path(tmpdir) / 'STRU' - fn.write_text(content) - stru = read_stru(fn) - self.assertEqual(stru['lat']['const'], 10.2) - self.assertNotIn('vec', stru['lat']) - self.assertEqual(stru['lat']['param'], [5.43, 5.43, 5.43, 90.0, 90.0, 90.0]) - self.assertEqual(stru['species'][0]['symbol'], 'Si') - self.assertEqual(stru['species'][0]['natom'], 2) - - def test_write_stru_preserves_first_occurrence_species_order(self): - atoms = Atoms( - symbols=['C', 'C', 'Pt', 'H', 'H'], - positions=np.zeros((5, 3)), - cell=np.eye(3), - ) - - with tempfile.TemporaryDirectory() as tmpdir: - write_stru( - atoms, - outdir=tmpdir, - pp_file={'C': 'C.upf', 'Pt': 'Pt.upf', 'H': 'H.upf'}, - ) - stru = read_stru(Path(tmpdir) / 'STRU') - - self.assertEqual([s['symbol'] for s in stru['species']], ['C', 'Pt', 'H']) - self.assertEqual([s['natom'] for s in stru['species']], [2, 1, 2]) - - def test_stru_velocities_keep_physical_units_and_species_order(self): - from ase.units import fs - - atoms = Atoms('HOH', positions=[[0, 0, 0], [1, 0, 0], [2, 0, 0]], - cell=[8, 8, 8], pbc=True) - atoms.set_velocities(np.array([[1, 0, 0], [0, 2, 0], [0, 0, -0.5]]) / fs) - # ABACUS writes MD_dump Angstrom/fs from STRU atomic-unit velocity. - au_per_angstrom_fs = 1.8897270 * 2.418884326505e-2 - with tempfile.TemporaryDirectory() as tmpdir: - path = write_stru(atoms, tmpdir, {'H': 'H.upf', 'O': 'O.upf'}) - species = read_stru(path)['species'] - self.assertEqual([entry['symbol'] for entry in species], ['H', 'O']) - self.assertEqual([entry['natom'] for entry in species], [2, 1]) - np.testing.assert_allclose([entry['v'] for entry in species[0]['atom']], - np.array([[1, 0, 0], [0, 0, -0.5]]) * au_per_angstrom_fs, - rtol=0, atol=1e-7) - np.testing.assert_allclose(species[1]['atom'][0]['v'], - [0, 2 * au_per_angstrom_fs, 0], rtol=0, atol=1e-7) - - def test_write_stru_uses_ase_constraints_for_mobility(self): - atoms = Atoms( - symbols=['C', 'C', 'Pt', 'H'], - positions=np.zeros((4, 3)), - cell=np.eye(3), - ) - atoms.set_constraint([ - FixAtoms(indices=[0]), - FixCartesian(2, mask=[True, False, True]), - ]) - - with tempfile.TemporaryDirectory() as tmpdir: - write_stru( - atoms, - outdir=tmpdir, - pp_file={'C': 'C.upf', 'Pt': 'Pt.upf', 'H': 'H.upf'}, - ) - stru = read_stru(Path(tmpdir) / 'STRU') - - self.assertEqual(stru['species'][0]['atom'][0]['m'], [0, 0, 0]) - self.assertEqual(stru['species'][0]['atom'][1]['m'], [1, 1, 1]) - self.assertEqual(stru['species'][1]['atom'][0]['m'], [0, 1, 0]) - self.assertEqual(stru['species'][2]['atom'][0]['m'], [1, 1, 1]) - - def test_kpt_io(self): - kpt = { - 'mode': 'point', - 'coordinate': 'Cartesian', - 'nk': 1, - 'kpoints': [(0.0, 0.0, 0.0)], - 'weights': [1] - } - with tempfile.NamedTemporaryFile(mode='w') as f: - fn_ = Path(f.name) - write_kpt(kpt, fn_.as_posix()) - self.assertDictEqual(kpt, read_kpt(fn_.as_posix())) - - kpt = { - 'mode': 'mp-sampling', - 'nk': (5, 5, 5), - 'kshift': (0, 0, 0), - 'gamma-centered': True, - } - with tempfile.NamedTemporaryFile(mode='w') as f: - fn_ = Path(f.name) - write_kpt(kpt, fn_.as_posix()) - self.assertDictEqual(kpt, read_kpt(fn_.as_posix())) - - # kpt = { - # 'mode': 'line', - # 'nk': 6, - # 'kpoints': [ - # (0.0, 0.0, 0.0), - # (0.5, 0.5, 0.5), - # (0.0, 0.5, 0.5), - # (0.5, 0.5, 0.0), - # (0.5, 0.0, 0.5), - # (0.0, 0.0, 0.5), - # ], - # 'nkinterpl': [10, 10, 10, 10, 10, 1], - # 'coordinate': 'Cartesian', - # } - # with tempfile.NamedTemporaryFile(mode='w') as f: - # fn_ = Path(f.name) - # write_kpt(kpt, fn_.as_posix()) - # self.assertDictEqual(kpt, read_kpt(fn_.as_posix())) - - @unittest.skip('') - def test_load_pseudo(self): - pseudo_dir = self.testfiles / 'pporb' - pseudo_map = load_pseudo(pseudo_dir) - self.assertEqual(pseudo_map['O'], 'O.upf') - self.assertEqual(pseudo_map['Si'], 'Si.upf') - self.assertEqual(pseudo_map['In'], 'In.upf') - self.assertEqual(pseudo_map['As'], 'As.pbe-n-rrkjus_psl.0.2.UPF') - self.assertEqual(pseudo_map['Ga'], 'Ga.pbe-dn-kjpaw_psl.1.0.0.UPF') - - @unittest.skip('') - def test_load_orbital(self): - orbital_dir = self.testfiles / 'pporb' - orbital_map = load_orbital(orbital_dir) - self.assertEqual(orbital_map['O'], 'O_gga_6au_100Ry_2s2p1d.orb') - self.assertEqual(orbital_map['Si'], 'Si_gga_7au_100Ry_2s2p1d.orb') - self.assertEqual(orbital_map['In'], 'In_gga_8au_100Ry_1s1p1d.orb') - - orbital_map = load_orbital(orbital_dir, efficiency=False) - self.assertEqual(orbital_map['O'], 'O_gga_6au_100Ry_2s2p1d.orb') - self.assertEqual(orbital_map['Si'], 'Si_gga_7au_100Ry_2s2p1d.orb') - self.assertEqual(orbital_map['In'], 'In_gga_8au_100Ry_2s2p2d1f.orb') - - def test_write_stru_with_magmom(self): - # from issue #6516: https://github.com/deepmodeling/abacus-develop/issues/6516 - atoms = Atoms( - symbols=['Co', 'Cr', 'Ni', 'Co'], - cell=np.eye(3), - magmoms=[1, 2, 3, 1] - ) - with tempfile.NamedTemporaryFile(mode='w') as f: - write_stru( - stru=atoms, - outdir=self.testfiles, - pp_file={ # as if we have some pseudopotentials - 'Co': 'Co.upf', 'Cr': 'Cr.upf', 'Ni': 'Ni.upf', 'Co': 'Co.upf', - }, - orb_file={ # as if we have some orbitals - 'Co': 'Co.orb', 'Cr': 'Cr.orb', 'Ni': 'Ni.orb', 'Co': 'Co.orb', - }, - fname=f.name, - ) - stru = read_stru(f.name) - # Co - self.assertEqual(stru['species'][0]['mag_each'], 0.0) - self.assertEqual(stru['species'][0]['atom'][0]['mag'], 1.0) - self.assertEqual(stru['species'][0]['atom'][1]['mag'], 1.0) - # Cr - self.assertEqual(stru['species'][1]['mag_each'], 0.0) - self.assertEqual(stru['species'][1]['atom'][0]['mag'], 2.0) - # Ni - self.assertEqual(stru['species'][2]['mag_each'], 0.0) - self.assertEqual(stru['species'][2]['atom'][0]['mag'], 3.0) - - # then the non-colinear case - atoms = Atoms( - symbols=['Co', 'Cr', 'Ni', 'Co'], - cell=np.eye(3), - magmoms=np.array([ - [1, 0, 0], - [0, 2, 0], - [0, 0, 3], - [1, 0, 0], - ]) - ) - with tempfile.NamedTemporaryFile(mode='w') as f: - write_stru( - stru=atoms, - outdir=self.testfiles, - pp_file={ # as if we have some pseudopotentials - 'Co': 'Co.upf', 'Cr': 'Cr.upf', 'Ni': 'Ni.upf', 'Co': 'Co.upf', - }, - orb_file={ # as if we have some orbitals - 'Co': 'Co.orb', 'Cr': 'Cr.orb', 'Ni': 'Ni.orb', 'Co': 'Co.orb', - }, - fname=f.name, - ) - stru = read_stru(f.name) - # Co - self.assertEqual(stru['species'][0]['mag_each'], 0.0) - self.assertEqual(stru['species'][0]['atom'][0]['mag'], ('Cartesian', [1.0, 0.0, 0.0])) - self.assertEqual(stru['species'][0]['atom'][1]['mag'], ('Cartesian', [1.0, 0.0, 0.0])) - # Cr - self.assertEqual(stru['species'][1]['mag_each'], 0.0) - self.assertEqual(stru['species'][1]['atom'][0]['mag'], ('Cartesian', [0.0, 2.0, 0.0])) - # Ni - self.assertEqual(stru['species'][2]['mag_each'], 0.0) - self.assertEqual(stru['species'][2]['atom'][0]['mag'], ('Cartesian', [0.0, 0.0, 3.0])) - -if __name__ == '__main__': - unittest.main() diff --git a/interfaces/ASE_interface/abacuslite/io/latestio.py b/interfaces/ASE_interface/abacuslite/io/latestio.py deleted file mode 100644 index 294f93d619b..00000000000 --- a/interfaces/ASE_interface/abacuslite/io/latestio.py +++ /dev/null @@ -1,717 +0,0 @@ -'''implements the parser for the latest version of ABACUS''' -import re -import shutil -import unittest -from pathlib import Path -from typing import Dict, List, Optional, Tuple - -import numpy as np -from ase.atoms import Atoms -from ase.calculators.singlepoint import ( - SinglePointKPoint, - SinglePointDFTCalculator -) -from ase.units import GPa -from ase.stress import full_3x3_to_voigt_6_stress - -# some output formats are not updated, -# for these cases, we import from the legacyio module -from abacuslite.io.legacyio import ( - read_kpoints_from_running_log, - read_energies_from_running_log, - read_traj_from_md_dump, - read_magmom_from_running_log, - is_invalid_arr, - find_final_info_with_iter_header -) - -def read_esolver_type_from_running_log(src: str | Path | List[str]) \ - -> str: - ''' - read the esolver type from the ABACUS running log file. - - Parameters - ---------- - fn : str - The path to the ABACUS running log file. - - Returns - ------- - str - The esolver type used in the ABACUS calculation. - ''' - if isinstance(src, (str, Path)): - with open(src) as f: - raw = f.readlines() - else: # assume the src is the return of the readlines() - raw = src - # with open(fn) as f: - # raw = f.readlines() - raw = [l.strip() for l in raw] - raw = [l for l in raw if l] # remove empty lines - - # search for the line with information like: - # "#ENERGY SOLVER# ksdft_lcao" - lines = [ - re.match(r'#ENERGY SOLVER#\s+(\S+)', l) - for l in raw - ] - eslvtyp = [m.group(1) for m in lines if m is not None] - assert len(set(eslvtyp)) == 1, \ - f'Inconsistent esolver type: {set(eslvtyp)}' - return eslvtyp[0] - -def read_band_from_running_log(src: str | Path | List[str]): - '''in the latest branch, the band information is removed from running log. - This function is only for backward compatibility. To get the band information, - please call the "read_band_from_eig_occ" function.''' - return None - -def read_band_from_eig_occ(src: str | Path | List[str]) \ - -> List[Dict[str, np.ndarray]]: - '''read the OUT.${suffix}/eig_occ.txt - - Parameters - ---------- - src : str or Path or list of str - The path to the ABACUS running log file or the return of the readlines() method. - - Returns - ------- - list of dict - A list of dictionaries containing the k-points and band energies. - Each dictionary has keys 'k' and 'e', where 'k' is a list of k-point - coordinates and 'e' is a numpy array of band energies. - ''' - if isinstance(src, (str, Path)): - with open(src) as f: - raw = f.readlines() - else: # assume the src is the return of the readlines() - raw = src - # with open(fn) as f: - # raw = f.readlines() - raw = [l.strip() for l in raw] - raw = [l for l in raw if l] # remove empty lines - - nframe = len([l for l in raw if l.endswith('# ionic step')]) - ekb_leading_pat = r'spin=(\d)\s+k-point=(\d+)/(\d+)\s+Cartesian=\s*' \ - + r'(-?\d(\.\d+)?(e-\d+)?)\s+' \ - + r'(-?\d(\.\d+)?(e-\d+)?)\s+' \ - + r'(-?\d(\.\d+)?(e-\d+)?)\s+' \ - + r'\(\d+\s+plane wave\)' - # from the ekb leading line, there are nspin, nk information, also the - # coordinate of kpoints - iekb = [i for i, l in enumerate(raw) if re.match(ekb_leading_pat, l)] - assert len(iekb) > 0, f'No k-point found' - - k_raw = [re.match(ekb_leading_pat, raw[i]).groups() for i in iekb] - - # spin - ispin = [int(g[0]) for g in k_raw] - assert all(i in [1, 2] for i in ispin) # what about nspin 4? - nspin = len(set(ispin)) - assert nspin in [1, 2] - - # k-points - nk = [int(g[2]) for g in k_raw] - nk = set(nk) - assert len(nk) == 1, f'Sampling on k-points changed during the calculation: {nk}' - nk = nk.pop() - ik = [int(g[1]) for g in k_raw] - assert all(i-1 in range(nk) for i in ik), f'k-point index out of range: {ik}' - assert len(ik) == nframe * nspin * nk - k = np.array([[float(g[i]) for i in [3, 6, 9]] for g in k_raw]) - assert k.shape == (nframe * nspin * nk, 3) - k = k.reshape((nframe, nspin, nk, 3)) - - ekbpat = r'\d+\s+' - ekbpat += r'(-?\d+(\.\d+)?(e[+-]\d+)?)\s+' - ekbpat += r'(\d+(\.\d+)?(e[+-]\d+)?)' - iekb = [i for i, l in enumerate(raw) if re.match(ekbpat, l)] - assert len(iekb) > 0, f'No band energy found' - assert len(iekb) % (nspin*nk) == 0 - nb = len(iekb) // (nframe * nspin * nk) - ekb_raw = np.array([list(map(float, raw[i].split()[1:])) for i in iekb]) - assert ekb_raw.shape == (nframe * nspin * nk * nb, 2) - ekb_raw = ekb_raw.reshape(nframe, nspin, nk, nb, 2) - ekb, occ = ekb_raw[:, :, :, :, 0], ekb_raw[:, :, :, :, 1] - - return [{'k': ki, 'e': eki, 'occ': occi} - for ki, eki, occi in zip(k, ekb, occ)] - -def read_traj_from_running_log(src: str | Path | List[str]) \ - -> List[Dict[str, np.ndarray|str]]: - ''' - read the trajectory from the ABACUS running log file. - NOTE: in MD runs, the trajectory is not recorded in the running log file. - - Parameters - ---------- - fn : str - The path to the ABACUS running log file. - - Returns - ------- - list of dict - A list of dictionaries containing the coordinate system, cell, elements - and the coordinates of the atoms. Each dictionary has keys 'coordinate', - 'cell', 'cell_unit', 'alat_in_angstrom', 'elem', and 'coords'. - The values are numpy arrays or strings. - - 'coordinate': string, the coordinate system, e.g., 'Cartesian' or 'Direct' - - 'cell': numpy array of shape (3, 3) - - 'cell_unit': string, the unit of the cell, e.g., 'Angstrom' - - 'alat_in_angstrom': float, the lattice constant in Angstrom - - 'elem': list of strings, the chemical symbols of the elements - - 'coords': numpy array of shape (natoms, 3), the coordinates of the atoms - ''' - if isinstance(src, (str, Path)): - with open(src) as f: - raw = f.readlines() - else: # assume the src is the return of the readlines() - raw = src - # with open(fn) as f: - # raw = f.readlines() - raw = [l.strip() for l in raw] - raw = [l for l in raw if l] # remove empty lines - - # search for the total number of atoms - # r'TOTAL ATOM NUMBER = (\d+)' - natoms = [re.search(r'TOTAL ATOM NUMBER\s*=\s*(\d+)', l) for l in raw] - natoms = [int(n.group(1)) for n in natoms if n] - assert len(set(natoms)) == 1, \ - f'Inconsistent number of atoms: {set(natoms)}' - natoms = natoms[0] - - # search for the coordinate system - # r'^([DIRECT|CARTESIAN]) COORDINATES$' - coordinate = [re.match(r'^(DIRECT|CARTESIAN) COORDINATES', l) for l in raw] - coordinate = [l.group(1).lower().capitalize() for l in coordinate if l] - assert len(set(coordinate)) == 1, \ - f'Inconsistent coordinate system: {set(coordinate)}' - coordinate = coordinate[0] - - # search for the cell, but first get the "a0": lattice constant - # r'lattice constant (Angstrom) = (-?\d+(\.\d+)?)' - a0 = [re.search(r'lattice constant \(Angstrom\)\s*=\s*(-?\d+(\.\d+)?)', l, - re.IGNORECASE) for l in raw] - a0 = [float(n.group(1)) for n in a0 if n] - assert len(set(a0)) == 1, f'Inconsistent lattice constant: {set(a0)}' - a0 = a0[0] - # then the cell - # r'^Lattice vectors: \(Cartesian coordinate: in unit of a\_0\)$' - icell = [i for i, l in enumerate(raw) - if re.match(r'^Lattice vectors: \(Cartesian coordinate: in unit of a_0\)$', l)] - assert len(icell) > 0, f'No cell found' - # nframe = len(icell) # will be 1 for cell-invariant MD - # assert nframe > 0, f'Invalid trajectory with length {nframe}') - cell_raw = [raw[i+1:i+1+3] for i in icell] - cell = [np.array([list(map(float, l.split())) for l in c]) * a0 - for c in cell_raw] # convert to Angstrom - assert all(c.shape == (3, 3) for c in cell), \ - f'Unexpected shape of cell: {[c.shape for c in cell]}' - - # search for the elements and coordinates - coord_leading_pat = r'atom\s+x\s+y\s+z\s+mag' # what about nspin 4? - icoord = [i for i, l in enumerate(raw) if re.match(coord_leading_pat, l)] - nframe = len(icoord) - coord_raw = np.array([l.split() for i in icoord for l in raw[i+1:i+1+natoms]]) - elem = coord_raw[:, 0].astype(str).reshape((nframe, natoms)) - coords = coord_raw[:, 1:4].astype(float).reshape((nframe, natoms, 3)) - mag = coord_raw[:, 4].astype(float).reshape((nframe, natoms, )) - - cell = np.array(cell).reshape(-1, 3, 3) - if cell.shape[0] == 1: - cell = [cell[0] for _ in range(nframe)] - assert len(cell) == nframe, \ - f'Unexpected number of cells: {len(cell)}, ' \ - f'expected {nframe}' - return [{ - 'coordinate': coordinate, - 'cell': c, - 'cell_unit': 'Angstrom', - 'alat_in_angstrom': a0, - 'elem': e, - 'coords': co - } for c, e, co in zip(cell, elem, coords)] - -def read_forces_from_running_log(src: str | Path | List[str]) \ - -> List[np.ndarray]: - ''' - read the forces from the ABACUS running log file. - - Parameters - ---------- - src : str or Path or list of str - The path to the ABACUS running log file or the return of the readlines() method. - - Returns - ------- - list of numpy array - A list of numpy arrays containing the forces on each atom. - Each array has shape (natoms, 3). - ''' - if isinstance(src, (str, Path)): - with open(src) as f: - raw = f.readlines() - else: # assume the src is the return of the readlines() - raw = src - # with open(fn) as f: - # raw = f.readlines() - raw = [l.strip() for l in raw] - raw = [l for l in raw if l] # remove empty lines - - # iteratively search for the forces, which led by the title `#TOTAL-FORCE (eV/Angstrom)#` - forces, istart = [], 0 - while istart < len(raw): - ith = None # index of the table header - for i, l in enumerate(raw[istart:]): - if re.match(r'#\s*TOTAL\-FORCE\s*\(eV\s*/Angstrom\)\s*#', l, re.IGNORECASE): - ith = i - break - if ith is None: # no forces found - break - # otherwise - # search for the first line that matches the pattern - FORCEPAT_ = r'\s*([A-Z][a-z]?\d+)\s+(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)' - itb = None # index of the first line of the table body - for i, l in enumerate(raw[istart+ith+1:]): - if re.match(FORCEPAT_, l): - itb = i - break - if itb is None: # no content found - break - # otherwise - jtb = None # index of the last line of the table body - for j, l in enumerate(raw[istart+ith+1+itb:]): - if not re.match(FORCEPAT_, l): - jtb = j - break - if jtb is None: # no content found - break - - # truncate the force table and append - force_raw = raw[istart+ith+1+itb:istart+ith+1+itb+jtb] - force = np.array([list(map(float, l.split()[1:])) for l in force_raw]) - forces.append(force) - - # update the istart - istart += ith + itb + jtb + 1 - - return forces - -def read_stress_from_running_log(src: str | Path | List[str]) \ - -> List[np.ndarray]: - ''' - read the stress from the ABACUS running log file. - - Parameters - ---------- - src : str or Path or list of str - The path to the ABACUS running log file or the return of the readlines() method. - - Returns - ------- - list of numpy array - A list of numpy arrays containing the stress on each atom. - Each array has shape (3, 3). - ''' - if isinstance(src, (str, Path)): - with open(src) as f: - raw = f.readlines() - else: # assume the src is the return of the readlines() - raw = src - # with open(fn) as f: - # raw = f.readlines() - raw = [l.strip() for l in raw] - raw = [l for l in raw if l] # remove empty lines - - # iteratively search for the stress, which led by the title `#TOTAL-STRESS (kbar)#` - stresses, istart = [], 0 - - while istart < len(raw): - ith = None # index of the table header - for i, l in enumerate(raw[istart:]): - if re.match(r'#\s*TOTAL\-STRESS\s*\(kbar\)\s*#', l, re.IGNORECASE): - ith = i - break - if ith is None: # no stress found - break - # otherwise - # search for the first line that matches the pattern - STRESSPAT_ = r'\s*(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)' - itb = None # index of the first line of the table body - for i, l in enumerate(raw[istart+ith+1:]): - if re.match(STRESSPAT_, l): - itb = i - break - if itb is None: # no content found - break - # otherwise - jtb = 3 # because the stress tensor would be a (3, 3)-matrix - - # truncate the stress table and append - stress_raw = raw[istart+ith+1+itb:istart+ith+1+itb+jtb] - stress = np.array([list(map(float, l.split())) for l in stress_raw]).reshape(3, 3) - # unit: kbar -> GPa - stresses.append(-0.1 * stress * GPa) - - # update the istart - istart += ith + itb + jtb + 1 - - return stresses - -def read_iter_header_from_running_log(src: str | Path | List[str]) \ - -> List[Tuple[int, int]]: - ''' - read the "iteration header" from the running log, useful for determining - which is the final iteration. The "iteration header" is defined as: - ``` - --> #ION MOVE# 1 #ELEC ITER# 3 - ``` - ''' - if isinstance(src, (str, Path)): - with open(src) as f: - raw = f.readlines() - else: # assume the src is the return of the readlines() - raw = src - # with open(fn) as f: - # raw = f.readlines() - raw = [l.strip() for l in raw] - raw = [l for l in raw if l] # remove empty lines - - HEADER_PAT = r'-->\s+#ION MOVE#\s+(\d+)\s+#ELEC ITER#\s+(\d+)' - res = [re.findall(HEADER_PAT, l) for l in raw] - - return [(int(pack[0][0]), int(pack[0][1])) for pack in res if pack] - -def read_abacus_out(fileobj, - index=slice(None), - results_required=True, - sort_atoms_with: Optional[List[int]] = None) -> Atoms | List[Atoms]: - '''Reads the ABACUS output files. This function would be called by - the AbacusTemplate.read_results() function. The detailed call stack - is as follows: - get_potential_energy() - -> get_property() - -> calculate() - -> read_results() - -> read_abacus_out() *here* - - To use this function as a standalone one, the fileobj should be - the return of the open() function, which is a TextIOWrapper object: - >>> with open(fn) as fileobj: - ... read_abacus_out(fileobj) - - Parameters - ---------- - fileobj : str | Path | TextIOWrapper - The file object to read. - index : slice - The index of the frames to read. - results_required : bool - Whether the results are required. If True, the results will be - returned. If False, the results will not be returned. This parameter - is not used. - sort_atoms_with: Optional[List[int]] - The sort order of the atoms. If not None, the atoms will be sorted - according to the order in the list. - - Returns - ------- - atoms : Atoms | List[Atoms] - The atoms object, whose calculator is the `SinglePointDFTCalculator`. - ''' - assert isinstance(fileobj, Path) - # it is required that the running log file is passed as the Path object - # so that the band structure file ``eig_occ.txt`` can be located as the - # one in the same folder, otherwise the reading of elecstate will be - # invalid and cause the failure - with open(fileobj) as f: - abacus_lines = f.readlines() - - # read the esolver type - eslvtyp = read_esolver_type_from_running_log(abacus_lines) - # FIXME: implement read_ksdft_esolver_out instead of read_abacus_out to - # make flexible esolver support - - # read the structure, with the cell, elem, etc. (nframe) - # if it is MD run, the trajectories will be in the file MD_dump, instead - # of the running log - is_md_dump = fileobj.name == 'running_md.log' - trajectory = read_traj_from_md_dump(fileobj.parent / 'MD_dump') \ - if is_md_dump else read_traj_from_running_log(abacus_lines) - - # read the eigenvalues (nframe, nk, nbnd) - elecstate = read_band_from_eig_occ(fileobj.parent / 'eig_occ.txt') - - # read the atomic forces (nframe, nat, 3) - forces = read_forces_from_running_log(abacus_lines) - - # read the stress (nframe, 3, 3), but may be None - stress = read_stress_from_running_log(abacus_lines) - - # read all kpoints tables (but only want the first, spinless, with ibz2bz) - k, _, _, _, _ = read_kpoints_from_running_log(abacus_lines) - # unpack the kpoints information - kvecd, wk, _ = k - - # FIXME: in principle, the two spin channels share the same set of - # the kpoints, so it is not needed to have two sets of kpoints - # and it is assumed that the sampling of kpoints won't change during - # the simulation, which is, not exactly to be true for the NPT-MD - # runs. - - # only keep the energies of the final iteration for each ion step - energies = find_final_info_with_iter_header( - read_energies_from_running_log(abacus_lines)[1], - read_iter_header_from_running_log(abacus_lines) - ) - - # read the magmom - magmom = read_magmom_from_running_log(abacus_lines) - - # roughly check the integrity of data from their length consistency - assert len(trajectory) == len(energies), \ - f'Inconsistent length: {len(trajectory)} != {len(energies)}' - assert len(trajectory) == len(elecstate), \ - f'Inconsistent length: {len(trajectory)} != {len(elecstate)}' - if len(forces) == 0: - forces = [None] * len(trajectory) - assert len(trajectory) == len(forces), \ - f'Inconsistent length: {len(trajectory)} != {len(forces)}' - if len(stress) == 0: - stress = [None] * len(trajectory) - assert len(trajectory) == len(stress), \ - f'Inconsistent length: {len(trajectory)} != {len(stress)}' - if len(magmom) == 0: - magmom = [np.zeros(shape=(len(trajectory[0]['elem'])))] * len(trajectory) - - # loop over the frame... - images, ind = [], sort_atoms_with - for frame, estat, mag, frs, strs, ener in zip( - trajectory, elecstate, magmom, forces, stress, energies): - # for each frame, a structure can be defined - ind = ind or list(range(len(frame['elem']))) - # MD_dump lattice vectors are multiples of alat, while its Cartesian - # positions and the running-log structures are already in Angstrom. - cell = frame['cell'] * frame['alat_in_angstrom'] \ - if is_md_dump else frame['cell'] - atoms = Atoms(symbols=np.array(frame['elem'])[ind].tolist(), - positions=frame['coords'][ind], - cell=cell, - pbc=is_md_dump, - magmoms=mag[ind]) - # from result, a calculator can be assembled - # however, sometimes the force and stress is not calculated - # in this case, we set them to None - frs = None if is_invalid_arr(frs) else frs[ind] - strs = None if is_invalid_arr(strs) else full_3x3_to_voigt_6_stress(strs) - calc = SinglePointDFTCalculator(atoms=atoms, energy=ener['E_KohnSham'], - free_energy=ener['E_KohnSham'], - forces=frs, stress=strs, - magmoms=mag[ind], efermi=ener['E_Fermi'], - ibzkpts=kvecd, dipole=None) - # import the eigenvalues and occupations kpoint-by-kpoint - calc.kpts = [] - for ispn, (ekb, occ) in enumerate(zip(estat['e'], estat['occ'])): # loop over the spin - calc.kpts += [SinglePointKPoint(weight=wk[ik], - s=ispn, - k=kvecd[ik], - eps_n=ekb[ik,:], - f_n=occ[ik,:]) - for ik in range(len(kvecd))] - # attach the calculator to the atoms - atoms.calc = calc - images.append(atoms) - - return images[index] - -class TestLatestIO(unittest.TestCase): - ''' - test the validities of functions for the I/O of the latest version of ABACUS - ''' - here = Path(__file__).parent - testfiles = here / 'testfiles' - - def test_read_abacus_out_reorders_calculator_magmoms(self): - import tempfile - from unittest.mock import patch - - frame = { - 'elem': ['Na', 'Na', 'Cl'], - 'coords': np.array([[0.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - [2.0, 0.0, 0.0]]), - 'cell': np.eye(3), - } - elecstate = [{ - 'k': np.zeros((1, 1, 3)), - 'e': np.zeros((1, 1, 1)), - 'occ': np.ones((1, 1, 1)), - }] - energies = [{'E_KohnSham': -1.0, 'E_Fermi': 0.0}] - kpoints = ((np.zeros((1, 3)), np.ones(1), None), None, None, None, None) - - with tempfile.TemporaryDirectory() as tmpdir: - running_log = Path(tmpdir) / 'running_scf.log' - running_log.write_text('') - (Path(tmpdir) / 'eig_occ.txt').write_text('') - with patch(__name__ + '.read_esolver_type_from_running_log', return_value='ksdft'), \ - patch(__name__ + '.read_traj_from_running_log', return_value=[frame]), \ - patch(__name__ + '.read_band_from_eig_occ', return_value=elecstate), \ - patch(__name__ + '.read_forces_from_running_log', return_value=[]), \ - patch(__name__ + '.read_stress_from_running_log', return_value=[]), \ - patch(__name__ + '.read_kpoints_from_running_log', return_value=kpoints), \ - patch(__name__ + '.read_energies_from_running_log', return_value=([], [])), \ - patch(__name__ + '.read_iter_header_from_running_log', return_value=[]), \ - patch(__name__ + '.find_final_info_with_iter_header', return_value=energies), \ - patch(__name__ + '.read_magmom_from_running_log', return_value=[np.array([10.0, 20.0, 30.0])]): - atoms = read_abacus_out(running_log, sort_atoms_with=[0, 2, 1])[0] - - self.assertEqual(atoms.get_chemical_symbols(), ['Na', 'Cl', 'Na']) - self.assertTrue(np.allclose(atoms.calc.results['magmoms'], [10.0, 30.0, 20.0])) - - def test_read_esolver_type_from_running_log(self): - self.assertEqual( - read_esolver_type_from_running_log( - self.testfiles / 'lcao-symm0-nspin2-multik-cellrelax'), - 'ksdft_lcao' - ) - self.assertEqual( - read_esolver_type_from_running_log( - self.testfiles / 'lcao-symm0-nspin2-multik-relax'), - 'ksdft_lcao' - ) - self.assertEqual( - read_esolver_type_from_running_log( - self.testfiles / 'lcao-symm1-nspin1-multik-scf'), - 'ksdft_lcao' - ) - self.assertEqual( - read_esolver_type_from_running_log( - self.testfiles / 'pw-symm0-nspin4-gamma-md'), - 'ksdft_pw' - ) - - def test_read_band_from_running_log(self): - self.assertIsNone( - read_band_from_running_log(self.testfiles / 'lcao-symm0-nspin2-multik-cellrelax') - ) - - def test_read_band_from_eig_occ(self): - fn = self.testfiles / 'nspin4-gamma-eigocc' - elecstate = read_band_from_eig_occ(fn) - # 2 frames, 1 spin channel (nspin 4), 1 kpoint, 35 bands - self.assertEqual(len(elecstate), 2) - for es in elecstate: # loop over frames - self.assertIn('k', es) - self.assertEqual(es['k'].shape, (1, 1, 3)) # ispin, ik, 3 - self.assertIn('e', es) - self.assertEqual(es['e'].shape, (1, 1, 35)) # ispin, ik, nbnd - self.assertIn('occ', es) - self.assertEqual(es['occ'].shape, (1, 1, 35)) # ispin, ik, nbnd - - def test_read_traj_from_running_log(self): - traj = read_traj_from_running_log(self.testfiles / 'lcao-symm0-nspin2-multik-cellrelax') - self.assertEqual(len(traj), 3) - - reference = [ - np.array([[4.17203 , 2.086015, 2.086015], - [2.086015, 4.17203 , 2.086015], - [2.086015, 2.086015, 4.17203 ]]), - np.array([[4.17550947, 2.05376104, 2.05376104], - [2.05811246, 4.12078078, 2.09979522], - [2.05811246, 2.09979522, 4.12078078]]), - np.array([[4.17667347, 2.04296382, 2.04296382], - [2.04876712, 4.10361705, 2.10440948], - [2.04876712, 2.10440948, 4.10361705]]) - ] - - taud = np.array([[0. , 1. , 1. ], - [0.5 , 0.5 , 0.5 ], - [0.25, 0.25, 0.25], - [0.75, 0.75, 0.75]]) - for t, c in zip(traj, reference): - self.assertEqual(t['coordinate'], 'Direct') - self.assertEqual(t['cell_unit'], 'Angstrom'), - self.assertEqual(t['alat_in_angstrom'], 4.17203) - self.assertTrue(all(e1 == e2 for e1, e2 in zip(t['elem'], np.array(['Ni1', 'Ni2', 'O', 'O'])))) - self.assertTrue(np.allclose(t['cell'], c)) - self.assertTrue(np.allclose(t['coords'] % 1, taud % 1)) - - def test_read_forces_from_running_log(self): - forces = read_forces_from_running_log(self.testfiles / 'lcao-symm0-nspin2-multik-relax') - self.assertEqual(len(forces), 2) # two frames - reference = [ - np.array([ - [-1.2994798821, -0.7502719805, 0.0000000000], - [ 1.2994798821, 0.7502719805, 0.0000000000], - ]), - np.array([ - [-1.6814741322, -0.9708025265, 0.0000000000], - [ 1.6814741322, 0.9708025265, 0.0000000000], - ]), - ] - for f, ref in zip(forces, reference): - self.assertTrue(np.allclose(f, ref)) - - def test_read_stress_from_running_log(self): - stresses = read_stress_from_running_log( - self.testfiles / 'lcao-symm0-nspin2-multik-cellrelax') - self.assertEqual(len(stresses), 2) # two frames - reference = [ - np.array([ - [ 28.0257165781, -25.0814687477, -25.0814687477], - [-25.0814687477, -57.2309475134, 52.8147857580], - [-25.0814687477, 52.8147857580, -57.2309475134], - ]), - np.array([ - [ 10.5133147463,-28.8166870580, -28.8166870559], - [-28.8166870580, 0.1640200010, 7.6016822662], - [-28.8166870559, 7.6016822662, 0.1640199977], - ]), - ] - for s, ref in zip(stresses, reference): - self.assertTrue(np.allclose(s, -0.1 * GPa * ref)) - - def test_read_abacus_out_string(self): - fn = self.testfiles / 'pw-symm0-nspin4-gamma-md' - # the case that does not give the instance of Path - with self.assertRaises(AssertionError): - read_abacus_out(str(fn)) - - def test_read_pw_symm0_nspin4_gamma_md(self): - # make files ready - shutil.copy(self.testfiles / 'pw-symm0-nspin4-gamma-md', - self.testfiles / 'running_md.log') - shutil.copy(self.testfiles / 'nspin4-gamma-eigocc', - self.testfiles / 'eig_occ.txt') - shutil.copy(self.testfiles / 'nspin4-gamma-mddump', - self.testfiles / 'MD_dump') - - res = read_abacus_out(self.testfiles / 'running_md.log') - self.assertIsNotNone(res) - self.assertEqual(len(res), 2) # two frames - - for atoms in res: - self.assertIsInstance(atoms, Atoms) - self.assertIsInstance(atoms.calc, SinglePointDFTCalculator) - self.assertGreater(len(atoms.calc.kpts), 0) - # Gamma point calculation - for k in atoms.calc.kpts: - self.assertIsInstance(k, SinglePointKPoint) - - # remove the files - (self.testfiles / 'running_md.log').unlink() - (self.testfiles / 'eig_occ.txt').unlink() - (self.testfiles / 'MD_dump').unlink() - - def test_read_iter_header_from_running_log(self): - header = read_iter_header_from_running_log( - self.testfiles / 'lcao-symm0-nspin2-multik-cellrelax') - self.assertTupleEqual(header[0], (1, 1)) - self.assertTupleEqual(header[1], (1, 2)) - self.assertTupleEqual(header[2], (2, 1)) - -if __name__ == '__main__': - unittest.main() diff --git a/interfaces/ASE_interface/abacuslite/io/legacyio.py b/interfaces/ASE_interface/abacuslite/io/legacyio.py deleted file mode 100644 index 223a31b52d1..00000000000 --- a/interfaces/ASE_interface/abacuslite/io/legacyio.py +++ /dev/null @@ -1,1261 +0,0 @@ -import re -import shutil -import unittest -from io import TextIOWrapper -from pathlib import Path -from typing import Dict, List, Tuple, Optional, Any - -import numpy as np -from ase.atoms import Atoms -from ase.calculators.singlepoint import ( - SinglePointKPoint, - SinglePointDFTCalculator -) -from ase.units import Ry, eV, GPa, bar -from ase.stress import full_3x3_to_voigt_6_stress -# from ase.utils import reader - -__all__ = ['read_kpoints_from_running_log', - 'read_band_from_running_log', 'read_traj_from_running_log', - 'read_traj_from_md_dump', 'read_forces_from_running_log', - 'read_stress_from_running_log', 'read_energies_from_running_log', - 'read_abacus_out'] - -def parse_kpoints_table(raw: List[str]) \ - -> Tuple[np.ndarray, np.ndarray, List[int] | None]: - '''parse the lines that are the kpoints table - - Parameters - ---------- - raw : list of str - The lines of the kpoints table. - - Returns - ------- - tuple - A tuple containing the kpoints coordinates, weights and ibz2bz indexing, - or without the indexing. - ''' - # the pattern with the ibz2bz indexing - W_PAT_ = r'\s*(\d+)' - W_PAT_ += r'\s+(-?\d\.\d+)\s+(-?\d\.\d+)\s+(-?\d\.\d+)' - W_PAT_ += r'\s+(\d\.\d+)\s+(\d+)' - # the pattern without the ibz2bz indexing - WOPAT_ = r'\s*(\d+)' - WOPAT_ += r'\s+(-?\d\.\d+)\s+(-?\d\.\d+)\s+(-?\d\.\d+)' - WOPAT_ += r'\s+(\d\.\d+)' - # for each line, must match either the first or the second - - with_ibz = all(re.match(W_PAT_, l) for l in raw) - assert with_ibz or all(re.match(WOPAT_, l) for l in raw), \ - f'Unexpected format of kpoints table: {raw}' - if with_ibz: - data = [re.match(W_PAT_, l).groups() for l in raw] - k = np.array([list(map(float, ki[1:4])) for ki in data]) - w = np.array([float(ki[4]) for ki in data]) - ibz2bz = [int(ki[5]) for ki in data] - return k, w, ibz2bz - # otherwise, it must be the case without the ibz2bz - data = [re.match(WOPAT_, l).groups() for l in raw] - k = np.array([list(map(float, ki[1:4])) for ki in data]) - w = np.array([float(ki[4]) for ki in data]) - return k, w, None - -def read_kpoints_from_running_log(src: str | Path | List[str]) \ - -> Tuple[Tuple[np.ndarray, np.ndarray, List[int]], - Tuple[np.ndarray, np.ndarray], - List[Tuple[np.ndarray, np.ndarray]], - List[Tuple[np.ndarray, np.ndarray]], - List[Tuple[np.ndarray, np.ndarray]]]: - ''' - read the kpoint coordinates and weights from the running log file. Up to now, - the ABACUS version 3.10.1 LTS, the ABACUS will print 4 tables of kpoints at - first, including the one with the ibz2bz indexing, one of spinless direct - coordinates, then two spin cartesian and direct coordinates of kpoints. If - the cell would change during the calculation, then after each cell-relax step, - there will be 3 tables, including direct, cartesian and then direct (again, - but i don't know why!) kpoint coordinates. - - Parameters - ---------- - fn : str - The path to the ABACUS running log file. - - Returns - ------- - tuple - A tuple contains the first table (with ibz2bz indexing), the second table - (spinless direct coordinates), the trajectories of the third (spin-direct), - fourth (spin-cartesian) and fifth (spin-direct) tables. - ''' - if isinstance(src, (str, Path)): - with open(src) as f: - raw = f.readlines() - else: # assume the src is the return of the readlines() - raw = src - # with open(fn) as f: - # raw = f.readlines() - raw = [l.strip() for l in raw if l.strip()] - raw = [l for l in raw if l] # remove empty lines - - # the pattern of table header - THPAT_ = r'\s*(IBZ|KPOINTS)' - THPAT_ += r'\s+(DIRECT|CARTESIAN)_X\s+(DIRECT|CARTESIAN)_Y\s+(DIRECT|CARTESIAN)_Z' - THPAT_ += r'\s+WEIGHT(\s+ibz2bz)?' - # the pattern of table content, optionally with the ibz2bz data - TBPAT_ = r'\s*\d+' - TBPAT_ += r'\s+(-?\d(\.\d+)?)\s+(-?\d(\.\d+)?)\s+(-?\d(\.\d+)?)' - TBPAT_ += r'\s+(\d(\.\d+)?)(\s+\d+)?' - - # search for the kpoints table - tables, istart = [], 0 - while istart < len(raw): - # search for the table header... - ith = None - for i, l in enumerate(raw[istart:]): - if re.match(THPAT_, l): # find a table header - ith = i - break - if ith is None: # no more table - break - itb = ith + 1 # we assume there is no more header before the data - assert re.match(TBPAT_, raw[istart+itb]) # really? - - # search for the end of the table... - jtb = None - for i, l in enumerate(raw[istart+itb:]): - if not re.match(TBPAT_, l): # the next line is not a table content - jtb = i - break - if jtb is None: # no more table - break - # parse the table - ktab_raw = raw[istart+itb:istart+itb+jtb] - tables.append(parse_kpoints_table(ktab_raw)) - - # update the starting point to search for the next table - istart += itb + jtb + 1 - - # sort tables... - kibz = tables.pop(0) - # get the number of kpoints, then will use in reshaping the kpoints trajectories - nk, _ = kibz[0].shape - kspnls = (tables[0][0], tables[0][1]) # spinless direct, drop the None in [2] - kd1traj = [(t[0].reshape(-1, nk, 3), t[1].reshape(-1, nk)) # drop the [2] because it is None - for i, t in enumerate(tables) if i % 3 == 0] - kctraj = [(t[0].reshape(-1, nk, 3), t[1].reshape(-1, nk)) # drop the [2] because it is None - for i, t in enumerate(tables) if i % 3 == 1] - kd2traj = [(t[0].reshape(-1, nk, 3), t[1].reshape(-1, nk)) # drop the [2] because it is None - for i, t in enumerate(tables) if i % 3 == 2] - return kibz, kspnls, kd1traj, kctraj, kd2traj - -def read_esolver_type_from_running_log(src: str | Path | List[str]) \ - -> str: - ''' - read the esolver type from the ABACUS running log file. - - Parameters - ---------- - fn : str - The path to the ABACUS running log file. - - Returns - ------- - str - The esolver type used in the ABACUS calculation. - ''' - if isinstance(src, (str, Path)): - with open(src) as f: - raw = f.readlines() - else: # assume the src is the return of the readlines() - raw = src - # with open(fn) as f: - # raw = f.readlines() - raw = [l.strip() for l in raw] - raw = [l for l in raw if l] # remove empty lines - - # search for the line with information like: - # "The esolver type has been set to : lj_pot" - lines = [ - re.match(r'The esolver type has been set to : (\S+)', - l) for l in raw - ] - eslvtyp = [m.group(1) for m in lines if m is not None] - assert len(set(eslvtyp)) == 1, \ - f'Inconsistent esolver type: {set(eslvtyp)}' - return eslvtyp[0] - -def read_band_from_running_log(src: str | Path | List[str]) \ - -> List[Dict[str, np.ndarray]]: - ''' - read the band structure from the ABACUS running log file. This would be - helpful for MD case. - - Parameters - ---------- - src : str or Path or list of str - The path to the ABACUS running log file or the return of the readlines() method. - - Returns - ------- - list of dict - A list of dictionaries containing the k-points and band energies. - Each dictionary has keys 'k' and 'e', where 'k' is a list of k-point - coordinates and 'e' is a numpy array of band energies. - ''' - masknspin = lambda n: 1 if n in [1, 4] else n - - if isinstance(src, (str, Path)): - with open(src) as f: - raw = f.readlines() - else: # assume the src is the return of the readlines() - raw = src - # with open(fn) as f: - # raw = f.readlines() - raw = [l.strip() for l in raw] - raw = [l for l in raw if l] # remove empty lines - - # search for the number of spin channels, bands and kpoints first - # r'nspin\s+=\s+(\d+)' - nspin = [re.search(r'nspin\s+=\s+(\d+)', l) for l in raw] - nspin = [int(n.group(1)) for n in nspin if n] - assert len(set(nspin)) == 1, \ - f'Inconsistent number of spins: {set(nspin)}' - nspin = nspin[0] - # r'NBANDS\s+=\s+(\d+)' - nbnd = [re.search(r'NBANDS\s+=\s+(\d+)', l) for l in raw] - nbnd = [int(n.group(1)) for n in nbnd if n] - assert len(set(nbnd)) == 1, \ - f'Inconsistent number of bands: {set(nbnd)}' - nbnd = nbnd[0] - # r'nkstot\snow\s=\s(\d+)' - # (there may be not the nkstot_ibz for symmetry 0 case) - # symmetry 1 - nkibz = [re.search(r'nkstot_ibz\s+=\s+(\d+)', l) for l in raw] - nkibz = [int(n.group(1)) for n in nkibz if n] - # symmetry 0, -1 - nktot = [re.search(r'nkstot\s+=\s+(\d+)', l) for l in raw] - nktot = [int(n.group(1)) for n in nktot if n] - nk = nktot if len(nkibz) == 0 else nkibz - assert len(set(nk)) == 1, \ - f'Inconsistent number of k-points: {set(nk)}' - nk = nk[0] - - # extract the band, first find the leading line - # 333/511 kpoint (Cartesian) = 0.13022 0.052050 -6.5974e-10 (190 pws) - ekb_leading_pat = r'\d+/\d+\s+kpoint\s+\(Cartesian\)\s+=\s+' \ - + r'(-?\d(\.\d+)?(e-\d+)?)\s+' \ - + r'(-?\d(\.\d+)?(e-\d+)?)\s+' \ - + r'(-?\d(\.\d+)?(e-\d+)?)\s+' \ - + r'\(\d+\s+pws\)' - iekb = [i for i, l in enumerate(raw) if re.match(ekb_leading_pat, l)] - assert len(iekb) > 0, f'No k-point found' - assert len(iekb) % (masknspin(nspin)*nk) == 0, \ - f'Inconsistent number of k-points: {len(iekb)} vs {nk}' - k_raw = [re.match(ekb_leading_pat, raw[i]).groups() for i in iekb] - k = np.array([list(map(float, ki[::3])) for ki in k_raw]) - assert k.shape == (len(iekb), 3), \ - f'Unexpected shape of k-points: {k.shape}, expected ({len(iekb)}, 3)' - - nframe = len(iekb) // (masknspin(nspin)*nk) # number of frames, for MD or relax tasks - ekb_raw = [] - for i in iekb: - rows = [] - j = i + 1 - while j < len(raw) and len(rows) < nbnd: - if re.match(ekb_leading_pat, raw[j]): - break - parts = raw[j].strip().split() - if len(parts) >= 3 and parts[0].isdigit(): - try: - band_index = int(parts[0]) - if band_index == len(rows) + 1: - rows.append([float(parts[0]), float(parts[1]), float(parts[2])]) - except ValueError: - pass - j += 1 - assert len(rows) == nbnd, \ - f'Unexpected number of band rows: {len(rows)} vs {nbnd}' - ekb_raw.extend(rows) - ekb_raw = np.array(ekb_raw) - assert ekb_raw.shape == (nframe * masknspin(nspin) * nk * nbnd, 3), \ - f'Unexpected shape of band energies: {ekb_raw.shape}. ' \ - f'Expected ({nframe * masknspin(nspin) * nk * nbnd}, 3), in which ' \ - f'nframe={nframe}, nspin={nspin}, nk={nk}, nbnd={nbnd}' - ekb = ekb_raw[:, 1].reshape(nframe, masknspin(nspin), nk, nbnd) - occ = ekb_raw[:, 2].reshape(nframe, masknspin(nspin), nk, nbnd) - # reshape the k-points to (nframe, nspin, nk, 3) - k = k.reshape(nframe, masknspin(nspin), nk, 3) - - return [{'k': ki, 'e': eki, 'occ': occi} - for ki, eki, occi in zip(k, ekb, occ)] - -def read_traj_from_running_log(src: str | Path | List[str]) \ - -> List[Dict[str, np.ndarray|str]]: - ''' - read the trajectory from the ABACUS running log file. This would be - helpful for MD case. - - Parameters - ---------- - fn : str - The path to the ABACUS running log file. - - Returns - ------- - list of dict - A list of dictionaries containing the coordinate system, cell, elements - and the coordinates of the atoms. Each dictionary has keys 'coordinate', - 'cell', 'cell_unit', 'alat_in_angstrom', 'elem', and 'coords'. - The values are numpy arrays or strings. - - 'coordinate': string, the coordinate system, e.g., 'Cartesian' or 'Direct' - - 'cell': numpy array of shape (3, 3) - - 'cell_unit': string, the unit of the cell, e.g., 'Angstrom' - - 'alat_in_angstrom': float, the lattice constant in Angstrom - - 'elem': list of strings, the chemical symbols of the elements - - 'coords': numpy array of shape (natoms, 3), the coordinates of the atoms - ''' - if isinstance(src, (str, Path)): - with open(src) as f: - raw = f.readlines() - else: # assume the src is the return of the readlines() - raw = src - # with open(fn) as f: - # raw = f.readlines() - raw = [l.strip() for l in raw] - raw = [l for l in raw if l] # remove empty lines - - # search for the total number of atoms - # r'TOTAL ATOM NUMBER = (\d+)' - natoms = [re.search(r'TOTAL ATOM NUMBER\s*=\s*(\d+)', l) for l in raw] - natoms = [int(n.group(1)) for n in natoms if n] - assert len(set(natoms)) == 1, \ - f'Inconsistent number of atoms: {set(natoms)}' - natoms = natoms[0] - - # search for the coordinate system - # r'^([DIRECT|CARTESIAN]) COORDINATES$' - coordinate = [re.match(r'^(DIRECT|CARTESIAN) COORDINATES', l) for l in raw] - coordinate = [l.group(1).lower().capitalize() for l in coordinate if l] - assert len(set(coordinate)) == 1, \ - f'Inconsistent coordinate system: {set(coordinate)}' - coordinate = coordinate[0] - - # search for the cell, but first get the "a0": lattice constant - # r'lattice constant (Angstrom) = (-?\d+(\.\d+)?)' - a0 = [re.search(r'lattice constant \(Angstrom\)\s*=\s*(-?\d+(\.\d+)?)', l, - re.IGNORECASE) for l in raw] - a0 = [float(n.group(1)) for n in a0 if n] - assert len(set(a0)) == 1, f'Inconsistent lattice constant: {set(a0)}' - a0 = a0[0] - # then the cell - # r'^Lattice vectors: \(Cartesian coordinate: in unit of a\_0\)$' - icell = [i for i, l in enumerate(raw) - if re.match(r'^Lattice vectors: \(Cartesian coordinate: in unit of a_0\)$', l)] - assert len(icell) > 0, f'No cell found' - # nframe = len(icell) # will be 1 for NVT MD - # assert nframe > 0, f'Invalid trajectory with length {nframe}') - cell_raw = [raw[i+1:i+1+3] for i in icell] - cell = [np.array([list(map(float, l.split())) for l in c]) * a0 - for c in cell_raw] # convert to Angstrom - assert all(c.shape == (3, 3) for c in cell), \ - f'Unexpected shape of cell: {[c.shape for c in cell]}' - - # search for the elements and coordinates - # r'^tau[c|d]_([A-Z][a-z]?)\d+\s+(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)*' - m_tau = [re.match(r'^tau[c|d]_([A-Z][a-z]?)\d+' - r'\s+(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)', l) - for l in raw] - m_tau = [m for m in m_tau if m] - assert len(m_tau) > 0, f'No atoms found' - nframe = len(m_tau) // natoms - assert nframe > 0, f'Invalid trajectory with length {nframe}' - elem = [m.group(1) for m in m_tau] - coords = np.array([[float(m.group(2)), float(m.group(4)), float(m.group(6))] - for m in m_tau]).reshape(-1, natoms, 3) - assert coords.shape == (nframe, natoms, 3), \ - f'Unexpected shape of coordinates: {coords.shape}, ' \ - f'expected ({nframe}, {natoms}, 3)' - assert len(elem) == natoms * nframe, \ - f'Unexpected number of elements: {len(elem)}, ' \ - f'expected {natoms * nframe}' - elem = [elem[i:i+natoms] for i in range(0, len(elem), natoms)] - - # final: for volume-constant run, the cell information will only be printed for once - cell = np.array(cell).reshape(-1, 3, 3) - if cell.shape[0] == 1: - cell = [cell[0] for _ in range(nframe)] - assert len(cell) == nframe, \ - f'Unexpected number of cells: {len(cell)}, ' \ - f'expected {nframe}' - return [{ - 'coordinate': coordinate, - 'cell': c, - 'cell_unit': 'Angstrom', - 'alat_in_angstrom': a0, - 'elem': e, - 'coords': co - } for c, e, co in zip(cell, elem, coords)] - -def read_traj_from_md_dump(src: str | Path | List[str]) \ - -> List[Dict[str, np.ndarray]]: - ''' - read the trajectory from the ABACUS MD dump file - - Parameters - ---------- - fn : str - The path to the ABACUS MD dump file. - - Returns - ------- - list of dict - A list of dictionaries containing the coordinate system, cell, elements - and the coordinates of the atoms. Each dictionary has keys 'coordinate', - 'cell', 'alat_in_angstrom', 'elem', and 'coords'. - The values are numpy arrays or strings. - ''' - if isinstance(src, (str, Path)): - with open(src) as f: - raw = f.readlines() - else: # assume the src is the return of the readlines() - raw = src - # with open(fn) as f: - # raw = f.readlines() - raw = [l.strip() for l in raw] - raw = [l for l in raw if l] # remove empty lines - - # search for the lattice constant - # r'LATTICE_CONSTANT:\s+(-?\d+(\.\d+)?) Angstrom' - a0 = [re.search(r'LATTICE_CONSTANT:\s+(-?\d+(\.\d+)?) Angstrom', l) for l in raw] - a0 = [float(n.group(1)) for n in a0 if n] - assert len(set(a0)) == 1, \ - f'Inconsistent lattice constant: {set(a0)}' - a0 = a0[0] - # search for the cell - # r'^LATTICE_VECTORS$' - icell = [i for i, l in enumerate(raw) if re.match(r'^LATTICE_VECTORS$', l)] - assert len(icell) > 0, f'No cell found in file' - cell_raw = [raw[i+1:i+1+3] for i in icell] - cell = [np.array([list(map(float, l.split())) for l in c]) - for c in cell_raw] - assert all(c.shape == (3, 3) for c in cell), \ - f'Unexpected shape of cell: {[c.shape for c in cell]}' - nframe = len(cell) - - # search for the elements and coordinates - # r'^\d+\s+([A-Z][a-z]?)\s+(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)*' - m_tau = [re.match(r'^\d+\s+([A-Z][a-z]?)\s+' - r'(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)', l) - for l in raw] - m_tau = [m for m in m_tau if m] - assert len(m_tau) > 0, f'No atoms found' - natoms = len(m_tau) // nframe - assert natoms > 0, f'Invalid trajectory with length {nframe}.' - elem = [m.group(1) for m in m_tau] - coords = np.array([[float(m.group(2)), float(m.group(4)), float(m.group(6))] - for m in m_tau]).reshape(nframe, natoms, 3) - assert coords.shape == (nframe, natoms, 3), \ - f'Unexpected shape of coordinates: {coords.shape}, ' \ - f'expected ({nframe}, {natoms}, 3)' - assert len(elem) == natoms * nframe, \ - f'Unexpected number of elements: {len(elem)}, ' \ - f'expected {natoms * nframe}' - elem = [elem[i:i+natoms] for i in range(0, len(elem), natoms)] - assert len(elem) == nframe, \ - f'Unexpected number of elements: {len(elem)}, ' \ - f'expected {nframe}' - - return [{ - 'coordinate': 'Cartesian', - 'cell': c, - 'alat_in_angstrom': a0, - 'elem': e, - 'coords': co - } for c, e, co in zip(cell, elem, coords)] - -def read_forces_from_running_log(src: str | Path | List[str]) \ - -> List[np.ndarray]: - '''''' - if isinstance(src, (str, Path)): - with open(src) as f: - raw = f.readlines() - else: # assume the src is the return of the readlines() - raw = src - # with open(fn) as f: - # raw = f.readlines() - raw = [l.strip() for l in raw] - raw = [l for l in raw if l] # remove empty lines - - # iteratively search for the forces, which led by the title `TOTAL-FORCE` - forces, istart = [], 0 - - while istart < len(raw): - ith = None # index of the table header - for i, l in enumerate(raw[istart:]): - if re.match(r'\s*TOTAL\-FORCE\s*\(eV\s*/Angstrom\)', l, re.IGNORECASE): - ith = i - break - if ith is None: # no forces found - break - # otherwise - # search for the first line that matches the pattern - FORCEPAT_ = r'\s*([A-Z][a-z]?\d+)\s+(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)' - itb = None # index of the first line of the table body - for i, l in enumerate(raw[istart+ith+1:]): - if re.match(FORCEPAT_, l): - itb = i - break - if itb is None: # no content found - break - # otherwise - jtb = None # index of the last line of the table body - for j, l in enumerate(raw[istart+ith+1+itb:]): - if not re.match(FORCEPAT_, l): - jtb = j - break - if jtb is None: # no content found - break - - # truncate the force table and append - force_raw = raw[istart+ith+1+itb:istart+ith+1+itb+jtb] - force = np.array([list(map(float, l.split()[1:])) for l in force_raw]) - forces.append(force) - - # update the istart - istart += ith + itb + jtb + 1 - - return forces - -def read_stress_from_running_log(src: str | Path | List[str]) \ - -> List[np.ndarray]: - '''''' - if isinstance(src, (str, Path)): - with open(src) as f: - raw = f.readlines() - else: # assume the src is the return of the readlines() - raw = src - # with open(fn) as f: - # raw = f.readlines() - raw = [l.strip() for l in raw] - raw = [l for l in raw if l] # remove empty lines - - # iteratively search for the stress, which led by the title `TOTAL-STRESS` - stresses, istart = [], 0 - - while istart < len(raw): - ith = None # index of the table header - for i, l in enumerate(raw[istart:]): - if re.match(r'\s*TOTAL\-STRESS\s*\(KBAR\)', l, re.IGNORECASE): - ith = i - break - if ith is None: # no stress found - break - # otherwise - # search for the first line that matches the pattern - STRESSPAT_ = r'\s*(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)' - itb = None # index of the first line of the table body - for i, l in enumerate(raw[istart+ith+1:]): - if re.match(STRESSPAT_, l): - itb = i - break - if itb is None: # no content found - break - # otherwise - jtb = 3 # because the stress tensor would be a (3, 3)-matrix - - # truncate the stress table and append - stress_raw = raw[istart+ith+1+itb:istart+ith+1+itb+jtb] - stress = np.array([list(map(float, l.split())) for l in stress_raw]).reshape(3, 3) - # unit: kbar -> GPa - stresses.append(-0.1 * stress * GPa) - - # update the istart - istart += ith + itb + jtb + 1 - - return stresses - -def read_energies_from_running_log(src: str | Path | List[str]) \ - -> List[Dict[str, float]]: - '''''' - if isinstance(src, (str, Path)): - with open(src) as f: - raw = f.readlines() - else: # assume the src is the return of the readlines() - raw = src - # with open(fn) as f: - # raw = f.readlines() - raw = [l.strip() for l in raw] - raw = [l for l in raw if l] # remove empty lines - - energies_ry, energies_ev, istart = [], [], 0 - - while istart < len(raw): - ith = None # index of the table header - for i, l in enumerate(raw[istart:]): - if re.match(r'\s*ENERGY\s+Rydberg\s+eV', l, re.IGNORECASE): - ith = i - break - if ith is None: # no energies found - break - # otherwise - # search for the first line that matches the pattern - ENERGYPAT_ = r'\s*E_(\S+)\s+(-?\d+(\.\d+)?)\s+(-?\d+(\.\d+)?)' - itb = None # index of the first line of the table body - for i, l in enumerate(raw[istart+ith+1:]): - if re.match(ENERGYPAT_, l): - itb = i - break - if itb is None: # no content found - break - # otherwise - jtb = None - for j, l in enumerate(raw[istart+ith+1+itb:]): - if not re.match(ENERGYPAT_, l): - jtb = j - break - if jtb is None: # no content found - break - - # truncate the energy table and append - tb_raw = raw[istart+ith+1+itb:istart+ith+1+itb+jtb] - # first item is the name of the energy component - # the second and third items are the ry and ev values - name, e_ry, e_ev = zip(*[l.split() for l in tb_raw]) - energies_ry.append(dict(zip(name, list(map(float, e_ry))))) - energies_ev.append(dict(zip(name, list(map(float, e_ev))))) - - # update the istart - istart += ith + itb + jtb + 1 - - return energies_ry, energies_ev - -def read_magmom_from_running_log(src: str | Path | List[str]) \ - -> List[np.ndarray]: - ''' - Read the magnetic momentum from ABACUS running log. Note: - this function returns the list of np.ndarray which has dimension of - (nat, 3), where nat is the number of atoms, and 3 stands for - the x, y and z components. - There will be problem if the output directly imported to the - ASE SinglePointDFTCalculator, because the magmom required by - ASE-side is the (N,) array, so another operation `np.linalg.norm - (magmom, axis=1)` should be called to postprocess the magmom. - - Parameters - ---------- - src : str | Path | List[str] - The source of the running log. If it is a string, it will be - treated as the path to the running log. If it is a list of - strings, it will be treated as the return of the readlines() - function. - - Returns - ------- - magmom : List[np.ndarray] - The trajectories of the magnetic moment of each atom. - ''' - if isinstance(src, (str, Path)): - with open(src) as f: - raw = f.readlines() - else: # assume the src is the return of the readlines() - raw = src - # with open(fn) as f: - # raw = f.readlines() - raw = [l.strip() for l in raw] - raw = [l for l in raw if l] # remove empty lines - - magmom, istart = [], 0 - while istart < len(raw): - ith = None # index of the table header - for i, l in enumerate(raw[istart:]): - if re.match(r'\s*Total\sMagnetism\s\(uB\)(\s+x\s+y\s+z)?\s*$', l, - re.IGNORECASE): - ith = i - break - if ith is None: # no magmom found - break - # otherwise - # search for the first line that matches the pattern - MAGMOMPAT_ = r'\s*([A-Z][a-z]?\d+)' - MAGMOMPAT_ += r'\s+([-+]?\d+\.\d+)' - MAGMOMPAT_ += r'(\s+([-+]?\d+\.\d+)\s+([-+]?\d+\.\d+))?\s*' - itb = None # index of the first line of the table body - for i, l in enumerate(raw[istart+ith+1:]): - if re.match(MAGMOMPAT_, l): - itb = i - break - if itb is None: # no content found - break - # otherwise - jtb = None - for j, l in enumerate(raw[istart+ith+1+itb:]): - if not re.match(MAGMOMPAT_, l): - jtb = j - break - if jtb is None: # no content found - break - - # truncate the magmom table and append - tb_raw = raw[istart+ith+1+itb:istart+ith+1+itb+jtb] - # first item is the name of the magmom component - # the second to fourth items are the x, y, z components - res = list(zip(*[l.split() for l in tb_raw]))[1:] - assert len(res) in [1, 3] # colinear or non-colinear case - if len(res) == 1: # colinear case - magmom.append(np.array([list(map(float, res[-1]))]).flatten()) - else: # non-colinear case - mx, my, mz = res - magmom.append(np.array([list(map(float, (mx, my, mz))) - for mx, my, mz in zip(mx, my, mz)])) - # update the istart - istart += ith + itb + jtb + 1 - - return magmom - -def read_iter_header_from_running_log(src: str | Path | List[str]) \ - -> List[Tuple[int, int]]: - ''' - read the "iteration header" from the running log, useful for determining - which is the final iteration. The "iteration header" is defined as: - ``` - LCAO ALGORITHM --------------- ION= 1 ELEC= 100--------------------- - ``` - or for PW: - ``` - PW ALGORITHM --------------- ION=2 ELEC=1 ----------------------- - ``` - ''' - if isinstance(src, (str, Path)): - with open(src) as f: - raw = f.readlines() - else: # assume the src is the return of the readlines() - raw = src - # with open(fn) as f: - # raw = f.readlines() - raw = [l.strip() for l in raw] - raw = [l for l in raw if l] # remove empty lines - - HEADER_PAT = r'(LCAO|PW)\s+ALGORITHM\s+-+\s+ION=\s+([0-9]+)\s+ELEC=\s+([0-9]+)' - res = [re.findall(HEADER_PAT, l) for l in raw] - - return [(int(pack[0][1]), int(pack[0][2])) for pack in res if pack] - -def find_final_info_with_iter_header( - info: List[Any], - headers: List[Tuple[int, int]] - ) -> List[Any]: - '''find the energies of the final iteration - - Parameters - ---------- - info: List[Dict[str, float]] - The information that is printed after each iteration. - headers: List[Tuple[int, int]] - The "iteration header" returned from the function - `read_iter_header_from_running_log` - - Returns - ------- - List[Any] - The information of the final iteration. - ''' - headers = np.array(headers) # because indices should start from 0 - assert headers.ndim == 2 - assert headers.shape[1] == 2 - - ion = headers[:, 0].flatten() - ion, nelec = np.unique(ion, return_counts=True) - ielec = np.cumsum(nelec) - 1 - - return [info[i] for i in ielec] - -def is_invalid_arr(arr) -> bool: - '''Check if the array is invalid, including the cases of None, - empty array, and array with NaN values. - - Parameters - ---------- - arr : np.ndarray - The array to check. - - Returns - ------- - is_invalid : bool - Whether the array is invalid. - ''' - if arr is None: - return True - if isinstance(arr, list): - if len(arr) == 0: - return True - if isinstance(arr, np.ndarray): - if len(arr) == 0: - return True - if np.isnan(arr).any(): - return True - return False - -# @reader -def read_abacus_out(fileobj, - index=slice(None), - results_required=True, - sort_atoms_with: Optional[List[int]] = None) -> Atoms | List[Atoms]: - '''Reads the ABACUS output files. This function would be called by - the AbacusTemplate.read_results() function. The detailed call stack - is as follows: - get_potential_energy() - -> get_property() - -> calculate() - -> read_results() - -> read_abacus_out() *here* - - To use this function as a standalone one, the fileobj should be - the return of the open() function, which is a TextIOWrapper object: - >>> with open(fn) as fileobj: - ... read_abacus_out(fileobj) - - Parameters - ---------- - fileobj : str | Path | TextIOWrapper - The file object to read. - index : slice - The index of the frames to read. - results_required : bool - Whether the results are required. If True, the results will be - returned. If False, the results will not be returned. This parameter - is not used. - sort_atoms_with : Optional[List[int]] - The index of the atoms to sort. If None, the atoms will not be sorted. - - Returns - ------- - atoms : Atoms | List[Atoms] - The atoms object, whose calculator is the `SinglePointDFTCalculator`. - ''' - if isinstance(fileobj, (str, Path)): - with open(fileobj) as f: - abacus_lines = f.readlines() - else: # from the `with open(fn) as fileobj:` context - assert isinstance(fileobj, TextIOWrapper) - abacus_lines = fileobj.readlines() - - # read the esolver type - eslvtyp = read_esolver_type_from_running_log(abacus_lines) - - # read the structure, with the cell, elem, etc. (nframe) - trajectory = read_traj_from_running_log(abacus_lines) - # read the eigenvalues (nframe, nk, nbnd) - elecstate = read_band_from_running_log(abacus_lines) - # read the atomic forces (nframe, nat, 3) - forces = read_forces_from_running_log(abacus_lines) - # read the stress (nframe, 3, 3), but may be None - stress = read_stress_from_running_log(abacus_lines) - # read all kpoints tables (but only want the first, spinless, with ibz2bz) - k, _, _, _, _ = read_kpoints_from_running_log(abacus_lines) - # unpack the kpoints information - kvecd, wk, _ = k - # FIXME: in principle, the two spin channels share the same set of - # the kpoints, so it is not needed to have two sets of kpoints - # and it is assumed that the sampling of kpoints won't change during - # the simulation, which is, not exactly to be true for the NPT-MD - # runs. - - # only keep the energies of the final iteration for each ion step - energies = find_final_info_with_iter_header( - read_energies_from_running_log(abacus_lines)[1], - read_iter_header_from_running_log(abacus_lines) - ) - - # read the magmom - magmom = read_magmom_from_running_log(abacus_lines) - - # roughly check the integrity of data from their length consistency - assert len(trajectory) == len(energies), \ - f'Inconsistent length: {len(trajectory)} != {len(energies)}' - assert len(trajectory) == len(elecstate), \ - f'Inconsistent length: {len(trajectory)} != {len(elecstate)}' - if len(forces) == 0: - forces = [None] * len(trajectory) - assert len(trajectory) == len(forces), \ - f'Inconsistent length: {len(trajectory)} != {len(forces)}' - if len(stress) == 0: - stress = [None] * len(trajectory) - assert len(trajectory) == len(stress), \ - f'Inconsistent length: {len(trajectory)} != {len(stress)}' - if len(magmom) == 0: - magmom = [np.zeros(shape=(len(trajectory[0]['elem'])))] * len(trajectory) - - # loop over the frame... - images, ind = [], sort_atoms_with - for frame, estat, mag, frs, strs, ener in zip( - trajectory, elecstate, magmom, forces, stress, energies): - # for each frame, a structure can be defined - ind = ind or list(range(len(frame['elem']))) - atoms = Atoms(symbols=np.array(frame['elem'])[ind].tolist(), - positions=frame['coords'][ind], - cell=frame['cell'], - magmoms=mag[ind]) - - # from result, a calculator can be assembled - # however, sometimes the force and stress is not calculated - # in this case, we set them to None - frs = None if is_invalid_arr(frs) else frs[ind] - strs = None if is_invalid_arr(strs) else full_3x3_to_voigt_6_stress(strs) - calc = SinglePointDFTCalculator(atoms=atoms, energy=ener['E_KohnSham'], - free_energy=ener['E_KohnSham'], - forces=frs, stress=strs, - magmoms=mag[ind], efermi=ener['E_Fermi'], - ibzkpts=kvecd, dipole=None) - # import the eigenvalues and occupations kpoint-by-kpoint - calc.kpts = [] - for ispn, (ekb, occ) in enumerate(zip(estat['e'], estat['occ'])): # loop over the spin - calc.kpts += [SinglePointKPoint(weight=wk[ik], - s=ispn, - k=kvecd[ik], - eps_n=ekb[ik,:], - f_n=occ[ik,:]) - for ik in range(len(kvecd))] - # attach the calculator to the atoms - atoms.calc = calc - images.append(atoms) - - return images[index] - -class TestLegacyIO(unittest.TestCase): - - here = Path(__file__).parent - testfiles = here / 'testfiles' - - def test_read_band_from_running_log(self): - # nspin1 - fn = self.testfiles / 'lcao-symm1-nspin1-multik-scf_' - data = read_band_from_running_log(fn) - self.assertIsInstance(data, list) - self.assertGreater(len(data), 0) - self.assertTrue(all('k' in d and 'e' in d and 'occ' in d for d in data)) - nspin, nk, nband = data[0]['e'].shape - self.assertEqual(nspin, 1) - self.assertEqual(nband, 24) - self.assertEqual(nk, 2) - for d in data: # for each frame - self.assertTrue(d['k'].shape == (nspin, nk, 3)) - self.assertTrue(d['e'].shape == (nspin, nk, nband)) - self.assertTrue(d['occ'].shape == (nspin, nk, nband)) - - # nspin2 - fn = self.testfiles / 'lcao-symm0-nspin2-multik-relax_' - data = read_band_from_running_log(fn) - self.assertIsInstance(data, list) - self.assertGreater(len(data), 0) - self.assertTrue(all('k' in d and 'e' in d and 'occ' in d for d in data)) - nspin, nk, nband = data[0]['e'].shape - self.assertEqual(nspin, 2) - self.assertEqual(nband, 24) - self.assertEqual(nk, 2) - for d in data: # for each frame - self.assertTrue(d['k'].shape == (nspin, nk, 3)) - self.assertTrue(d['e'].shape == (nspin, nk, nband)) - self.assertTrue(d['occ'].shape == (nspin, nk, nband)) - - # nspin 2, cell-relax (multi-frames) - fn = self.testfiles / 'lcao-symm0-nspin2-multik-cellrelax_' - data = read_band_from_running_log(fn) - self.assertIsInstance(data, list) - self.assertEqual(len(data), 2) # two frames - self.assertTrue(all('k' in d and 'e' in d and 'occ' in d for d in data)) - nspin, nk, nband = data[0]['e'].shape - self.assertEqual(nspin, 2) - self.assertEqual(nband, 40) - self.assertEqual(nk, 2) - for d in data: # for each frame - self.assertTrue(d['k'].shape == (nspin, nk, 3)) - self.assertTrue(d['e'].shape == (nspin, nk, nband)) - self.assertTrue(d['occ'].shape == (nspin, nk, nband)) - - # nspin 2, MD (multi-frames) - fn = self.testfiles / 'pw-symm0-nspin4-gamma-md_' - data = read_band_from_running_log(fn) - self.assertIsInstance(data, list) - self.assertEqual(len(data), 3) - self.assertTrue(all('k' in d and 'e' in d and 'occ' in d for d in data)) - nspin, nk, nband = data[0]['e'].shape - self.assertEqual(nspin, 1) - self.assertEqual(nband, 35) - self.assertEqual(nk, 1) - for d in data: # for each frame - self.assertTrue(d['k'].shape == (nspin, nk, 3)) - self.assertTrue(d['e'].shape == (nspin, nk, nband)) - self.assertTrue(d['occ'].shape == (nspin, nk, nband)) - - def test_read_band_from_running_log_skips_non_band_rows(self): - data = read_band_from_running_log([ - 'nspin = 1', - 'NBANDS = 2', - 'nkstot = 1', - '1/1 kpoint (Cartesian) = 0.0 0.0 0.0 (1 pws)', - 'BAND ENERGY OCCUPATION', - '1 -1.0 1.0', - '2 0.5 0.0', - ]) - - self.assertEqual(len(data), 1) - self.assertEqual(data[0]['e'].shape, (1, 1, 2)) - self.assertTrue(np.allclose(data[0]['e'][0, 0], [-1.0, 0.5])) - self.assertTrue(np.allclose(data[0]['occ'][0, 0], [1.0, 0.0])) - - def test_read_band_from_running_log_does_not_cross_kpoint_blocks(self): - with self.assertRaises(AssertionError): - read_band_from_running_log([ - 'nspin = 1', - 'NBANDS = 2', - 'nkstot = 2', - '1/2 kpoint (Cartesian) = 0.0 0.0 0.0 (1 pws)', - '1 -1.0 1.0', - '2/2 kpoint (Cartesian) = 0.5 0.0 0.0 (1 pws)', - '1 -0.5 1.0', - '2 0.5 0.0', - ]) - - def test_read_abacus_out_reorders_calculator_magmoms(self): - import tempfile - from unittest.mock import patch - - frame = { - 'elem': ['Na', 'Na', 'Cl'], - 'coords': np.array([[0.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - [2.0, 0.0, 0.0]]), - 'cell': np.eye(3), - } - elecstate = [{ - 'k': np.zeros((1, 1, 3)), - 'e': np.zeros((1, 1, 1)), - 'occ': np.ones((1, 1, 1)), - }] - energies = [{'E_KohnSham': -1.0, 'E_Fermi': 0.0}] - kpoints = ((np.zeros((1, 3)), np.ones(1), None), None, None, None, None) - - with tempfile.NamedTemporaryFile(mode='w') as f: - with patch(__name__ + '.read_esolver_type_from_running_log', return_value='ksdft'), \ - patch(__name__ + '.read_traj_from_running_log', return_value=[frame]), \ - patch(__name__ + '.read_band_from_running_log', return_value=elecstate), \ - patch(__name__ + '.read_forces_from_running_log', return_value=[]), \ - patch(__name__ + '.read_stress_from_running_log', return_value=[]), \ - patch(__name__ + '.read_kpoints_from_running_log', return_value=kpoints), \ - patch(__name__ + '.read_energies_from_running_log', return_value=([], [])), \ - patch(__name__ + '.read_iter_header_from_running_log', return_value=[]), \ - patch(__name__ + '.find_final_info_with_iter_header', return_value=energies), \ - patch(__name__ + '.read_magmom_from_running_log', return_value=[np.array([10.0, 20.0, 30.0])]): - atoms = read_abacus_out(f.name, sort_atoms_with=[0, 2, 1])[0] - - self.assertEqual(atoms.get_chemical_symbols(), ['Na', 'Cl', 'Na']) - self.assertTrue(np.allclose(atoms.calc.results['magmoms'], [10.0, 30.0, 20.0])) - - def test_read_traj_from_running_log(self): - fn = self.testfiles / 'lcao-symm1-nspin1-multik-scf_' - data = read_traj_from_running_log(fn) - self.assertIsInstance(data, list) - self.assertEqual(len(data), 1) # it is scf run, only one frame - self.assertTrue(all('coordinate' in d and 'cell' in d and - 'elem' in d and 'coords' in d for d in data)) - for d in data: - self.assertIn(d['coordinate'], ['Cartesian', 'Direct']) - self.assertEqual(d['cell'].shape, (3, 3)) - self.assertIsInstance(d['elem'], list) - self.assertEqual(len(d['elem']), 2) - - # relax - fn = self.testfiles / 'lcao-symm0-nspin2-multik-relax_' - data = read_traj_from_running_log(fn) - self.assertIsInstance(data, list) - self.assertEqual(len(data), 1) - # relax task will only print the coordinate at the first run - # but band structure will be printed for multiple times... - self.assertTrue(all('coordinate' in d and 'cell' in d and - 'elem' in d and 'coords' in d for d in data)) - for d in data: - self.assertIn(d['coordinate'], ['Cartesian', 'Direct']) - self.assertEqual(d['cell'].shape, (3, 3)) - self.assertIsInstance(d['elem'], list) - self.assertEqual(len(d['elem']), 2) - self.assertEqual(d['coords'].shape, (2, 3)) - - # cell-relax - fn = self.testfiles / 'lcao-symm0-nspin2-multik-cellrelax_' - data = read_traj_from_running_log(fn) - self.assertIsInstance(data, list) - self.assertEqual(len(data), 3) # print each time - self.assertTrue(all('coordinate' in d and 'cell' in d and - 'elem' in d and 'coords' in d for d in data)) - for d in data: - self.assertIn(d['coordinate'], ['Cartesian', 'Direct']) - self.assertEqual(d['cell'].shape, (3, 3)) - self.assertIsInstance(d['elem'], list) - self.assertEqual(len(d['elem']), 4) - self.assertEqual(d['coords'].shape, (4, 3)) - - def test_read_traj_from_md_dump(self): - fn = self.testfiles / 'nspin4-gamma-mddump' - data = read_traj_from_md_dump(fn) - self.assertIsInstance(data, list) - self.assertEqual(len(data), 2) - self.assertTrue(all('coordinate' in d and 'cell' in d and - 'alat_in_angstrom' in d and 'elem' in d and - 'coords' in d for d in data)) - for d in data: - self.assertEqual(d['coordinate'], 'Cartesian') - self.assertEqual(d['cell'].shape, (3, 3)) - self.assertEqual(d['alat_in_angstrom'], 0.529177000000) - self.assertIsInstance(d['elem'], list) - self.assertEqual(len(d['elem']), 2) - self.assertEqual(d['coords'].shape, (2, 3)) - - def test_read_forces_from_running_log(self): - fn = self.testfiles / 'pw-symm0-nspin4-gamma-md_' - forces = read_forces_from_running_log(fn) - self.assertEqual(len(forces), 3) # 3 frames - self.assertTrue(all(f.shape == (2, 3) for f in forces)) # 2 atoms, 3 components - - def test_read_stress_from_running_log(self): - fn = self.testfiles / 'lcao-symm0-nspin2-multik-cellrelax_' - stress = read_stress_from_running_log(fn) - self.assertEqual(len(stress), 2) - self.assertTrue(all(s.shape == (3, 3) for s in stress)) # 3x3 matrix - - reference = np.array([ - [ 28.0257165780, -25.0814687477, -25.0814687478], - [-25.0814687477, -57.2309475134, 52.8147857580], - [-25.0814687478, 52.8147857580, -57.2309475133], - [ 7.3061116295, -27.6053387696, -27.6053387674], - [-27.6053387696, 0.8540980637, 7.6367729837], - [-27.6053387674, 7.6367729837, 0.8540980603], - ]).reshape(-1, 3, 3) - for s, sref in zip(stress, reference): - self.assertTrue(np.allclose(s, -0.1 * GPa * sref)) - - def test_read_energies_from_running_log(self): - fn = self.testfiles / 'lcao-symm0-nspin2-multik-cellrelax_' - energies_ry, energies_ev = read_energies_from_running_log(fn) - self.assertIsInstance(energies_ry, list) - self.assertIsInstance(energies_ev, list) - self.assertEqual(len(energies_ry), 3) - self.assertEqual(len(energies_ev), 3) - self.assertTrue(all(isinstance(e, dict) for e in energies_ry)) # dict of energies - self.assertTrue(all(isinstance(e, dict) for e in energies_ev)) - for e_ev, e_ry in zip(energies_ev, energies_ry): - # take energies of one SCF step, there are still many energy terms - self.assertEqual(len(e_ev), len(e_ry)) - for k, ei_ev in e_ev.items(): - self.assertIn(k, e_ry) - self.assertAlmostEqual(ei_ev, e_ry[k] * Ry / eV, delta=1e-2) - - def test_read_kpoints_from_running_log(self): - fn = self.testfiles / 'lcao-symm0-nspin2-multik-cellrelax_' - kpoints = read_kpoints_from_running_log(fn) - self.assertIsInstance(kpoints, tuple) - self.assertEqual(len(kpoints), 5) - # thus we unpack - kibz, kdspnls, kd1traj, kctraj, kd2traj = kpoints - - # kibz - self.assertIsInstance(kibz, tuple) - self.assertEqual(len(kibz), 3) - # thus we unpack - kibz, wk, ibz2bz = kibz - self.assertIsInstance(kibz, np.ndarray) - self.assertEqual(kibz.shape, (2, 3)) - self.assertIsInstance(wk, np.ndarray) - self.assertEqual(wk.shape, (2,)) - self.assertAlmostEqual(wk.sum(), 1.0, delta=1e-3) - self.assertIsInstance(ibz2bz, list) - self.assertEqual(len(ibz2bz), 2) - - # kdspnls - self.assertIsInstance(kdspnls, tuple) - self.assertEqual(len(kdspnls), 2) - # thus we unpack - kdspnls, wk = kdspnls - self.assertIsInstance(kdspnls, np.ndarray) - self.assertEqual(kdspnls.shape, (2, 3)) - self.assertIsInstance(wk, np.ndarray) - self.assertEqual(wk.shape, (2,)) - self.assertAlmostEqual(wk.sum(), 1.0, delta=1e-3) - - # kd1traj - self.assertIsInstance(kd1traj, list) - self.assertEqual(len(kd1traj), 2) # 2 cell-relax steps - for i, kd1 in enumerate(kd1traj): - # because the i=0 corresponds to the spinless case, - # it has 29 instead of 58 kpoints like the others - nk = 2 - nspin = 1 if i == 0 else 2 - wktot = 1 if i == 0 else 2 - self.assertIsInstance(kd1, tuple) - self.assertEqual(len(kd1), 2) - # thus we unpack - kd1, wk = kd1 - self.assertIsInstance(kd1, np.ndarray) - self.assertEqual(kd1.shape, (nspin, nk, 3)) - self.assertIsInstance(wk, np.ndarray) - self.assertEqual(wk.shape, (nspin, nk)) - self.assertAlmostEqual(wk.sum(), wktot, delta=1e-3) - - # kctraj and kd2traj - for ktraj in [kctraj, kd2traj]: - self.assertIsInstance(ktraj, list) - self.assertEqual(len(ktraj), 2) # 2 cell-relax steps - for i, k in enumerate(ktraj): - self.assertIsInstance(k, tuple) - self.assertEqual(len(k), 2) - # thus we unpack - k, wk = k - self.assertIsInstance(k, np.ndarray) - self.assertEqual(k.shape, (2, 2, 3)) - self.assertIsInstance(wk, np.ndarray) - self.assertEqual(wk.shape, (2, 2)) - self.assertAlmostEqual(wk.sum(), 2.0, delta=1e-3) - - def test_read_magmom_from_running_log(self): - fn = self.testfiles / 'lcao-symm0-nspin2-multik-cellrelax_' - magmoms = read_magmom_from_running_log(fn) - self.assertIsInstance(magmoms, list) - self.assertEqual(len(magmoms), 2) # 2 cell-relax steps - for i, magmom in enumerate(magmoms): - self.assertIsInstance(magmom, np.ndarray) - self.assertEqual(magmom.shape, (4,)) - self.assertAlmostEqual(magmom.sum(), 0.0, delta=1e-3) # AFM - - # non-colinear case - fn = self.testfiles / 'pw-symm0-nspin4-gamma-md_' - magmoms = read_magmom_from_running_log(fn) - self.assertIsInstance(magmoms, list) - self.assertEqual(len(magmoms), 2) - for magmom in magmoms: - self.assertTrue( - np.allclose(magmom, - np.array([[0. , 0. , 3.62032142], - [0. , 0. , 3.62032142]]))) - - def test_read_iter_header_from_running_log(self): - fn = self.testfiles / 'lcao-symm0-nspin2-multik-cellrelax_' - header = read_iter_header_from_running_log(fn) - self.assertEqual(header[0], (1,1)) - self.assertEqual(header[1], (1,2)) - self.assertEqual(header[2], (2,1)) - fn = self.testfiles / 'pw-symm0-nspin4-gamma-md_' - header = read_iter_header_from_running_log(fn) - self.assertEqual(header[0], (1,1)) - self.assertEqual(header[1], (1,2)) - self.assertEqual(header[2], (1,3)) - self.assertEqual(header[3], (3,2)) - self.assertEqual(header[4], (3,3)) - - def test_find_final_info_with_iter_header(self): - info = [1, 2, 3, 4, 5, 6] - header = [[1,1], [1,2], [2,1], [3,1], [3,2], [3,3]] - final_info = find_final_info_with_iter_header(info, header) - self.assertListEqual(final_info, [info[1], info[2], info[5]]) - -if __name__ == '__main__': - unittest.main() diff --git a/interfaces/ASE_interface/abacuslite/io/testfiles/input-script b/interfaces/ASE_interface/abacuslite/io/testfiles/input-script deleted file mode 100644 index 1ecbd7e13a3..00000000000 --- a/interfaces/ASE_interface/abacuslite/io/testfiles/input-script +++ /dev/null @@ -1,34 +0,0 @@ -INPUT_PARAMETERS -#Parameters (1.General) -suffix autotest -calculation scf -device gpu -gamma_only 1 # GPU acceleration currently only support gamma_only set to 1. ### Abacus will generate/overwrite a KPT file when gamma_only is set to 1. -ks_solver cusolver # if not set, the default ks_solver is cusolver, - # you can also choose genelpa or scalapack_gvx. - -#nbands 8 -symmetry 1 - -#Parameters (2.Iteration) -ecutwfc 100 ###Energy cutoff needs to be tested to ensure your calculation is reliable.[1] -scf_thr 1e-6 -scf_nmax 100 -cal_force 1 -cal_stress 1 - -#Parameters (3.Basis) -basis_type lcao - -#Parameters (4.Smearing) -smearing_method gauss -smearing_sigma 0.002 - -#Parameters (5.Mixing) -mixing_type broyden -mixing_beta 0.3 - - -### [1] Energy cutoff determines the quality of numerical quadratures in your calculations. -### So it is strongly recommended to test whether your result (such as converged SCF energies) is -### converged with respect to the energy cutoff. diff --git a/interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm0-nspin2-multik-cellrelax b/interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm0-nspin2-multik-cellrelax deleted file mode 100644 index dca11488a45..00000000000 --- a/interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm0-nspin2-multik-cellrelax +++ /dev/null @@ -1,315 +0,0 @@ - READING UNITCELL INFORMATION - Number of elements = 3 - Lattice constant (Bohr) = 7.884 - Lattice constant (Angstrom) = 4.17203 - - TOTAL ATOM NUMBER = 4 - - DIRECT COORDINATES - atom x y z mag - Ni1 0.000000000000 0.000000000000 0.000000000000 2.0000 - Ni2 0.500000000000 0.500000000000 0.500000000000 -2.0000 - O 0.250000000000 0.250000000000 0.250000000000 0.0000 - O 0.750000000000 0.750000000000 0.750000000000 0.0000 - - ATOMIC VELOCITIES - atom vx vy vz - Ni1 0 0 0 - Ni2 0 0 0 - O 0 0 0 - O 0 0 0 - - Cell volume (Bohr^3) = 245.025 - Cell volume (A^3) = 36.3089 - - Lattice vectors: (Cartesian coordinate: in unit of a_0) - +1 +0.5 +0.5 - +0.5 +1 +0.5 - +0.5 +0.5 +1 - Reciprocal vectors: (Cartesian coordinate: in unit of 2 pi/a_0) - +1.5 -0.5 -0.5 - -0.5 +1.5 -0.5 - -0.5 -0.5 +1.5 - - #ENERGY SOLVER# ksdft_lcao - - SETUP K-POINTS - nspin = 2 - Input type of k points = Monkhorst-Pack(Gamma) - nkstot = 2 - Number of irreducible k-points = 2 - - K-POINTS REDUCTION ACCORDING TO SYMMETRY - IBZ DIRECT_X DIRECT_Y DIRECT_Z WEIGHT ibz2bz - 1 0.00000000 0.00000000 0.00000000 0.5000 0 - 2 0.50000000 0.00000000 0.00000000 0.5000 1 - - nkstot now = 2 - K-POINTS DIRECT COORDINATES - KPOINTS DIRECT_X DIRECT_Y DIRECT_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.5000 - 2 0.50000000 0.00000000 0.00000000 0.5000 - - - Number of k-points in this process = 2 - Minimum distributed k-point number = 2 - nks(nspin=2) = 4 - nkstot(nspin=2) = 4 - - K-POINTS CARTESIAN COORDINATES - KPOINTS CARTESIAN_X CARTESIAN_Y CARTESIAN_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.5000 - 2 0.75000000 -0.25000000 -0.25000000 0.5000 - 3 0.00000000 0.00000000 0.00000000 0.5000 - 4 0.75000000 -0.25000000 -0.25000000 0.5000 - - - K-POINTS DIRECT COORDINATES - KPOINTS DIRECT_X DIRECT_Y DIRECT_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.5000 - 2 0.50000000 0.00000000 0.00000000 0.5000 - 3 0.00000000 0.00000000 0.00000000 0.5000 - 4 0.50000000 0.00000000 0.00000000 0.5000 - - ================================================================ - RELAX STEP: 1 (CELL_CHANGE# 1 IONS_CHANGE# 1) - ================================================================ - - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - --> #ION MOVE# 1 #ELEC ITER# 1 - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - Electron density deviation 0.095868 - ---------------------------------------------------------- - Energy Rydberg eV - ---------------------------------------------------------- - E_KohnSham -680.0824614910 -9252.9965861430 - E_Harris -681.2945326460 -9269.4876602324 - E_Fermi 1.1216473600 15.2607952433 - E_gap(k) 0.0497750855 0.6772247815 - ---------------------------------------------------------- - - - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - --> #ION MOVE# 1 #ELEC ITER# 2 - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - - Total magnetism (Bohr mag/cell) = 0.00000000 - Absolute magnetism (Bohr mag/cell) = 2.80151196 - Electron number for spin up = 24.00000000 - Electron number for spin down = 24.00000000 - Electron density deviation 0.044770 - ---------------------------------------------------------- - Energy Rydberg eV - ---------------------------------------------------------- - E_KohnSham -679.9255100509 -9250.8611522481 - E_KS(sigma->0) -679.9255100509 -9250.8611522481 - E_Harris -676.7523312621 -9207.6878399479 - E_band -48.6414989983 -661.8015456379 - E_one_elec -311.2597632447 -4234.9063382588 - E_Hartree 181.7820867842 2473.2721745958 - E_xc -74.7512281470 -1017.0426352976 - E_Ewald -478.4202561664 -6509.2415224829 - E_entropy(-TS) -0.0000000000 -0.0000000000 - E_descf 0.0000000000 0.0000000000 - E_localpp -401.8063652052 -5466.8560594599 - E_plusU 2.7236507231 37.0571691954 - E_exx 0.0000000000 0.0000000000 - E_Fermi 1.1111466448 15.1179256829 - E_gap(k) 0.1931559722 2.6280218248 - ---------------------------------------------------------- - - #SCF IS CONVERGED# - #TOTAL ENERGY# -9250.86115224814 eV - Write eigenvalues and occupations to file: OUT.NiO/eig_occ.txt -------------------------------------------------- - Total Magnetism (uB) -------------------------------------------------- - Ni11 1.2715948597 - Ni21 -1.2715948597 - O1 0.0000000000 - O2 0.0000000000 -------------------------------------------------- - - - #TOTAL-FORCE (eV/Angstrom)# - ------------------------------------------------------------------------- - Atoms Force_x Force_y Force_z - ------------------------------------------------------------------------- - Ni11 0.0000000000 0.0000000000 0.0000000000 - Ni21 0.0000000000 0.0000000000 0.0000000000 - O1 0.0000000000 0.0000000000 0.0000000000 - O2 0.0000000000 0.0000000000 0.0000000000 - ------------------------------------------------------------------------- - - #TOTAL-STRESS (kbar)# - ---------------------------------------------------------------- - Stress_x Stress_y Stress_z - ---------------------------------------------------------------- - 28.0257165781 -25.0814687477 -25.0814687477 - -25.0814687477 -57.2309475134 52.8147857580 - -25.0814687477 52.8147857580 -57.2309475134 - ---------------------------------------------------------------- - #TOTAL-PRESSURE# (EXCLUDE KINETIC PART OF IONS): -28.812059 kbar - - Largest force is 0.000000 eV/Angstrom while threshold is 0.025711 eV/Angstrom - Largest stress is 57.230948 kbar while threshold is 0.500000 kbar - - Relaxation is not converged yet! - DIRECT COORDINATES - atom x y z mag - Ni1 0.000000000000 1.000000000000 1.000000000000 2.0000 - Ni2 0.500000000000 0.500000000000 0.500000000000 -2.0000 - O 0.250000000000 0.250000000000 0.250000000000 0.0000 - O 0.750000000000 0.750000000000 0.750000000000 0.0000 - - ATOMIC VELOCITIES - atom vx vy vz - Ni1 0.000000000000 0.000000000000 0.000000000000 - Ni2 0.000000000000 0.000000000000 0.000000000000 - O 0.000000000000 0.000000000000 0.000000000000 - O 0.000000000000 0.000000000000 0.000000000000 - - Cell volume (Bohr^3) = 238.947741 - Cell volume (A^3) = 35.408360 - - Lattice vectors: (Cartesian coordinate: in unit of a_0) - +1.000834 +0.492269 +0.492269 - +0.493312 +0.987716 +0.503303 - +0.493312 +0.503303 +0.987716 - Reciprocal vectors: (Cartesian coordinate: in unit of 2 pi/a_0) - +1.481276 -0.490089 -0.490089 - -0.489053 +1.529324 -0.535029 - -0.489053 -0.535029 +1.529324 - DONE : SETUP UNITCELL Time : 34.044430 (SEC) - - ================================================================ - RELAX STEP: 2 (CELL_CHANGE# 2 IONS_CHANGE# 1) - ================================================================ - DONE : LOCAL POTENTIAL Time : 34.155598 (SEC) - - - SETUP K-POINTS - nspin = 2 -K-POINTS DIRECT COORDINATES - KPOINTS DIRECT_X DIRECT_Y DIRECT_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.5000 - 2 0.50000000 0.00000000 0.00000000 0.5000 - 3 0.00000000 0.00000000 0.00000000 0.5000 - 4 0.50000000 0.00000000 0.00000000 0.5000 - - - K-POINTS CARTESIAN COORDINATES - KPOINTS CARTESIAN_X CARTESIAN_Y CARTESIAN_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.5000 - 2 0.74063794 -0.24504430 -0.24504430 0.5000 - 3 0.00000000 0.00000000 0.00000000 0.5000 - 4 0.74063794 -0.24504430 -0.24504430 0.5000 - - - K-POINTS DIRECT COORDINATES - KPOINTS DIRECT_X DIRECT_Y DIRECT_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.5000 - 2 0.50000000 0.00000000 0.00000000 0.5000 - 3 0.00000000 0.00000000 0.00000000 0.5000 - 4 0.50000000 0.00000000 0.00000000 0.5000 - - DONE : INIT K-POINTS Time : 34.155733 (SEC) - - NEW-OLD atomic charge density approx. for the potential ! - - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - --> #ION MOVE# 2 #ELEC ITER# 1 - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - Total magnetism (Bohr mag/cell) = -0.00000000 - Absolute magnetism (Bohr mag/cell) = 3.21518097 - Electron number for spin up = 24.00000000 - Electron number for spin down = 24.00000000 - Electron density deviation 0.025755 - ---------------------------------------------------------- - Energy Rydberg eV - ---------------------------------------------------------- - E_KohnSham -679.8533719985 -9249.8796636937 - E_KS(sigma->0) -679.8533719985 -9249.8796636937 - E_Harris -684.2300352486 -9309.4272221222 - E_band -43.6339455623 -593.6702858688 - E_one_elec -300.8081834810 -4092.7053003714 - E_Hartree 175.1973991234 2383.6829028589 - E_xc -74.7285731514 -1016.7343982682 - E_Ewald -482.3768468830 -6563.0737008819 - E_entropy(-TS) -0.0000000000 -0.0000000000 - E_descf 0.0000000000 0.0000000000 - E_localpp -392.5829798906 -5341.3654643309 - E_plusU 2.8628323934 38.9508329690 - E_exx 0.0000000000 0.0000000000 - E_Fermi 1.1742844955 15.9769602119 - E_gap(k) 0.2577258748 3.5065404191 - ---------------------------------------------------------- - - #SCF IS CONVERGED# - #TOTAL ENERGY# -9249.87966369367 eV - Write eigenvalues and occupations to file: OUT.NiO/eig_occ.txt -------------------------------------------------- - Total Magnetism (uB) -------------------------------------------------- - Ni11 1.5398722566 - Ni21 -1.5398722566 - O1 0.0000000000 - O2 -0.0000000000 -------------------------------------------------- - - - #TOTAL-FORCE (eV/Angstrom)# - ------------------------------------------------------------------------- - Atoms Force_x Force_y Force_z - ------------------------------------------------------------------------- - Ni11 0.0000000000 0.0000000000 0.0000000000 - Ni21 0.0000000000 0.0000000000 0.0000000000 - O1 0.0000000000 0.0000000000 0.0000000000 - O2 0.0000000000 0.0000000000 0.0000000000 - ------------------------------------------------------------------------- - - #TOTAL-STRESS (kbar)# - ---------------------------------------------------------------- - Stress_x Stress_y Stress_z - ---------------------------------------------------------------- - 10.5133147463 -28.8166870580 -28.8166870559 - -28.8166870580 0.1640200010 7.6016822662 - -28.8166870559 7.6016822662 0.1640199977 - ---------------------------------------------------------------- - #TOTAL-PRESSURE# (EXCLUDE KINETIC PART OF IONS): 3.613785 kbar - - Largest force is 0.000000 eV/Angstrom while threshold is 0.025711 eV/Angstrom - Largest stress is 28.816687 kbar while threshold is 0.500000 kbar - - Relaxation is not converged yet! - DIRECT COORDINATES - atom x y z mag - Ni1 0.000000000000 1.000000000000 1.000000000000 2.0000 - Ni2 0.500000000000 0.500000000000 0.500000000000 -2.0000 - O 0.250000000000 0.250000000000 0.250000000000 0.0000 - O 0.750000000000 0.750000000000 0.750000000000 0.0000 - - ATOMIC VELOCITIES - atom vx vy vz - Ni1 0.000000000000 0.000000000000 0.000000000000 - Ni2 0.000000000000 0.000000000000 0.000000000000 - O 0.000000000000 0.000000000000 0.000000000000 - O 0.000000000000 0.000000000000 0.000000000000 - - Cell volume (Bohr^3) = 236.878456 - Cell volume (A^3) = 35.101724 - - Lattice vectors: (Cartesian coordinate: in unit of a_0) - +1.001113 +0.489681 +0.489681 - +0.491072 +0.983602 +0.504409 - +0.491072 +0.504409 +0.983602 - Reciprocal vectors: (Cartesian coordinate: in unit of 2 pi/a_0) - +1.475132 -0.486822 -0.486822 - -0.485442 +1.539645 -0.547196 - -0.485442 -0.547196 +1.539645 - DONE : SETUP UNITCELL Time : 63.323510 (SEC) - - - -------------------------------------------- - !FINAL_ETOT_IS -9249.8796636936658615 eV - -------------------------------------------- diff --git a/interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm0-nspin2-multik-cellrelax_ b/interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm0-nspin2-multik-cellrelax_ deleted file mode 100644 index 93870d85621..00000000000 --- a/interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm0-nspin2-multik-cellrelax_ +++ /dev/null @@ -1,648 +0,0 @@ -NBANDS = 40 - - READING UNITCELL INFORMATION - ntype = 3 - lattice constant (Bohr) = 7.884 - lattice constant (Angstrom) = 4.17203 - - TOTAL ATOM NUMBER = 4 -DIRECT COORDINATES - atom x y z mag vx vy vz -taud_Ni11 0.0000000000 0.0000000000 0.0000000000 2.0000 0.0000000000 0.0000000000 0.0000000000 -taud_Ni21 0.5000000000 0.5000000000 0.5000000000 -2.0000 0.0000000000 0.0000000000 0.0000000000 -taud_O1 0.2500000000 0.2500000000 0.2500000000 0.0000 0.0000000000 0.0000000000 0.0000000000 -taud_O2 0.7500000000 0.7500000000 0.7500000000 0.0000 0.0000000000 0.0000000000 0.0000000000 - - - - Volume (Bohr^3) = 245.025 - Volume (A^3) = 36.3089 - - Lattice vectors: (Cartesian coordinate: in unit of a_0) - +1 +0.5 +0.5 - +0.5 +1 +0.5 - +0.5 +0.5 +1 - Reciprocal vectors: (Cartesian coordinate: in unit of 2 pi/a_0) - +1.5 -0.5 -0.5 - -0.5 +1.5 -0.5 - -0.5 -0.5 +1.5 - The esolver type has been set to : ksdft_lcao - - SETUP K-POINTS - nspin = 2 - Input type of k points = Monkhorst-Pack(Gamma) - nkstot = 2 - nkstot_ibz = 2 -K-POINTS REDUCTION ACCORDING TO SYMMETRY - IBZ DIRECT_X DIRECT_Y DIRECT_Z WEIGHT ibz2bz - 1 0.00000000 0.00000000 0.00000000 0.5000 0 - 2 0.50000000 0.00000000 0.00000000 0.5000 1 - - nkstot now = 2 -K-POINTS DIRECT COORDINATES - KPOINTS DIRECT_X DIRECT_Y DIRECT_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.5000 - 2 0.50000000 0.00000000 0.00000000 0.5000 - - - k-point number in this process = 2 - minimum distributed K point number = 2 - nks(nspin=2) = 4 - nkstot(nspin=2) = 4 - -K-POINTS CARTESIAN COORDINATES - KPOINTS CARTESIAN_X CARTESIAN_Y CARTESIAN_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.5000 - 2 0.75000000 -0.25000000 -0.25000000 0.5000 - 3 0.00000000 0.00000000 0.00000000 0.5000 - 4 0.75000000 -0.25000000 -0.25000000 0.5000 - - -K-POINTS DIRECT COORDINATES - KPOINTS DIRECT_X DIRECT_Y DIRECT_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.5000 - 2 0.50000000 0.00000000 0.00000000 0.5000 - 3 0.00000000 0.00000000 0.00000000 0.5000 - 4 0.50000000 0.00000000 0.00000000 0.5000 - - DONE : INIT K-POINTS Time : 0.0449331 (SEC) - - LCAO ALGORITHM --------------- ION= 1 ELEC= 1-------------------------------- - total magnetism (Bohr mag/cell) = 0.00000000 - absolute magnetism (Bohr mag/cell) = 2.61029538 - nelec for spin up = 24.00000000 - nelec for spin down = 24.00000000 - - Density error is 0.095867875873 ----------------------------------------------------------- - Energy Rydberg eV ----------------------------------------------------------- - E_KohnSham -680.0824614910 -9252.9965861430 - E_Harris -681.2945326460 -9269.4876602324 - E_Fermi 1.1216473600 15.2607952433 ----------------------------------------------------------- - - - LCAO ALGORITHM --------------- ION= 1 ELEC= 2-------------------------------- - total magnetism (Bohr mag/cell) = 0.00000000 - absolute magnetism (Bohr mag/cell) = 2.80151196 - nelec for spin up = 24.00000000 - nelec for spin down = 24.00000000 - - Density error is 0.044770387430 ----------------------------------------------------------- - Energy Rydberg eV ----------------------------------------------------------- - E_KohnSham -679.9255100509 -9250.8611522481 - E_KS(sigma->0) -679.9255100509 -9250.8611522481 - E_Harris -676.7523312621 -9207.6878399479 - E_band -48.6414989983 -661.8015456379 - E_one_elec -311.2597632447 -4234.9063382588 - E_Hartree 181.7820867842 2473.2721745958 - E_xc -74.7512281470 -1017.0426352976 - E_Ewald -478.4202561664 -6509.2415224829 - E_entropy(-TS) -0.0000000000 -0.0000000000 - E_descf 0.0000000000 0.0000000000 - E_exx 0.0000000000 0.0000000000 - E_Fermi 1.1111466448 15.1179256829 ----------------------------------------------------------- - - - charge density convergence is achieved - final etot is -9250.86115224814 eV - EFERMI = 15.11792568287 eV - - STATE ENERGY(eV) AND OCCUPATIONS NSPIN == 2 -SPIN UP : - 1/2 kpoint (Cartesian) = 0.00000 0.00000 0.00000 (522 pws) - 1 -89.404874 0.500000 - 2 -89.125942 0.500000 - 3 -50.116722 0.500000 - 4 -50.012641 0.500000 - 5 -49.770942 0.500000 - 6 -49.167045 0.500000 - 7 -49.133084 0.500000 - 8 -49.072023 0.500000 - 9 -4.305764 0.500000 - 10 -2.828539 0.500000 - 11 7.064136 0.500000 - 12 8.571683 0.500000 - 13 8.610007 0.500000 - 14 10.099916 0.500000 - 15 10.229316 0.500000 - 16 10.273838 0.500000 - 17 10.620054 0.500000 - 18 10.883396 0.500000 - 19 12.026525 0.500000 - 20 12.870909 0.500000 - 21 12.951267 0.500000 - 22 13.103917 0.500000 - 23 13.160275 0.500000 - 24 13.160406 0.500000 - 25 16.848597 0.000000 - 26 16.983083 0.000000 - 27 17.314452 0.000000 - 28 21.985231 0.000000 - 29 25.988979 0.000000 - 30 31.586032 0.000000 - 31 31.633203 0.000000 - 32 40.413880 0.000000 - 33 40.422562 0.000000 - 34 40.463585 0.000000 - 35 40.901535 0.000000 - 36 41.381761 0.000000 - 37 46.434114 0.000000 - 38 46.444013 0.000000 - 39 46.469222 0.000000 - 40 49.052274 0.000000 - - 2/2 kpoint (Cartesian) = 0.75000 -0.25000 -0.25000 (525 pws) - 1 -89.403068 0.500000 - 2 -89.124281 0.500000 - 3 -50.122332 0.500000 - 4 -50.014725 0.500000 - 5 -49.789066 0.500000 - 6 -49.185597 0.500000 - 7 -49.139283 0.500000 - 8 -49.076151 0.500000 - 9 -2.873734 0.500000 - 10 -2.767857 0.500000 - 11 7.978080 0.500000 - 12 8.219411 0.500000 - 13 8.548029 0.500000 - 14 8.669009 0.500000 - 15 9.836748 0.500000 - 16 10.504337 0.500000 - 17 10.667347 0.500000 - 18 10.804437 0.500000 - 19 11.314523 0.500000 - 20 11.738650 0.500000 - 21 11.747180 0.500000 - 22 12.040081 0.500000 - 23 13.198973 0.500000 - 24 13.739675 0.500000 - 25 16.367697 0.000000 - 26 16.550384 0.000000 - 27 23.386516 0.000000 - 28 23.511720 0.000000 - 29 26.159604 0.000000 - 30 26.291125 0.000000 - 31 28.634180 0.000000 - 32 28.670785 0.000000 - 33 32.915320 0.000000 - 34 32.996909 0.000000 - 35 40.504483 0.000000 - 36 40.630715 0.000000 - 37 50.312984 0.000000 - 38 50.426416 0.000000 - 39 51.859669 0.000000 - 40 51.907506 0.000000 - -SPIN DOWN : - 1/2 kpoint (Cartesian) = 0.00000 0.00000 0.00000 (522 pws) - 1 -89.404874 0.500000 - 2 -89.125942 0.500000 - 3 -50.116722 0.500000 - 4 -50.012641 0.500000 - 5 -49.770942 0.500000 - 6 -49.167045 0.500000 - 7 -49.133084 0.500000 - 8 -49.072023 0.500000 - 9 -4.305764 0.500000 - 10 -2.828539 0.500000 - 11 7.064136 0.500000 - 12 8.571683 0.500000 - 13 8.610007 0.500000 - 14 10.099916 0.500000 - 15 10.229316 0.500000 - 16 10.273838 0.500000 - 17 10.620054 0.500000 - 18 10.883396 0.500000 - 19 12.026525 0.500000 - 20 12.870909 0.500000 - 21 12.951267 0.500000 - 22 13.103917 0.500000 - 23 13.160275 0.500000 - 24 13.160406 0.500000 - 25 16.848597 0.000000 - 26 16.983083 0.000000 - 27 17.314452 0.000000 - 28 21.985231 0.000000 - 29 25.988979 0.000000 - 30 31.586032 0.000000 - 31 31.633203 0.000000 - 32 40.413880 0.000000 - 33 40.422562 0.000000 - 34 40.463585 0.000000 - 35 40.901535 0.000000 - 36 41.381761 0.000000 - 37 46.434114 0.000000 - 38 46.444013 0.000000 - 39 46.469222 0.000000 - 40 49.052274 0.000000 - - 2/2 kpoint (Cartesian) = 0.75000 -0.25000 -0.25000 (525 pws) - 1 -89.403068 0.500000 - 2 -89.124281 0.500000 - 3 -50.122332 0.500000 - 4 -50.014725 0.500000 - 5 -49.789066 0.500000 - 6 -49.185597 0.500000 - 7 -49.139283 0.500000 - 8 -49.076151 0.500000 - 9 -2.873734 0.500000 - 10 -2.767857 0.500000 - 11 7.978080 0.500000 - 12 8.219411 0.500000 - 13 8.548029 0.500000 - 14 8.669009 0.500000 - 15 9.836748 0.500000 - 16 10.504337 0.500000 - 17 10.667347 0.500000 - 18 10.804437 0.500000 - 19 11.314523 0.500000 - 20 11.738650 0.500000 - 21 11.747180 0.500000 - 22 12.040081 0.500000 - 23 13.198973 0.500000 - 24 13.739675 0.500000 - 25 16.367697 0.000000 - 26 16.550384 0.000000 - 27 23.386516 0.000000 - 28 23.511720 0.000000 - 29 26.159604 0.000000 - 30 26.291125 0.000000 - 31 28.634180 0.000000 - 32 28.670785 0.000000 - 33 32.915320 0.000000 - 34 32.996909 0.000000 - 35 40.504483 0.000000 - 36 40.630715 0.000000 - 37 50.312984 0.000000 - 38 50.426416 0.000000 - 39 51.859669 0.000000 - 40 51.907506 0.000000 - -------------------------------------------------- - Total Magnetism (uB) -------------------------------------------------- - Ni11 1.2715948597 - Ni21 -1.2715948597 - O1 0.0000000000 - O2 0.0000000000 -------------------------------------------------- - - - Warning_Memory_Consuming allocated: Stress::dSR 5.079346 MB - correction force for each atom along direction 1 is 0.000000 - correction force for each atom along direction 2 is 0.000000 - correction force for each atom along direction 3 is 0.000000 ------------------------------------------------------------------------------------------- - TOTAL-FORCE (eV/Angstrom) ------------------------------------------------------------------------------------------- - Ni11 0.0000000000 0.0000000000 0.0000000000 - Ni21 0.0000000000 0.0000000000 0.0000000000 - O1 0.0000000000 0.0000000000 0.0000000000 - O2 0.0000000000 0.0000000000 0.0000000000 ------------------------------------------------------------------------------------------- - ----------------------------------------------------------------- - TOTAL-STRESS (KBAR) ----------------------------------------------------------------- - 28.0257165780 -25.0814687477 -25.0814687478 - -25.0814687477 -57.2309475134 52.8147857580 - -25.0814687478 52.8147857580 -57.2309475133 ----------------------------------------------------------------- - TOTAL-PRESSURE: -28.812059 KBAR - - - Largest gradient in force is 0.000000 eV/A. - Threshold is 0.025711 eV/A. - - Largest gradient in stress is 57.230948 kbar. - Threshold is 0.500000 kbar. - - Relaxation is not converged yet! -DIRECT COORDINATES - atom x y z mag vx vy vz -taud_Ni11 0.0000000000 0.0000000000 0.0000000000 2.0000 0.0000000000 0.0000000000 0.0000000000 -taud_Ni21 0.5000000000 0.5000000000 0.5000000000 -2.0000 0.0000000000 0.0000000000 0.0000000000 -taud_O1 0.2500000000 0.2500000000 0.2500000000 0.0000 0.0000000000 0.0000000000 0.0000000000 -taud_O2 0.7500000000 0.7500000000 0.7500000000 0.0000 0.0000000000 0.0000000000 0.0000000000 - - - - Volume (Bohr^3) = 238.947741 - Volume (A^3) = 35.408360 - - Lattice vectors: (Cartesian coordinate: in unit of a_0) - +1.000834 +0.492269 +0.492269 - +0.493312 +0.987716 +0.503303 - +0.493312 +0.503303 +0.987716 - Reciprocal vectors: (Cartesian coordinate: in unit of 2 pi/a_0) - +1.481276 -0.490089 -0.490089 - -0.489053 +1.529324 -0.535029 - -0.489053 -0.535029 +1.529324 - DONE : SETUP UNITCELL Time : 35.144061 (SEC) - - - ------------------------------------------- - STEP OF RELAXATION : 2 - ------------------------------------------- - DONE : LOCAL POTENTIAL Time : 35.274813 (SEC) - - - SETUP K-POINTS - nspin = 2 -K-POINTS DIRECT COORDINATES - KPOINTS DIRECT_X DIRECT_Y DIRECT_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.5000 - 2 0.50000000 0.00000000 0.00000000 0.5000 - 3 0.00000000 0.00000000 0.00000000 0.5000 - 4 0.50000000 0.00000000 0.00000000 0.5000 - - -K-POINTS CARTESIAN COORDINATES - KPOINTS CARTESIAN_X CARTESIAN_Y CARTESIAN_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.5000 - 2 0.74063794 -0.24504430 -0.24504430 0.5000 - 3 0.00000000 0.00000000 0.00000000 0.5000 - 4 0.74063794 -0.24504430 -0.24504430 0.5000 - - -K-POINTS DIRECT COORDINATES - KPOINTS DIRECT_X DIRECT_Y DIRECT_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.5000 - 2 0.50000000 0.00000000 0.00000000 0.5000 - 3 0.00000000 0.00000000 0.00000000 0.5000 - 4 0.50000000 0.00000000 0.00000000 0.5000 - - DONE : INIT K-POINTS Time : 35.274964 (SEC) - - NEW-OLD atomic charge density approx. for the potential ! - - LCAO ALGORITHM --------------- ION= 2 ELEC= 1-------------------------------- - total magnetism (Bohr mag/cell) = 0.00000000 - absolute magnetism (Bohr mag/cell) = 3.21425392 - nelec for spin up = 24.00000000 - nelec for spin down = 24.00000000 - - Density error is 0.026004815939 ----------------------------------------------------------- - Energy Rydberg eV ----------------------------------------------------------- - E_KohnSham -679.8473275556 -9249.7974248288 - E_KS(sigma->0) -679.8473275556 -9249.7974248288 - E_Harris -684.3263638890 -9310.7378405121 - E_band -43.4930687779 -591.7535588848 - E_one_elec -300.5649336133 -4089.3957161326 - E_Hartree 175.0901338700 2382.2234842145 - E_xc -74.7193316524 -1016.6086612241 - E_Ewald -482.3768468830 -6563.0737008819 - E_entropy(-TS) -0.0000000000 -0.0000000000 - E_descf 0.0000000000 0.0000000000 - E_exx 0.0000000000 0.0000000000 - E_Fermi 1.1742850813 15.9769681825 ----------------------------------------------------------- - - - charge density convergence is achieved - final etot is -9249.79742482878 eV - EFERMI = 15.97696818250 eV - - STATE ENERGY(eV) AND OCCUPATIONS NSPIN == 2 -SPIN UP : - 1/2 kpoint (Cartesian) = 0.00000 0.00000 0.00000 (522 pws) - 1 -87.313319 0.500000 - 2 -86.299919 0.500000 - 3 -48.144915 0.500000 - 4 -47.852774 0.500000 - 5 -47.594179 0.500000 - 6 -46.643110 0.500000 - 7 -46.469430 0.500000 - 8 -46.356732 0.500000 - 9 -4.441729 0.500000 - 10 -3.006060 0.500000 - 11 7.554887 0.500000 - 12 9.603175 0.500000 - 13 9.791693 0.500000 - 14 11.910486 0.500000 - 15 12.024017 0.500000 - 16 12.122462 0.500000 - 17 12.800279 0.500000 - 18 12.812330 0.500000 - 19 12.962308 0.500000 - 20 13.085032 0.500000 - 21 13.093740 0.500000 - 22 13.606898 0.500000 - 23 14.163995 0.500000 - 24 14.210286 0.500000 - 25 18.126647 0.000000 - 26 18.317661 0.000000 - 27 18.411251 0.000000 - 28 22.810951 0.000000 - 29 26.717376 0.000000 - 30 32.381051 0.000000 - 31 33.327582 0.000000 - 32 40.974339 0.000000 - 33 42.076111 0.000000 - 34 42.512684 0.000000 - 35 42.741748 0.000000 - 36 42.813320 0.000000 - 37 46.702895 0.000000 - 38 48.044296 0.000000 - 39 48.397388 0.000000 - 40 49.449904 0.000000 - - 2/2 kpoint (Cartesian) = 0.74064 -0.24504 -0.24504 (525 pws) - 1 -87.311541 0.500000 - 2 -86.298007 0.500000 - 3 -48.150190 0.500000 - 4 -47.853056 0.500000 - 5 -47.615448 0.500000 - 6 -46.649045 0.500000 - 7 -46.467146 0.500000 - 8 -46.383080 0.500000 - 9 -3.088880 0.500000 - 10 -3.004681 0.500000 - 11 8.440016 0.500000 - 12 8.606555 0.500000 - 13 9.402022 0.500000 - 14 9.896985 0.500000 - 15 10.486907 0.500000 - 16 11.502839 0.500000 - 17 12.152707 0.500000 - 18 12.604010 0.500000 - 19 12.956976 0.500000 - 20 13.237777 0.500000 - 21 13.559188 0.500000 - 22 14.001685 0.500000 - 23 14.085813 0.500000 - 24 14.514613 0.500000 - 25 18.021728 0.000000 - 26 18.254714 0.000000 - 27 24.223340 0.000000 - 28 24.392854 0.000000 - 29 27.003980 0.000000 - 30 27.096256 0.000000 - 31 29.660858 0.000000 - 32 29.728710 0.000000 - 33 34.805284 0.000000 - 34 35.022252 0.000000 - 35 42.072506 0.000000 - 36 42.132637 0.000000 - 37 51.290113 0.000000 - 38 51.325899 0.000000 - 39 52.955352 0.000000 - 40 53.076750 0.000000 - -SPIN DOWN : - 1/2 kpoint (Cartesian) = 0.00000 0.00000 0.00000 (522 pws) - 1 -87.313319 0.500000 - 2 -86.299919 0.500000 - 3 -48.144915 0.500000 - 4 -47.852774 0.500000 - 5 -47.594179 0.500000 - 6 -46.643110 0.500000 - 7 -46.469430 0.500000 - 8 -46.356732 0.500000 - 9 -4.441729 0.500000 - 10 -3.006060 0.500000 - 11 7.554887 0.500000 - 12 9.603175 0.500000 - 13 9.791693 0.500000 - 14 11.910486 0.500000 - 15 12.024017 0.500000 - 16 12.122462 0.500000 - 17 12.800279 0.500000 - 18 12.812330 0.500000 - 19 12.962308 0.500000 - 20 13.085032 0.500000 - 21 13.093740 0.500000 - 22 13.606898 0.500000 - 23 14.163995 0.500000 - 24 14.210286 0.500000 - 25 18.126647 0.000000 - 26 18.317661 0.000000 - 27 18.411251 0.000000 - 28 22.810951 0.000000 - 29 26.717376 0.000000 - 30 32.381051 0.000000 - 31 33.327582 0.000000 - 32 40.974339 0.000000 - 33 42.076111 0.000000 - 34 42.512684 0.000000 - 35 42.741748 0.000000 - 36 42.813320 0.000000 - 37 46.702895 0.000000 - 38 48.044296 0.000000 - 39 48.397388 0.000000 - 40 49.449904 0.000000 - - 2/2 kpoint (Cartesian) = 0.74064 -0.24504 -0.24504 (525 pws) - 1 -87.311541 0.500000 - 2 -86.298007 0.500000 - 3 -48.150190 0.500000 - 4 -47.853056 0.500000 - 5 -47.615448 0.500000 - 6 -46.649045 0.500000 - 7 -46.467146 0.500000 - 8 -46.383080 0.500000 - 9 -3.088880 0.500000 - 10 -3.004681 0.500000 - 11 8.440016 0.500000 - 12 8.606555 0.500000 - 13 9.402022 0.500000 - 14 9.896985 0.500000 - 15 10.486907 0.500000 - 16 11.502839 0.500000 - 17 12.152707 0.500000 - 18 12.604010 0.500000 - 19 12.956976 0.500000 - 20 13.237777 0.500000 - 21 13.559188 0.500000 - 22 14.001685 0.500000 - 23 14.085813 0.500000 - 24 14.514613 0.500000 - 25 18.021728 0.000000 - 26 18.254714 0.000000 - 27 24.223340 0.000000 - 28 24.392854 0.000000 - 29 27.003980 0.000000 - 30 27.096256 0.000000 - 31 29.660858 0.000000 - 32 29.728710 0.000000 - 33 34.805284 0.000000 - 34 35.022252 0.000000 - 35 42.072506 0.000000 - 36 42.132637 0.000000 - 37 51.290113 0.000000 - 38 51.325899 0.000000 - 39 52.955352 0.000000 - 40 53.076750 0.000000 - -------------------------------------------------- - Total Magnetism (uB) -------------------------------------------------- - Ni11 1.5398231110 - Ni21 -1.5398231110 - O1 -0.0000000000 - O2 -0.0000000000 -------------------------------------------------- - - correction force for each atom along direction 1 is 0.000000 - correction force for each atom along direction 2 is -0.000000 - correction force for each atom along direction 3 is -0.000000 ------------------------------------------------------------------------------------------- - TOTAL-FORCE (eV/Angstrom) ------------------------------------------------------------------------------------------- - Ni11 0.0000000000 0.0000000000 0.0000000000 - Ni21 0.0000000000 0.0000000000 0.0000000000 - O1 0.0000000000 0.0000000000 0.0000000000 - O2 0.0000000000 0.0000000000 0.0000000000 ------------------------------------------------------------------------------------------- - ----------------------------------------------------------------- - TOTAL-STRESS (KBAR) ----------------------------------------------------------------- - 7.3061116295 -27.6053387696 -27.6053387674 - -27.6053387696 0.8540980637 7.6367729837 - -27.6053387674 7.6367729837 0.8540980603 ----------------------------------------------------------------- - TOTAL-PRESSURE: 3.004769 KBAR - - - Largest gradient in force is 0.000000 eV/A. - Threshold is 0.025711 eV/A. - - Largest gradient in stress is 27.605339 kbar. - Threshold is 0.500000 kbar. - - Relaxation is not converged yet! -DIRECT COORDINATES - atom x y z mag vx vy vz -taud_Ni11 0.0000000000 0.0000000000 0.0000000000 2.0000 0.0000000000 0.0000000000 0.0000000000 -taud_Ni21 0.5000000000 0.5000000000 0.5000000000 -2.0000 0.0000000000 0.0000000000 0.0000000000 -taud_O1 0.2500000000 0.2500000000 0.2500000000 0.0000 0.0000000000 0.0000000000 0.0000000000 -taud_O2 0.7500000000 0.7500000000 0.7500000000 0.0000 0.0000000000 0.0000000000 0.0000000000 - - - - Volume (Bohr^3) = 237.073950 - Volume (A^3) = 35.130693 - - Lattice vectors: (Cartesian coordinate: in unit of a_0) - +1.001087 +0.489924 +0.489924 - +0.491283 +0.983989 +0.504305 - +0.491283 +0.504305 +0.983989 - Reciprocal vectors: (Cartesian coordinate: in unit of 2 pi/a_0) - +1.475708 -0.487128 -0.487128 - -0.485781 +1.538662 -0.546041 - -0.485781 -0.546041 +1.538662 - DONE : SETUP UNITCELL Time : 58.301245 (SEC) - - - - -------------------------------------------- - !FINAL_ETOT_IS -9249.7974248287773662 eV - -------------------------------------------- diff --git a/interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm0-nspin2-multik-relax b/interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm0-nspin2-multik-relax deleted file mode 100644 index 32c1b22a053..00000000000 --- a/interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm0-nspin2-multik-relax +++ /dev/null @@ -1,254 +0,0 @@ - READING UNITCELL INFORMATION - Number of elements = 2 - Lattice constant (Bohr) = 6.1416 - Lattice constant (Angstrom) = 3.24999 - - TOTAL ATOM NUMBER = 2 - - DIRECT COORDINATES - atom x y z mag - Zn 0.000000000000 0.000000000000 0.000000000000 1.0000 - O 0.333329999999 0.666670000001 0.500000000000 1.0000 - - ATOMIC VELOCITIES - atom vx vy vz - Zn 0 0 0 - O 0 0 0 - - Cell volume (Bohr^3) = 320.983 - Cell volume (A^3) = 47.5648 - - Lattice vectors: (Cartesian coordinate: in unit of a_0) - +1 +0 +0 - -0.5 +0.866 +0 - +0 +0 +1.6 - Reciprocal vectors: (Cartesian coordinate: in unit of 2 pi/a_0) - +1 +0.577367 -0 - -0 +1.15473 -0 - +0 -0 +0.625 - - #ENERGY SOLVER# ksdft_lcao - - SETUP K-POINTS - nspin = 2 - Input type of k points = Monkhorst-Pack(Gamma) - nkstot = 2 - Number of irreducible k-points = 2 - - K-POINTS REDUCTION ACCORDING TO SYMMETRY - IBZ DIRECT_X DIRECT_Y DIRECT_Z WEIGHT ibz2bz - 1 0.00000000 0.00000000 0.00000000 0.5000 0 - 2 0.50000000 0.00000000 0.00000000 0.5000 1 - - nkstot now = 2 - K-POINTS DIRECT COORDINATES - KPOINTS DIRECT_X DIRECT_Y DIRECT_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.5000 - 2 0.50000000 0.00000000 0.00000000 0.5000 - - - Number of k-points in this process = 2 - Minimum distributed k-point number = 2 - nks(nspin=2) = 4 - nkstot(nspin=2) = 4 - - K-POINTS CARTESIAN COORDINATES - KPOINTS CARTESIAN_X CARTESIAN_Y CARTESIAN_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.5000 - 2 0.50000000 0.28868360 0.00000000 0.5000 - 3 0.00000000 0.00000000 0.00000000 0.5000 - 4 0.50000000 0.28868360 0.00000000 0.5000 - - - K-POINTS DIRECT COORDINATES - KPOINTS DIRECT_X DIRECT_Y DIRECT_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.5000 - 2 0.50000000 0.00000000 0.00000000 0.5000 - 3 0.00000000 0.00000000 0.00000000 0.5000 - 4 0.50000000 0.00000000 0.00000000 0.5000 - - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - --> #ION MOVE# 1 #ELEC ITER# 1 - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - Total magnetism (Bohr mag/cell) = 1 - Absolute magnetism (Bohr mag/cell) = 1.12149 - Electron number for spin up = 9.5 - Electron number for spin down = 8.5 - Electron density deviation 0.125277 - ---------------------------------------------------------- - Energy Rydberg eV - ---------------------------------------------------------- - E_KohnSham -142.2770827085 -1935.7790196528 - E_Harris -143.4665603429 -1951.9626931241 - E_Fermi -0.0403816764 -0.5494208938 - E_gap(k) 0.0210767116 0.2867633735 - ---------------------------------------------------------- - - - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - --> #ION MOVE# 1 #ELEC ITER# 2 - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - Total magnetism (Bohr mag/cell) = 1.47102 - Absolute magnetism (Bohr mag/cell) = 1.73431 - Electron number for spin up = 9.73551 - Electron number for spin down = 8.26449 - Electron density deviation 0.0831681 - ---------------------------------------------------------- - Energy Rydberg eV - ---------------------------------------------------------- - E_KohnSham -142.3304483127 -1936.5050959474 - E_Harris -143.3480046582 -1950.3496602818 - E_Fermi 0.1727758798 2.3507364425 - E_gap(k) 0.0829435366 1.1285047105 - ---------------------------------------------------------- - - - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - --> #ION MOVE# 1 #ELEC ITER# 3 - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - Total magnetism (Bohr mag/cell) = 1.12003 - Absolute magnetism (Bohr mag/cell) = 1.31163 - Electron number for spin up = 9.56002 - Electron number for spin down = 8.43998 - Electron density deviation 0.0509266 - ---------------------------------------------------------- - Energy Rydberg eV - ---------------------------------------------------------- - E_KohnSham -142.3384608538 -1936.6141121617 - E_Harris -142.9615903984 -1945.0922245608 - E_Fermi 0.1548551093 2.1069118503 - E_gap(k) 0.0983427230 1.3380213902 - ---------------------------------------------------------- - - - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - --> #ION MOVE# 1 #ELEC ITER# 4 - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - Total magnetism (Bohr mag/cell) = 1.53777 - Absolute magnetism (Bohr mag/cell) = 1.6108 - Electron number for spin up = 9.76889 - Electron number for spin down = 8.23111 - Electron density deviation 0.0330854 - ---------------------------------------------------------- - Energy Rydberg eV - ---------------------------------------------------------- - E_KohnSham -142.3352576013 -1936.5705296758 - E_KS(sigma->0) -142.3323774224 -1936.5313428317 - E_Harris -142.7863078681 -1942.7073833888 - E_band -6.1860559614 -84.1656092218 - E_one_elec -82.8067447602 -1126.6435615698 - E_Hartree 52.7505694558 717.7083173431 - E_xc -22.3915899098 -304.6532100528 - E_Ewald -89.8817320294 -1222.9037017083 - E_entropy(-TS) -0.0057603578 -0.0783736881 - E_descf 0.0000000000 0.0000000000 - E_localpp -63.0117129308 -857.3183365997 - E_exx 0.0000000000 0.0000000000 - E_Fermi 0.1563671777 2.1274845966 - E_gap(k) 0.0088059230 0.1198107291 - ---------------------------------------------------------- - - #SCF IS CONVERGED# - #TOTAL ENERGY# -1936.5705297 eV - Write eigenvalues and occupations to file: OUT.ABACUS/eig_occ.txt - - #TOTAL-FORCE (eV/Angstrom)# - ------------------------------------------------------------------------- - Atoms Force_x Force_y Force_z - ------------------------------------------------------------------------- - Zn1 -1.2994798821 -0.7502719805 0.0000000000 - O1 1.2994798821 0.7502719805 0.0000000000 - ------------------------------------------------------------------------- - DIRECT COORDINATES - atom x y z mag - Zn 0.992383886577 0.996191795094 1.000000000000 1.0000 - O 0.340946113423 0.670478204907 0.500000000000 1.0000 - - ATOMIC VELOCITIES - atom vx vy vz - Zn 0 0 0 - O 0 0 0 - - ================================================================ - RELAX STEP: 2 - ================================================================ - NEW-OLD atomic charge density approx. for the potential ! - - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - --> #ION MOVE# 2 #ELEC ITER# 1 - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - Total magnetism (Bohr mag/cell) = 1 - Absolute magnetism (Bohr mag/cell) = 1.22086 - Electron number for spin up = 9.5 - Electron number for spin down = 8.5 - Electron density deviation 0.059569 - ---------------------------------------------------------- - Energy Rydberg eV - ---------------------------------------------------------- - E_KohnSham -142.2850208668 -1935.8870238374 - E_Harris -142.0296340730 -1932.4123082471 - E_Fermi 0.0811057930 1.1035009262 - E_gap(k) 0.0049642264 0.0675417656 - ---------------------------------------------------------- - - - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - --> #ION MOVE# 2 #ELEC ITER# 2 - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - Total magnetism (Bohr mag/cell) = 1.58366 - Absolute magnetism (Bohr mag/cell) = 1.79488 - Electron number for spin up = 9.79183 - Electron number for spin down = 8.20817 - Electron density deviation 0.0753194 - ---------------------------------------------------------- - Energy Rydberg eV - ---------------------------------------------------------- - E_KohnSham -142.3289737580 -1936.4850336018 - E_Harris -142.6117578899 -1940.3325090994 - E_Fermi 0.1443002225 1.9633052486 - E_gap(k) 0.0030396611 0.0413567116 - ---------------------------------------------------------- - - - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - --> #ION MOVE# 2 #ELEC ITER# 3 - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - Total magnetism (Bohr mag/cell) = 1.72786 - Absolute magnetism (Bohr mag/cell) = 1.80974 - Electron number for spin up = 9.86393 - Electron number for spin down = 8.13607 - Electron density deviation 0.0255995 - ---------------------------------------------------------- - Energy Rydberg eV - ---------------------------------------------------------- - E_KohnSham -142.3181389505 -1936.3376184830 - E_KS(sigma->0) -142.3160566944 -1936.3092879352 - E_Harris -142.1804183322 -1934.4638333420 - E_band -6.5047886054 -88.5021893191 - E_one_elec -83.0970862127 -1130.5938596904 - E_Hartree 53.0543074453 721.8408847003 - E_xc -22.3904168680 -304.6372499998 - E_Ewald -89.8807788029 -1222.8907323974 - E_entropy(-TS) -0.0041645122 -0.0566610957 - E_descf 0.0000000000 0.0000000000 - E_localpp -63.1799315004 -859.6070676546 - E_exx 0.0000000000 0.0000000000 - E_Fermi 0.1490975189 2.0285758150 - E_gap(k) 0.0166283571 0.2262404048 - ---------------------------------------------------------- - - #SCF IS CONVERGED# - #TOTAL ENERGY# -1936.3376185 eV - Write eigenvalues and occupations to file: OUT.ABACUS/eig_occ.txt - - #TOTAL-FORCE (eV/Angstrom)# - ------------------------------------------------------------------------- - Atoms Force_x Force_y Force_z - ------------------------------------------------------------------------- - Zn1 -1.6814741322 -0.9708025265 0.0000000000 - O1 1.6814741322 0.9708025265 0.0000000000 - ------------------------------------------------------------------------- - - -------------------------------------------- - !FINAL_ETOT_IS -1936.337618483019 eV - -------------------------------------------- diff --git a/interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm0-nspin2-multik-relax_ b/interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm0-nspin2-multik-relax_ deleted file mode 100644 index b2767839183..00000000000 --- a/interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm0-nspin2-multik-relax_ +++ /dev/null @@ -1,542 +0,0 @@ -NBANDS = 24 - READING UNITCELL INFORMATION - ntype = 2 - lattice constant (Bohr) = 6.1416 - lattice constant (Angstrom) = 3.24999 - - TOTAL ATOM NUMBER = 2 -DIRECT COORDINATES - atom x y z mag vx vy vz -taud_Zn1 0.0000000000 0.0000000000 0.0000000000 1.0000 0.0000000000 0.0000000000 0.0000000000 -taud_O1 0.3333300000 0.6666700000 0.5000000000 1.0000 0.0000000000 0.0000000000 0.0000000000 - - - - Volume (Bohr^3) = 320.983 - Volume (A^3) = 47.5648 - - Lattice vectors: (Cartesian coordinate: in unit of a_0) - +1 +0 +0 - -0.5 +0.866 +0 - +0 +0 +1.6 - Reciprocal vectors: (Cartesian coordinate: in unit of 2 pi/a_0) - +1 +0.577367 -0 - -0 +1.15473 -0 - +0 -0 +0.625 - The esolver type has been set to : ksdft_lcao - - SETUP K-POINTS - nspin = 2 - Input type of k points = Monkhorst-Pack(Gamma) - nkstot = 3 - nkstot_ibz = 2 -K-POINTS REDUCTION ACCORDING TO SYMMETRY - IBZ DIRECT_X DIRECT_Y DIRECT_Z WEIGHT ibz2bz - 1 0.00000000 0.00000000 0.00000000 0.3333 0 - 2 0.33333333 0.00000000 0.00000000 0.6667 1 - - nkstot now = 2 -K-POINTS DIRECT COORDINATES - KPOINTS DIRECT_X DIRECT_Y DIRECT_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.3333 - 2 0.33333333 0.00000000 0.00000000 0.6667 - - - k-point number in this process = 2 - minimum distributed K point number = 2 - nks(nspin=2) = 4 - nkstot(nspin=2) = 4 - -K-POINTS CARTESIAN COORDINATES - KPOINTS CARTESIAN_X CARTESIAN_Y CARTESIAN_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.3333 - 2 0.33333333 0.19245574 0.00000000 0.6667 - 3 0.00000000 0.00000000 0.00000000 0.3333 - 4 0.33333333 0.19245574 0.00000000 0.6667 - - -K-POINTS DIRECT COORDINATES - KPOINTS DIRECT_X DIRECT_Y DIRECT_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.3333 - 2 0.33333333 0.00000000 0.00000000 0.6667 - 3 0.00000000 0.00000000 0.00000000 0.3333 - 4 0.33333333 0.00000000 0.00000000 0.6667 - - DONE : INIT K-POINTS Time : 0.158631 (SEC) - - LCAO ALGORITHM --------------- ION= 1 ELEC= 1-------------------------------- - total magnetism (Bohr mag/cell) = 0.666667 - absolute magnetism (Bohr mag/cell) = 0.859038 - nelec for spin up = 9.33333 - nelec for spin down = 8.66667 - - Density error is 0.148375838039 ----------------------------------------------------------- - Energy Rydberg eV ----------------------------------------------------------- - E_KohnSham -142.1187686707 -1933.6250466660 - E_Harris -143.4705730938 -1952.0172894008 - E_Fermi -0.0353855376 -0.4814449377 ----------------------------------------------------------- - - - LCAO ALGORITHM --------------- ION= 1 ELEC= 2-------------------------------- - total magnetism (Bohr mag/cell) = 1.07915 - absolute magnetism (Bohr mag/cell) = 1.12107 - nelec for spin up = 9.53958 - nelec for spin down = 8.46042 - - Density error is 0.124524909173 ----------------------------------------------------------- - Energy Rydberg eV ----------------------------------------------------------- - E_KohnSham -142.3211355503 -1936.3783893140 - E_Harris -143.6941669355 -1955.0594396866 - E_Fermi 0.1465181279 1.9934813994 ----------------------------------------------------------- - - - LCAO ALGORITHM --------------- ION= 1 ELEC= 3-------------------------------- - total magnetism (Bohr mag/cell) = 1.99248 - absolute magnetism (Bohr mag/cell) = 1.99742 - nelec for spin up = 9.99624 - nelec for spin down = 8.00376 - - Density error is 0.0706868918851 ----------------------------------------------------------- - Energy Rydberg eV ----------------------------------------------------------- - E_KohnSham -142.3807869186 -1937.1899878166 - E_Harris -142.7640217764 -1942.4041655545 - E_Fermi 0.0369440819 0.5026500219 ----------------------------------------------------------- - - - LCAO ALGORITHM --------------- ION= 1 ELEC= 4-------------------------------- - Energy (eV) & Occupations for spin=1 K-point=1 - 1 -15.5220 0.333333 - 2 -5.15869 0.333333 - 3 -5.09633 0.333333 - 4 -5.09579 0.333333 - 5 -4.91098 0.333333 - 6 -4.88255 0.333333 - 7 -2.41462 0.333333 - 8 -1.17166 0.333333 - 9 -0.444834 0.333333 - 10 -0.0995765 0.333331 - 11 6.45991 0.00000 - 12 11.4422 0.00000 - 13 15.0234 0.00000 - 14 15.0596 0.00000 - 15 23.2513 0.00000 - 16 23.2764 0.00000 - 17 25.4767 0.00000 - 18 26.9713 0.00000 - 19 31.1215 0.00000 - 20 31.1309 0.00000 - 21 35.3447 0.00000 - 22 35.5400 0.00000 - 23 44.3560 0.00000 - 24 44.3653 0.00000 - Energy (eV) & Occupations for spin=1 K-point=2 - 1 -15.3130 0.666667 - 2 -5.19707 0.666667 - 3 -5.04322 0.666667 - 4 -4.95407 0.666667 - 5 -4.92694 0.666667 - 6 -4.90614 0.666667 - 7 -1.60476 0.666667 - 8 -0.647511 0.666667 - 9 -0.597491 0.666667 - 10 0.0977501 0.665705 - 11 6.63089 0.00000 - 12 7.81903 0.00000 - 13 12.0662 0.00000 - 14 13.7495 0.00000 - 15 17.0352 0.00000 - 16 18.4561 0.00000 - 17 23.0144 0.00000 - 18 27.6550 0.00000 - 19 29.0735 0.00000 - 20 32.7571 0.00000 - 21 34.0272 0.00000 - 22 34.4864 0.00000 - 23 45.8436 0.00000 - 24 47.8824 0.00000 - Energy (eV) & Occupations for spin=2 K-point=3 - 1 -13.9098 0.333333 - 2 -5.28447 0.333333 - 3 -5.23229 0.333333 - 4 -5.23152 0.333333 - 5 -5.04766 0.333333 - 6 -5.02138 0.333333 - 7 -2.41296 0.333333 - 8 -0.465692 0.333333 - 9 1.52893 6.64394e-13 - 10 1.91854 0.00000 - 11 6.73202 0.00000 - 12 11.8411 0.00000 - 13 14.9947 0.00000 - 14 15.0247 0.00000 - 15 23.2999 0.00000 - 16 23.3150 0.00000 - 17 25.6846 0.00000 - 18 27.1076 0.00000 - 19 31.1088 0.00000 - 20 31.1333 0.00000 - 21 36.4707 0.00000 - 22 36.7183 0.00000 - 23 45.7329 0.00000 - 24 45.7353 0.00000 - Energy (eV) & Occupations for spin=2 K-point=4 - 1 -13.6624 0.666667 - 2 -5.32995 0.666667 - 3 -5.17483 0.666667 - 4 -5.08486 0.666667 - 5 -5.06576 0.666667 - 6 -5.04464 0.666667 - 7 -0.935533 0.666667 - 8 0.232424 0.653097 - 9 0.818953 0.0145341 - 10 1.65447 1.92439e-15 - 11 6.66257 0.00000 - 12 7.91394 0.00000 - 13 12.0744 0.00000 - 14 14.3168 0.00000 - 15 17.1145 0.00000 - 16 18.4599 0.00000 - 17 23.6133 0.00000 - 18 27.6693 0.00000 - 19 30.2686 0.00000 - 20 33.0577 0.00000 - 21 34.2411 0.00000 - 22 34.5673 0.00000 - 23 46.5926 0.00000 - 24 48.6064 0.00000 - total magnetism (Bohr mag/cell) = 1.99807 - absolute magnetism (Bohr mag/cell) = 2.00454 - nelec for spin up = 9.99904 - nelec for spin down = 8.00096 - - Density error is 0.0656436757814 ----------------------------------------------------------- - Energy Rydberg eV ----------------------------------------------------------- - E_KohnSham -142.3712279182 -1937.0599309441 - E_KS(sigma->0) -142.3708533084 -1937.0548341158 - E_Harris -142.7308078006 -1941.9522662316 - E_band -6.2153589294 -84.5642965550 - E_one_elec -82.9844461107 -1129.0613124793 - E_Hartree 53.1237310038 722.7854406707 - E_xc -22.4953290567 -306.0646535560 - E_Ewald -89.8817320293 -1222.9037017083 - E_entropy(-TS) -0.0007492197 -0.0101936567 - E_descf -0.1327025056 -1.8055102146 - E_exx 0.0000000000 0.0000000000 - E_Fermi 0.0387889151 0.5277502650 ----------------------------------------------------------- - - - LCAO ALGORITHM --------------- ION= 1 ELEC= 5-------------------------------- - total magnetism (Bohr mag/cell) = 1.99993 - absolute magnetism (Bohr mag/cell) = 2.03979 - nelec for spin up = 9.99997 - nelec for spin down = 8.00003 - - Density error is 0.0248102508790 ----------------------------------------------------------- - Energy Rydberg eV ----------------------------------------------------------- - E_KohnSham -142.3828162660 -1937.2175985049 - E_KS(sigma->0) -142.3828138132 -1937.2175651321 - E_Harris -142.3278409962 -1936.4696215860 - E_band -6.4176731232 -87.3169223772 - E_one_elec -83.1842752559 -1131.7801274806 - E_Hartree 53.1444543678 723.0673965037 - E_xc -22.4612584429 -305.6010990741 - E_Ewald -89.8817320293 -1222.9037017083 - E_entropy(-TS) -0.0000049057 -0.0000667456 - E_descf 0.0000000000 0.0000000000 - E_exx 0.0000000000 0.0000000000 - E_Fermi 0.0397561785 0.5409105583 ----------------------------------------------------------- - - - charge density convergence is achieved - final etot is -1937.2175985 eV - EFERMI = 0.54091055829 eV - - STATE ENERGY(eV) AND OCCUPATIONS NSPIN == 2 -SPIN UP : - 1/2 kpoint (Cartesian) = 0.0000 0.0000 0.0000 (893 pws) - 1 -17.3578 0.333333 - 2 -4.81961 0.333333 - 3 -4.73286 0.333333 - 4 -4.72898 0.333333 - 5 -4.55927 0.333333 - 6 -4.54757 0.333333 - 7 -2.39449 0.333333 - 8 -2.32744 0.333333 - 9 -2.25455 0.333333 - 10 -2.23106 0.333333 - 11 6.19588 0.00000 - 12 10.9105 0.00000 - 13 15.2835 0.00000 - 14 15.3348 0.00000 - 15 23.4019 0.00000 - 16 23.4194 0.00000 - 17 25.2091 0.00000 - 18 26.8842 0.00000 - 19 31.1933 0.00000 - 20 31.2197 0.00000 - 21 34.2734 0.00000 - 22 34.3051 0.00000 - 23 42.9212 0.00000 - 24 42.9350 0.00000 - - 2/2 kpoint (Cartesian) = 0.33333 0.19246 0.0000 (885 pws) - 1 -17.1796 0.666667 - 2 -4.84064 0.666667 - 3 -4.70446 0.666667 - 4 -4.62781 0.666667 - 5 -4.57157 0.666667 - 6 -4.55825 0.666667 - 7 -3.28584 0.666667 - 8 -2.56381 0.666667 - 9 -1.80637 0.666667 - 10 -0.0190494 0.666632 - 11 6.80206 0.00000 - 12 7.76382 0.00000 - 13 12.3030 0.00000 - 14 12.9847 0.00000 - 15 16.9929 0.00000 - 16 18.6240 0.00000 - 17 22.2820 0.00000 - 18 27.7369 0.00000 - 19 28.0657 0.00000 - 20 32.3697 0.00000 - 21 33.8334 0.00000 - 22 34.6594 0.00000 - 23 44.8974 0.00000 - 24 46.3639 0.00000 - -SPIN DOWN : - 1/2 kpoint (Cartesian) = 0.0000 0.0000 0.0000 (893 pws) - 1 -14.0445 0.333333 - 2 -4.79338 0.333333 - 3 -4.73524 0.333333 - 4 -4.72894 0.333333 - 5 -4.55673 0.333333 - 6 -4.55287 0.333333 - 7 -2.01856 0.333333 - 8 -0.690145 0.333333 - 9 1.71782 5.55112e-17 - 10 1.71906 5.55112e-17 - 11 6.79442 0.00000 - 12 11.7239 0.00000 - 13 15.4745 0.00000 - 14 15.4804 0.00000 - 15 23.7304 0.00000 - 16 23.7520 0.00000 - 17 25.6891 0.00000 - 18 27.2645 0.00000 - 19 31.3363 0.00000 - 20 31.3391 0.00000 - 21 36.5216 0.00000 - 22 36.5324 0.00000 - 23 45.5784 0.00000 - 24 45.5799 0.00000 - - 2/2 kpoint (Cartesian) = 0.33333 0.19246 0.0000 (885 pws) - 1 -13.7943 0.666667 - 2 -4.83973 0.666667 - 3 -4.69011 0.666667 - 4 -4.60271 0.666667 - 5 -4.57650 0.666667 - 6 -4.56292 0.666667 - 7 -0.679818 0.666667 - 8 -0.0349660 0.666645 - 9 1.08344 5.67566e-05 - 10 1.45183 9.16874e-11 - 11 7.10261 0.00000 - 12 8.05898 0.00000 - 13 12.5265 0.00000 - 14 14.2714 0.00000 - 15 17.2717 0.00000 - 16 18.8507 0.00000 - 17 23.5800 0.00000 - 18 27.8847 0.00000 - 19 30.1119 0.00000 - 20 33.0443 0.00000 - 21 34.4305 0.00000 - 22 34.9634 0.00000 - 23 46.4765 0.00000 - 24 48.4894 0.00000 - - correction force for each atom along direction 1 is 0.00411709 - correction force for each atom along direction 2 is 0.00237660 - correction force for each atom along direction 3 is -1.26516e-14 ------------------------------------------------------------------------------------------- - TOTAL-FORCE (eV/Angstrom) ------------------------------------------------------------------------------------------- - Zn1 0.1954664241 0.1128694317 0.0000000000 - O1 -0.1954664241 -0.1128694317 0.0000000000 ------------------------------------------------------------------------------------------- - - - ------------------------------------------- - STEP OF ION RELAXATION : 2 - ------------------------------------------- - NEW-OLD atomic charge density approx. for the potential ! - - LCAO ALGORITHM --------------- ION= 2 ELEC= 1-------------------------------- - total magnetism (Bohr mag/cell) = 1.98397 - absolute magnetism (Bohr mag/cell) = 2.01565 - nelec for spin up = 9.99199 - nelec for spin down = 8.00801 - - Density error is 0.0228305178505 ----------------------------------------------------------- - Energy Rydberg eV ----------------------------------------------------------- - E_KohnSham -142.3657663672 -1936.9856227301 - E_KS(sigma->0) -142.3655346066 -1936.9824694665 - E_Harris -142.4529533528 -1938.1718625268 - E_band -6.3921548433 -86.9697283672 - E_one_elec -83.1041185049 -1130.6895389336 - E_Hartree 53.1961932590 723.7713402320 - E_xc -22.5756668328 -307.1577050760 - E_Ewald -89.8817107675 -1222.9034124254 - E_entropy(-TS) -0.0004635210 -0.0063065271 - E_descf 0.0000000000 0.0000000000 - E_exx 0.0000000000 0.0000000000 - E_Fermi 0.0342773968 0.4663679095 ----------------------------------------------------------- - - - charge density convergence is achieved - final etot is -1936.9856227 eV - EFERMI = 0.46636790945 eV - - STATE ENERGY(eV) AND OCCUPATIONS NSPIN == 2 -SPIN UP : - 1/2 kpoint (Cartesian) = 0.0000 0.0000 0.0000 (893 pws) - 1 -17.7025 0.333333 - 2 -4.59882 0.333333 - 3 -4.48702 0.333333 - 4 -4.48147 0.333333 - 5 -4.33746 0.333333 - 6 -4.33126 0.333333 - 7 -2.96724 0.333333 - 8 -2.55522 0.333333 - 9 -2.43500 0.333333 - 10 -2.06949 0.333333 - 11 6.19551 0.00000 - 12 10.7575 0.00000 - 13 15.5015 0.00000 - 14 15.5549 0.00000 - 15 23.5757 0.00000 - 16 23.5948 0.00000 - 17 25.1721 0.00000 - 18 26.8978 0.00000 - 19 31.2662 0.00000 - 20 31.2667 0.00000 - 21 33.9239 0.00000 - 22 34.1543 0.00000 - 23 42.5429 0.00000 - 24 42.5431 0.00000 - - 2/2 kpoint (Cartesian) = 0.33333 0.19246 0.0000 (885 pws) - 1 -17.5271 0.666667 - 2 -4.60169 0.666667 - 3 -4.48965 0.666667 - 4 -4.42403 0.666667 - 5 -4.35548 0.666667 - 6 -4.31050 0.666667 - 7 -3.48370 0.666667 - 8 -3.13075 0.666667 - 9 -1.99545 0.666667 - 10 0.140729 0.658654 - 11 6.98624 0.00000 - 12 7.79735 0.00000 - 13 12.5109 0.00000 - 14 12.8264 0.00000 - 15 17.0254 0.00000 - 16 18.7928 0.00000 - 17 22.1240 0.00000 - 18 27.7362 0.00000 - 19 27.7983 0.00000 - 20 32.2838 0.00000 - 21 33.8717 0.00000 - 22 34.8442 0.00000 - 23 44.6873 0.00000 - 24 45.9661 0.00000 - -SPIN DOWN : - 1/2 kpoint (Cartesian) = 0.0000 0.0000 0.0000 (893 pws) - 1 -14.6585 0.333333 - 2 -4.52650 0.333333 - 3 -4.44075 0.333333 - 4 -4.43523 0.333333 - 5 -4.29202 0.333333 - 6 -4.28421 0.333333 - 7 -1.78718 0.333333 - 8 -0.947410 0.333333 - 9 0.870330 0.000853653 - 10 1.06811 5.08182e-06 - 11 6.79068 0.00000 - 12 11.5147 0.00000 - 13 15.7693 0.00000 - 14 15.7810 0.00000 - 15 23.9664 0.00000 - 16 24.0061 0.00000 - 17 25.6603 0.00000 - 18 27.3078 0.00000 - 19 31.4681 0.00000 - 20 31.4747 0.00000 - 21 35.9897 0.00000 - 22 36.1264 0.00000 - 23 44.9479 0.00000 - 24 44.9594 0.00000 - - 2/2 kpoint (Cartesian) = 0.33333 0.19246 0.0000 (885 pws) - 1 -14.4180 0.666667 - 2 -4.55358 0.666667 - 3 -4.42144 0.666667 - 4 -4.34051 0.666667 - 5 -4.31573 0.666667 - 6 -4.26983 0.666667 - 7 -0.959860 0.666667 - 8 -0.337689 0.666667 - 9 0.813142 0.00542077 - 10 0.869628 0.00173321 - 11 7.33824 0.00000 - 12 8.13795 0.00000 - 13 12.8109 0.00000 - 14 13.9302 0.00000 - 15 17.3526 0.00000 - 16 19.0843 0.00000 - 17 23.2854 0.00000 - 18 28.0244 0.00000 - 19 29.7367 0.00000 - 20 32.9427 0.00000 - 21 34.4375 0.00000 - 22 35.1991 0.00000 - 23 46.1806 0.00000 - 24 48.1360 0.00000 - - correction force for each atom along direction 1 is -0.00291807 - correction force for each atom along direction 2 is -0.00168508 - correction force for each atom along direction 3 is -1.81361e-12 ------------------------------------------------------------------------------------------- - TOTAL-FORCE (eV/Angstrom) ------------------------------------------------------------------------------------------- - Zn1 -0.2709519327 -0.1564259419 0.0000000000 - O1 0.2709519327 0.1564259419 0.0000000000 ------------------------------------------------------------------------------------------- - - - - -------------------------------------------- - !FINAL_ETOT_IS -1936.985622730090 eV - -------------------------------------------- diff --git a/interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm1-nspin1-multik-scf b/interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm1-nspin1-multik-scf deleted file mode 100644 index c3ae15baf53..00000000000 --- a/interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm1-nspin1-multik-scf +++ /dev/null @@ -1,135 +0,0 @@ - READING UNITCELL INFORMATION - Number of elements = 2 - Lattice constant (Bohr) = 6.1416 - Lattice constant (Angstrom) = 3.24999 - - TOTAL ATOM NUMBER = 2 - - DIRECT COORDINATES - atom x y z mag - Zn 0.000000000000 0.000000000000 0.000000000000 0.0000 - O 0.333329999999 0.666670000001 0.500000000000 0.0000 - - ATOMIC VELOCITIES - atom vx vy vz - Zn 0 0 0 - O 0 0 0 - - Cell volume (Bohr^3) = 320.983 - Cell volume (A^3) = 47.5648 - - Lattice vectors: (Cartesian coordinate: in unit of a_0) - +1 +0 +0 - -0.5 +0.866 +0 - +0 +0 +1.6 - Reciprocal vectors: (Cartesian coordinate: in unit of 2 pi/a_0) - +1 +0.577367 -0 - -0 +1.15473 -0 - +0 -0 +0.625 - - #ENERGY SOLVER# ksdft_lcao - - SETUP K-POINTS - nspin = 1 - Input type of k points = Monkhorst-Pack(Gamma) - nkstot = 2 - Right-handed lattice = 1 - - For reciprocal-space lattice - Bravais lattice type = 11 - Bravais lattice name = 11. Orthorhombic C (base-centered) - Right-handed lattice = 1 - - For k-vectors - Bravais lattice type = 12 - Bravais lattice name = 12. Monoclinic P (simple) - Right-handed lattice = 1 - Number of rotation matrices = 8 - Number of irreducible k-points = 2 - - K-POINTS REDUCTION ACCORDING TO SYMMETRY - IBZ DIRECT_X DIRECT_Y DIRECT_Z WEIGHT ibz2bz - 1 0.00000000 0.00000000 0.00000000 0.5000 0 - 2 0.50000000 0.00000000 0.00000000 0.5000 1 - - nkstot now = 2 - K-POINTS DIRECT COORDINATES - KPOINTS DIRECT_X DIRECT_Y DIRECT_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.5000 - 2 0.50000000 0.00000000 0.00000000 0.5000 - - - Number of k-points in this process = 2 - Minimum distributed k-point number = 2 - - K-POINTS CARTESIAN COORDINATES - KPOINTS CARTESIAN_X CARTESIAN_Y CARTESIAN_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 1.0000 - 2 0.50000000 0.28868360 0.00000000 1.0000 - - - K-POINTS DIRECT COORDINATES - KPOINTS DIRECT_X DIRECT_Y DIRECT_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 1.0000 - 2 0.50000000 0.00000000 0.00000000 1.0000 - - DONE : INIT K-POINTS Time : 0.172973 (SEC) - - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - --> #ION MOVE# 1 #ELEC ITER# 1 - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - Electron density deviation 0.0893377 - ---------------------------------------------------------- - Energy Rydberg eV - ---------------------------------------------------------- - E_KohnSham -142.2578934767 -1935.5179367603 - E_Harris -143.4816580020 -1952.1681073148 - E_Fermi -0.0852280930 -1.1595876945 - E_gap(k) 0.3010005903 4.0953231295 - ---------------------------------------------------------- - - - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - --> #ION MOVE# 1 #ELEC ITER# 2 - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - Electron density deviation 0.0527619 - ---------------------------------------------------------- - Energy Rydberg eV - ---------------------------------------------------------- - E_KohnSham -142.3016957759 -1936.1138976154 - E_Harris -143.6433224427 -1954.3676648718 - E_Fermi 0.1242110447 1.6899779627 - E_gap(k) 0.0037426876 0.0509218768 - ---------------------------------------------------------- - - - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - --> #ION MOVE# 1 #ELEC ITER# 3 - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - Electron density deviation 0.0364934 - ---------------------------------------------------------- - Energy Rydberg eV - ---------------------------------------------------------- - E_KohnSham -142.2855446344 -1935.8941500616 - E_KS(sigma->0) -142.2812283423 -1935.8354238938 - E_Harris -143.1222058877 -1947.2775104019 - E_band -6.0716994722 -82.6097093654 - E_one_elec -82.4655761013 -1122.0017238299 - E_Hartree 52.4529512574 713.6590140163 - E_xc -22.3825551768 -304.5302862042 - E_Ewald -89.8817320294 -1222.9037017083 - E_entropy(-TS) -0.0086325843 -0.1174523356 - E_descf 0.0000000000 0.0000000000 - E_localpp -62.8392385880 -854.9717027777 - E_exx 0.0000000000 0.0000000000 - E_Fermi 0.1211730503 1.6486439277 - E_gap(k) 0.0066246245 0.0901326401 - ---------------------------------------------------------- - - #SCF IS CONVERGED# - #TOTAL ENERGY# -1935.8941501 eV - Write eigenvalues and occupations to file: OUT.ABACUS/eig_occ.txt - - -------------------------------------------- - !FINAL_ETOT_IS -1935.894150061628 eV - -------------------------------------------- diff --git a/interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm1-nspin1-multik-scf_ b/interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm1-nspin1-multik-scf_ deleted file mode 100644 index 264f7a2790c..00000000000 --- a/interfaces/ASE_interface/abacuslite/io/testfiles/lcao-symm1-nspin1-multik-scf_ +++ /dev/null @@ -1,259 +0,0 @@ -NBANDS = 24 - READING UNITCELL INFORMATION - ntype = 2 - lattice constant (Bohr) = 6.1416 - lattice constant (Angstrom) = 3.24999 - - TOTAL ATOM NUMBER = 2 -DIRECT COORDINATES - atom x y z mag vx vy vz -taud_Zn1 0.0000000000 0.0000000000 0.0000000000 0.0000 0.0000000000 0.0000000000 0.0000000000 -taud_O1 0.3333300000 0.6666700000 0.5000000000 0.0000 0.0000000000 0.0000000000 0.0000000000 - - - - Volume (Bohr^3) = 320.983 - Volume (A^3) = 47.5648 - - Lattice vectors: (Cartesian coordinate: in unit of a_0) - +1 +0 +0 - -0.5 +0.866 +0 - +0 +0 +1.6 - Reciprocal vectors: (Cartesian coordinate: in unit of 2 pi/a_0) - +1 +0.577367 -0 - -0 +1.15473 -0 - +0 -0 +0.625 - The esolver type has been set to : ksdft_lcao - - SETUP K-POINTS - nspin = 1 - Input type of k points = Monkhorst-Pack(Gamma) - nkstot = 3 - right hand lattice = 1 -(for reciprocal lattice: ) - BRAVAIS TYPE = 11 - BRAVAIS LATTICE NAME = 11. Orthorhombic C (base-centered) - ibrav = 11 - right hand lattice = 1 -(for k-lattice: ) - BRAVAIS TYPE = 12 - BRAVAIS LATTICE NAME = 12. Monoclinic P (simple) - ibrav = 12 - right hand lattice = 1 - ROTATION MATRICES = 8 - nkstot_ibz = 2 -K-POINTS REDUCTION ACCORDING TO SYMMETRY - IBZ DIRECT_X DIRECT_Y DIRECT_Z WEIGHT ibz2bz - 1 0.00000000 0.00000000 0.00000000 0.3333 0 - 2 0.33333333 0.00000000 0.00000000 0.6667 1 - - nkstot now = 2 -K-POINTS DIRECT COORDINATES - KPOINTS DIRECT_X DIRECT_Y DIRECT_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.3333 - 2 0.33333333 0.00000000 0.00000000 0.6667 - - - k-point number in this process = 2 - minimum distributed K point number = 2 - -K-POINTS CARTESIAN COORDINATES - KPOINTS CARTESIAN_X CARTESIAN_Y CARTESIAN_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.6667 - 2 0.33333333 0.19245574 0.00000000 1.3333 - - -K-POINTS DIRECT COORDINATES - KPOINTS DIRECT_X DIRECT_Y DIRECT_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 0.6667 - 2 0.33333333 0.00000000 0.00000000 1.3333 - - DONE : INIT K-POINTS Time : 0.175334 (SEC) - - LCAO ALGORITHM --------------- ION= 1 ELEC= 1-------------------------------- - - Density error is 0.116736119788 ----------------------------------------------------------- - Energy Rydberg eV ----------------------------------------------------------- - E_KohnSham -142.1079725377 -1933.4781577397 - E_Harris -143.5030834324 -1952.4596152498 - E_Fermi -0.0839876907 -1.1427111550 ----------------------------------------------------------- - - - LCAO ALGORITHM --------------- ION= 1 ELEC= 2-------------------------------- - - Density error is 0.109138222131 ----------------------------------------------------------- - Energy Rydberg eV ----------------------------------------------------------- - E_KohnSham -142.3154389289 -1936.3008828043 - E_Harris -143.8122525428 -1956.6660767972 - E_Fermi 0.1798485039 2.4469644292 ----------------------------------------------------------- - - - LCAO ALGORITHM --------------- ION= 1 ELEC= 3-------------------------------- - - Density error is 0.0699576059847 ----------------------------------------------------------- - Energy Rydberg eV ----------------------------------------------------------- - E_KohnSham -142.3463092814 -1936.7208954978 - E_Harris -142.9090540038 -1944.3774302413 - E_Fermi 0.0604391077 0.8223162464 ----------------------------------------------------------- - - - LCAO ALGORITHM --------------- ION= 1 ELEC= 4-------------------------------- - Energy (eV) & Occupations for spin=1 K-point=1 - 1 -14.1855 0.666667 - 2 -5.40800 0.666667 - 3 -5.35442 0.666667 - 4 -5.35098 0.666667 - 5 -5.16667 0.666667 - 6 -5.13296 0.666667 - 7 -2.53068 0.666667 - 8 -0.450756 0.666667 - 9 1.25776 0.0262682 - 10 1.27497 0.0201777 - 11 6.70135 0.00000 - 12 11.8000 0.00000 - 13 14.8582 0.00000 - 14 14.8935 0.00000 - 15 23.1672 0.00000 - 16 23.1756 0.00000 - 17 25.6709 0.00000 - 18 27.0330 0.00000 - 19 31.0530 0.00000 - 20 31.0704 0.00000 - 21 36.3368 0.00000 - 22 36.3404 0.00000 - 23 45.4171 0.00000 - 24 45.4249 0.00000 - Energy (eV) & Occupations for spin=1 K-point=2 - 1 -13.9469 1.33333 - 2 -5.45027 1.33333 - 3 -5.29277 1.33333 - 4 -5.20247 1.33333 - 5 -5.18723 1.33333 - 6 -5.15617 1.33333 - 7 -1.06442 1.33333 - 8 0.249999 1.33333 - 9 0.623862 1.32772 - 10 1.01520 0.625831 - 11 6.53028 0.00000 - 12 7.87336 0.00000 - 13 11.9322 0.00000 - 14 14.2512 0.00000 - 15 17.0724 0.00000 - 16 18.3473 0.00000 - 17 23.5221 0.00000 - 18 27.6028 0.00000 - 19 29.9496 0.00000 - 20 33.0442 0.00000 - 21 34.1542 0.00000 - 22 34.4176 0.00000 - 23 46.5051 0.00000 - 24 48.5929 0.00000 - - Density error is 0.0750606787978 ----------------------------------------------------------- - Energy Rydberg eV ----------------------------------------------------------- - E_KohnSham -142.2614224469 -1935.5659508624 - E_KS(sigma->0) -142.2579788666 -1935.5190985488 - E_Harris -142.8938709499 -1944.1708541959 - E_band -6.0463316667 -82.2645626649 - E_one_elec -82.7951856427 -1126.4862917089 - E_Hartree 53.2371174712 724.3281427039 - E_xc -22.3749457121 -304.4267541254 - E_Ewald -89.8817320293 -1222.9037017083 - E_entropy(-TS) -0.0068871606 -0.0937046273 - E_descf -0.4397893733 -5.9836413965 - E_exx 0.0000000000 0.0000000000 - E_Fermi 0.0738009554 1.0041135109 ----------------------------------------------------------- - - - LCAO ALGORITHM --------------- ION= 1 ELEC= 5-------------------------------- - - Density error is 0.0745845715339 ----------------------------------------------------------- - Energy Rydberg eV ----------------------------------------------------------- - E_KohnSham -142.2386028344 -1935.2554741064 - E_KS(sigma->0) -142.2353299761 -1935.2109445858 - E_Harris -142.9041261418 -1944.3103832391 - E_band -6.1936955078 -84.2695505825 - E_one_elec -82.6362478781 -1124.3238324822 - E_Hartree 52.8462564148 719.0102052107 - E_xc -22.3898719929 -304.6298365935 - E_Ewald -89.8817320293 -1222.9037017083 - E_entropy(-TS) -0.0065457165 -0.0890590413 - E_descf -0.1704616325 -2.3192494919 - E_exx 0.0000000000 0.0000000000 - E_Fermi -0.0124380091 -0.1692277961 ----------------------------------------------------------- - - !! convergence has not been achieved @_@ - - STATE ENERGY(eV) AND OCCUPATIONS NSPIN == 1 - 1/2 kpoint (Cartesian) = 0.0000 0.0000 0.0000 (893 pws) - 1 -15.5135 0.666667 - 2 -4.89219 0.666667 - 3 -4.81916 0.666667 - 4 -4.81485 0.666667 - 5 -4.64692 0.666667 - 6 -4.63262 0.666667 - 7 -2.19547 0.666667 - 8 -1.24988 0.666667 - 9 -0.220439 0.425770 - 10 -0.142710 0.284736 - 11 6.51116 0.00000 - 12 11.3767 0.00000 - 13 15.2976 0.00000 - 14 15.3258 0.00000 - 15 23.5074 0.00000 - 16 23.5115 0.00000 - 17 25.4888 0.00000 - 18 27.0509 0.00000 - 19 31.2293 0.00000 - 20 31.2429 0.00000 - 21 35.4387 0.00000 - 22 35.4738 0.00000 - 23 44.3435 0.00000 - 24 44.3518 0.00000 - - 2/2 kpoint (Cartesian) = 0.33333 0.19246 0.0000 (885 pws) - 1 -15.3000 1.33333 - 2 -4.92301 1.33333 - 3 -4.78276 1.33333 - 4 -4.69716 1.33333 - 5 -4.65638 1.33333 - 6 -4.64658 1.33333 - 7 -1.61044 1.33333 - 8 -0.684688 1.33310 - 9 -0.432982 1.28827 - 10 0.272787 0.00146112 - 11 6.87354 0.00000 - 12 7.89763 0.00000 - 13 12.3328 0.00000 - 14 13.6686 0.00000 - 15 17.1208 0.00000 - 16 18.6712 0.00000 - 17 22.9805 0.00000 - 18 27.7747 0.00000 - 19 29.1320 0.00000 - 20 32.7679 0.00000 - 21 34.1113 0.00000 - 22 34.7323 0.00000 - 23 45.8107 0.00000 - 24 47.8444 0.00000 - - - - -------------------------------------------- - !FINAL_ETOT_IS -1935.255474106441 eV - -------------------------------------------- diff --git a/interfaces/ASE_interface/abacuslite/io/testfiles/pw-symm0-nspin4-gamma-md_ b/interfaces/ASE_interface/abacuslite/io/testfiles/pw-symm0-nspin4-gamma-md_ deleted file mode 100644 index 45143f8269c..00000000000 --- a/interfaces/ASE_interface/abacuslite/io/testfiles/pw-symm0-nspin4-gamma-md_ +++ /dev/null @@ -1,409 +0,0 @@ -NBANDS = 35 - READING UNITCELL INFORMATION - ntype = 2 - lattice constant (Bohr) = 1 - lattice constant (Angstrom) = 0.529177 - - TOTAL ATOM NUMBER = 2 -DIRECT COORDINATES - atom x y z mag vx vy vz -taud_As1 0.2500000000 0.2500000000 0.2500000000 1.7321 0.0000000000 0.0000000000 0.0000000000 -taud_Ga1 0.0000000000 0.0000000000 0.0000000000 1.7321 0.0000000000 0.0000000000 0.0000000000 - - - - Volume (Bohr^3) = 304.884 - Volume (A^3) = 45.1791 - - Lattice vectors: (Cartesian coordinate: in unit of a_0) - +5.34197 +5.34197 +0 - +0 +5.34197 +5.34197 - +5.34197 +0 +5.34197 - Reciprocal vectors: (Cartesian coordinate: in unit of 2 pi/a_0) - +0.0935984 +0.0935984 -0.0935984 - -0.0935984 +0.0935984 +0.0935984 - +0.0935984 -0.0935984 +0.0935984 - The esolver type has been set to : ksdft_pw - - SETUP K-POINTS - nspin = 4 - Input type of k points = Monkhorst-Pack(Gamma) - nkstot = 1 -K-POINTS DIRECT COORDINATES - KPOINTS DIRECT_X DIRECT_Y DIRECT_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 1.0000 - - - k-point number in this process = 1 - minimum distributed K point number = 1 - -K-POINTS CARTESIAN COORDINATES - KPOINTS CARTESIAN_X CARTESIAN_Y CARTESIAN_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 1.0000 - - -K-POINTS DIRECT COORDINATES - KPOINTS DIRECT_X DIRECT_Y DIRECT_Z WEIGHT - 1 0.00000000 0.00000000 0.00000000 1.0000 - - DONE : INIT K-POINTS Time : 0.062681 (SEC) - - ------------------------------------------- - STEP OF MOLECULAR DYNAMICS : 0 - ------------------------------------------- - init_chg = atomic - DONE : INIT SCF Time : 0.344263 (SEC) - - - PW ALGORITHM --------------- ION= 1 ELEC= 1-------------------------------- -total magnetism (Bohr mag/cell) 0 0 1.43092 - absolute magnetism (Bohr mag/cell) = 1.46002 - - Density error is 0.425536171685 - Error Threshold = 0.01 ----------------------------------------------------------- - Energy Rydberg eV ----------------------------------------------------------- - E_KohnSham -142.5414819203 -1939.3763554796 - E_Harris -145.5088261529 -1979.7491449706 - E_Fermi 0.5470939917 7.4435956292 ----------------------------------------------------------- - - - PW ALGORITHM --------------- ION= 1 ELEC= 2-------------------------------- -total magnetism (Bohr mag/cell) 0 0 1.29536921409 - absolute magnetism (Bohr mag/cell) = 1.47327692817 - - Density error is 0.147809332734 - Error Threshold = 0.00236408984269 ----------------------------------------------------------- - Energy Rydberg eV ----------------------------------------------------------- - E_KohnSham -142.6162210470 -1940.3932334666 - E_Harris -143.6867081318 -1954.9579574558 - E_Fermi 0.5738478660 7.8076007634 ----------------------------------------------------------- - - - PW ALGORITHM --------------- ION= 1 ELEC= 3-------------------------------- -total magnetism (Bohr mag/cell) 0 0 1.90445147148 - absolute magnetism (Bohr mag/cell) = 1.92389538407 - - Density error is 0.044016528552 - Error Threshold = 0.000821162959636 ----------------------------------------------------------- - Energy Rydberg eV ----------------------------------------------------------- - E_KohnSham -142.6278099556 -1940.5509086572 - E_KS(sigma->0) -142.6217168267 -1940.4680073850 - E_Harris -142.2884165619 -1935.9332246392 - E_band -3.4602160123 -47.0786540781 - E_one_elec -81.1361343512 -1103.9137408700 - E_Hartree 53.0833187552 722.2356038215 - E_xc -21.5526527706 -293.2388846958 - E_Ewald -93.0101553311 -1265.4680843686 - E_entropy(-TS) -0.0121862579 -0.1658025443 - E_descf 0.0000000000 0.0000000000 - E_exx 0.0000000000 0.0000000000 - E_Fermi 0.6044100604 8.2234207503 ----------------------------------------------------------- - - - charge density convergence is achieved - final etot is -1940.5509087 eV - - STATE ENERGY(eV) AND OCCUPATIONS NSPIN == 4 - 1/1 kpoint (Cartesian) = 0.0000 0.0000 0.0000 (230 pws) - 1 -8.75727 1.00000 - 2 -8.71932 1.00000 - 3 -8.58843 1.00000 - 4 -8.56405 1.00000 - 5 -8.31527 1.00000 - 6 -8.08147 1.00000 - 7 -7.99239 1.00000 - 8 -7.87334 1.00000 - 9 -7.83653 1.00000 - 10 -7.77801 1.00000 - 11 -5.45404 1.00000 - 12 -4.60934 1.00000 - 13 6.99458 1.00000 - 14 7.16826 1.00000 - 15 7.42743 0.999982 - 16 7.81508 0.983090 - 17 7.88204 0.961984 - 18 8.12687 0.692099 - 19 8.29292 0.358984 - 20 8.73601 0.00386068 - 21 10.5767 0.00000 - 22 10.6695 0.00000 - 23 10.7736 0.00000 - 24 11.2484 0.00000 - 25 11.3392 0.00000 - 26 11.4112 0.00000 - 27 14.2930 0.00000 - 28 14.7540 0.00000 - 29 17.3699 0.00000 - 30 17.4624 0.00000 - 31 17.8797 0.00000 - 32 17.9894 0.00000 - 33 18.4671 0.00000 - 34 18.4688 0.00000 - 35 18.5138 0.00000 - - - ------------------------------------------------------------------------------------------- - TOTAL-FORCE (eV/Angstrom) ------------------------------------------------------------------------------------------- - As1 1.0712942948 2.1781413019 1.2152201133 - Ga1 -1.0712942948 -2.1781413019 -1.2152201133 ------------------------------------------------------------------------------------------- - -------------------------------------------------------------------------------------------- - Total Magnetism (uB) x y z -------------------------------------------------------------------------------------------- - As1 0.0000000000 0.0000000000 3.6203214193 - Ga1 0.0000000000 0.0000000000 3.6203214193 -------------------------------------------------------------------------------------------- - -------------------------------------------------------------------------------------------- - Total Magnetism (uB) Magnitude (uB) Polar (degree) Azimuth (degree) -------------------------------------------------------------------------------------------- - As1 3.6203214193 0.0000000000 0.0000000000 - Ga1 3.6203214193 0.0000000000 0.0000000000 -------------------------------------------------------------------------------------------- - - ------------------------------------------------------------------------------------------------ - Energy (Ry) Potential (Ry) Kinetic (Ry) Temperature (K) - -142.62496 -142.62781 0.0028501339 300.00000 - ------------------------------------------------------------------------------------------------ - - - - ------------------------------------------- - STEP OF MOLECULAR DYNAMICS : 1 - ------------------------------------------- - NEW-OLD atomic charge density approx. for the potential ! - DONE : INIT SCF Time : 1.27344 (SEC) - - - PW ALGORITHM --------------- ION=2 ELEC=1 -------------------------------- - Notice: Threshold on eigenvalues was too large. - hsover_error=0.180000 > DRHO=0.0154165 - Origin diag ethr = 0.0100000 - New diag ethr = 8.56474e-05 -total magnetism (Bohr mag/cell) 0.00000 0.00000 1.92056 - absolute magnetism (Bohr mag/cell) = 1.92336 - - Density error is 0.0450726236251 - Error Threshold = 8.56473592548e-05 ----------------------------------------------------------- - Energy Rydberg eV ----------------------------------------------------------- - E_KohnSham -142.6244218927 -1940.5048116970 - E_KS(sigma->0) -142.6165258278 -1940.3973802229 - E_Harris -141.7382337402 -1928.4476033223 - E_band -2.9490141284 -40.1233956284 - E_one_elec -82.0470893599 -1116.3079196092 - E_Hartree 54.0249296563 735.0468773755 - E_xc -21.5799670555 -293.6105146070 - E_Ewald -93.0065030040 -1265.4183919081 - E_entropy(-TS) -0.0157921297 -0.2148629482 - E_descf 0.0000000000 0.0000000000 - E_exx 0.0000000000 0.0000000000 - E_Fermi 0.6723961422 9.1484188472 ----------------------------------------------------------- - - - charge density convergence is achieved - final etot is -1940.5048117 eV - - STATE ENERGY(eV) AND OCCUPATIONS NSPIN == 4 - 1/1 kpoint (Cartesian) = 0.0000 0.0000 0.0000 (230 pws) - 1 -8.54134 1.00000 - 2 -8.49986 1.00000 - 3 -8.39548 1.00000 - 4 -8.38225 1.00000 - 5 -8.10904 1.00000 - 6 -8.09947 1.00000 - 7 -8.01762 1.00000 - 8 -7.94956 1.00000 - 9 -7.93862 1.00000 - 10 -7.87801 1.00000 - 11 -4.80928 1.00000 - 12 -3.75351 1.00000 - 13 7.57656 1.00000 - 14 7.75964 1.00000 - 15 8.11289 1.00000 - 16 8.69241 0.991104 - 17 8.97032 0.822677 - 18 9.04463 0.705205 - 19 9.17915 0.436556 - 20 9.47574 0.0444576 - 21 10.8453 0.00000 - 22 10.9566 0.00000 - 23 10.9829 0.00000 - 24 11.6632 0.00000 - 25 11.7908 0.00000 - 26 11.8574 0.00000 - 27 14.5416 0.00000 - 28 15.1202 0.00000 - 29 17.6699 0.00000 - 30 17.7719 0.00000 - 31 18.3448 0.00000 - 32 18.4351 0.00000 - 33 18.5704 0.00000 - 34 18.6714 0.00000 - 35 18.7804 0.00000 - - - ------------------------------------------------------------------------------------------- - TOTAL-FORCE (eV/Angstrom) ------------------------------------------------------------------------------------------- - As1 -5.8703800606 -4.7946411869 -6.0982122022 - Ga1 5.8703800606 4.7946411869 6.0982122022 ------------------------------------------------------------------------------------------- - - - - ------------------------------------------------------------------------------------------------ - Energy (Ry) Potential (Ry) Kinetic (Ry) Temperature (K) - -142.62247 -142.62442 0.0019474418 204.98424 - ------------------------------------------------------------------------------------------------ - - - - ------------------------------------------- - STEP OF MOLECULAR DYNAMICS : 2 - ------------------------------------------- - first order charge density extrapolation ! - DONE : INIT SCF Time : 1.71701 (SEC) - - - PW ALGORITHM --------------- ION=3 ELEC=1 -------------------------------- -total magnetism (Bohr mag/cell) 0.00000 0.00000 1.95640 - absolute magnetism (Bohr mag/cell) = 1.95962 - - Density error is 0.477550291489 - Error Threshold = 0.0100000000000 ----------------------------------------------------------- - Energy Rydberg eV ----------------------------------------------------------- - E_KohnSham -142.6498507178 -1940.8507886117 - E_Harris -146.4491358068 -1992.5427141488 - E_Fermi 0.6192799018 8.4257353211 ----------------------------------------------------------- - - - PW ALGORITHM --------------- ION= 3 ELEC= 2-------------------------------- -total magnetism (Bohr mag/cell) 0.00000000000 0.00000000000 1.98933318075 - absolute magnetism (Bohr mag/cell) = 2.03111991516 - - Density error is 0.175985640789 - Error Threshold = 0.00265305717494 ----------------------------------------------------------- - Energy Rydberg eV ----------------------------------------------------------- - E_KohnSham -142.5965805196 -1940.1260103830 - E_Harris -144.3777550999 -1964.3601338067 - E_Fermi 0.6461984287 8.7919806691 ----------------------------------------------------------- - - - PW ALGORITHM --------------- ION= 3 ELEC= 3-------------------------------- -total magnetism (Bohr mag/cell) 0.00000000000 0.00000000000 1.97493083463 - absolute magnetism (Bohr mag/cell) = 2.02445029910 - - Density error is 0.0131512819145 - Error Threshold = 0.000977698004383 ----------------------------------------------------------- - Energy Rydberg eV ----------------------------------------------------------- - E_KohnSham -142.6376231476 -1940.6844239839 - E_KS(sigma->0) -142.6342116592 -1940.6380083031 - E_Harris -142.3168424376 -1936.3199785192 - E_band -2.9205568119 -39.7362139749 - E_one_elec -81.5182312080 -1109.1124353098 - E_Hartree 53.4665656534 727.4499453771 - E_xc -21.5755011497 -293.5497528418 - E_Ewald -93.0036334665 -1265.3793498479 - E_entropy(-TS) -0.0068229768 -0.0928313616 - E_descf 0.0000000000 0.0000000000 - E_exx 0.0000000000 0.0000000000 - E_Fermi 0.6632047358 9.0233633480 ----------------------------------------------------------- - - - charge density convergence is achieved - final etot is -1940.6844240 eV - - STATE ENERGY(eV) AND OCCUPATIONS NSPIN == 4 - 1/1 kpoint (Cartesian) = 0.0000 0.0000 0.0000 (230 pws) - 1 -8.31421 1.00000 - 2 -8.20430 1.00000 - 3 -8.17182 1.00000 - 4 -8.14404 1.00000 - 5 -7.85336 1.00000 - 6 -7.76073 1.00000 - 7 -7.71833 1.00000 - 8 -7.68256 1.00000 - 9 -7.64568 1.00000 - 10 -7.60154 1.00000 - 11 -5.11512 1.00000 - 12 -3.87763 1.00000 - 13 7.03852 1.00000 - 14 7.57655 1.00000 - 15 7.83884 1.00000 - 16 8.41377 0.999233 - 17 8.62549 0.980671 - 18 8.79132 0.886086 - 19 9.25378 0.115555 - 20 9.42488 0.0184549 - 21 10.6927 0.00000 - 22 10.9099 0.00000 - 23 11.0110 0.00000 - 24 11.5312 0.00000 - 25 11.7823 0.00000 - 26 11.8581 0.00000 - 27 14.4850 0.00000 - 28 15.1448 0.00000 - 29 17.4644 0.00000 - 30 17.6904 0.00000 - 31 18.2557 0.00000 - 32 18.5078 0.00000 - 33 18.5427 0.00000 - 34 18.6574 0.00000 - 35 18.7331 0.00000 - - - ------------------------------------------------------------------------------------------- - TOTAL-FORCE (eV/Angstrom) ------------------------------------------------------------------------------------------- - As1 -10.1802174084 -5.5311564523 -6.8477853698 - Ga1 10.1802174084 5.5311564523 6.8477853698 ------------------------------------------------------------------------------------------- - -------------------------------------------------------------------------------------------- - Total Magnetism (uB) x y z -------------------------------------------------------------------------------------------- - As1 0.0000000000 0.0000000000 3.6203214193 - Ga1 0.0000000000 0.0000000000 3.6203214193 -------------------------------------------------------------------------------------------- - -------------------------------------------------------------------------------------------- - Total Magnetism (uB) Magnitude (uB) Polar (degree) Azimuth (degree) -------------------------------------------------------------------------------------------- - As1 3.6203214193 0.0000000000 0.0000000000 - Ga1 3.6203214193 0.0000000000 0.0000000000 -------------------------------------------------------------------------------------------- - - ------------------------------------------------------------------------------------------------ - Energy (Ry) Potential (Ry) Kinetic (Ry) Temperature (K) - -142.57602 -142.63762 0.061605080 6484.4407 - ------------------------------------------------------------------------------------------------ - - -------------------------------------------- - !FINAL_ETOT_IS -1940.684423983896 eV - -------------------------------------------- diff --git a/interfaces/ASE_interface/abacuslite/utils/__init__.py b/interfaces/ASE_interface/abacuslite/utils/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/interfaces/ASE_interface/abacuslite/utils/ksampling.py b/interfaces/ASE_interface/abacuslite/utils/ksampling.py deleted file mode 100644 index 328973e9558..00000000000 --- a/interfaces/ASE_interface/abacuslite/utils/ksampling.py +++ /dev/null @@ -1,260 +0,0 @@ -'''this module provides some useful tools for k-sampling''' -import seekpath -import unittest -from typing import Optional, Tuple, List, Dict - -import numpy as np -from ase.atoms import Atoms - -def convert_kspacing_to_kpts(cell: np.ndarray, - kspacing: float | Tuple[float, float, float])\ - -> Tuple[int, int, int]: - ''' - convert the kspacing to kpts, according to the given cell in Angstrom. - This function would be helpful for those calculators that only support - kpts, but not kspacing. A negative value in kspacing would yield nk=1 - for that direction. - - Parameters - ---------- - cell : np.ndarray - The cell in Angstrom, in shape (3, 3). - kspacing : float or tuple of float - The kspacing in Angstrom. If a tuple of float is given, it would be - interpreted as the kspacing in each direction. - - Returns - ------- - tuple of int - The kpts in each direction. - ''' - # get reciprocal cell vectors... - bvec = 2*np.pi * np.linalg.solve(cell.T, np.eye(3)) - bnorm = np.linalg.norm(bvec, axis=1).tolist() - - kspacing = (kspacing, kspacing, kspacing) if isinstance(kspacing, float) else kspacing - assert len(kspacing) == 3 - nk = [int(norm / kspac) if kspac > 0 else 1 for kspac, norm in zip(kspacing, bnorm)] - return tuple(map(lambda x: max(1, x+1), nk)) - -def get_kpath(atoms: Atoms, n_interpl: int = 10) -> Tuple[np.ndarray, np.ndarray, List[str]]: - '''''' - from seekpath import get_path as _corefunc_seekpath - seeked = _corefunc_seekpath( - structure=(atoms.get_cell().tolist(), - atoms.get_scaled_positions(), - atoms.get_atomic_numbers())) - - # get the most useful informaiton - k = np.array(list(seeked['point_coords'].values())) - assert k.ndim == 2 - nk, dim = k.shape - assert dim == 3 - - # interpolate... - kvec = np.vstack([np.linspace(k[i, :], k[i + 1, :], - num=n_interpl+int(i == nk - 2), - endpoint=bool(i == nk - 2)) - for i in range(nk - 1)]) - - # for easy-drawing - kdist = np.linalg.norm(kvec[1:] - kvec[:-1], axis=1) - kdist = np.cumsum(kdist) - kdist = np.hstack([[0], kdist]) - - kname = list(seeked['point_coords'].keys()) - kname = [[kn] + [''] * (n_interpl-1) for kn in kname[:-1]] + [[kname[-1]]] - kname = [n for kn in kname for n in kn] - - # finally, we assert the consistency of the data - assert len(kvec) == len(kdist) == len(kname), f'{len(kvec)}, {len(kdist)}, {len(kname)}' - return kvec, kdist, kname - -def interpolate_kpath(knodes: np.ndarray, - knames: List[str], - n_interpl: int | List[int] = 10) -> Tuple[np.ndarray, np.ndarray, List[str]]: - ''' - interpolate between the given k-points (nodes, as they are nodes along the path). - The number of interpolated points is given by `n_interpl`, in which the last - number will be ignored. - ''' - if isinstance(n_interpl, int): - n_interpl = [n_interpl] * (len(knodes) - 1) + [1] - assert isinstance(knodes, np.ndarray) - assert knodes.ndim == 2 - nk, dim = knodes.shape - - assert dim == 3 - assert len(n_interpl) == len(knames) == nk - - kvec = np.vstack([np.linspace(knodes[i, :], knodes[i + 1, :], - num=n_interpl[i]+int(i == nk - 2), - endpoint=bool(i == nk - 2)) - for i in range(nk - 1)]) - - # for easy-drawing - kdist = np.linalg.norm(kvec[1:] - kvec[:-1], axis=1) - kdist = np.cumsum(kdist) - kdist = np.hstack([[0], kdist]) - - # padding the knames to have the same length as kvec - kname = [[kn] + [''] * (n_interpl[i]-1) for i, kn in enumerate(knames[:-1])] + [[knames[-1]]] - kname = [n for kn in kname for n in kn] - - # finally, we assert the consistency of the data - assert len(kvec) == len(kdist) == len(kname), f'{len(kvec)}, {len(kdist)}, {len(kname)}' - return kvec, kdist, kname - -def merge_ksgm(segments) -> Tuple[List[str], List[bool]]: - ''' - SeeKpath generates the path of kpoints in the format of list of 2-element tuples. - This is good but not quite compatible with some DFT softwares like ABACUS. This - function will return a list of special kpoint labels accompanied with a boolean - list that helps distinguish the breakpoint of path. - - Parameters - ---------- - segments : List[Tuple[str, str]] - The list of kpath segments. Each segment is a tuple of two strings, - where the first string is the starting point of the segment and the - second string is the ending point of the segment, like [(Gamma, M), - (M, K), (K, Gamma)] - - Returns - ------- - Tuple[List[str], List[bool]] - The first element is the list of special kpoint labels. The second element - is the boolean list that helps distinguish the breakpoint of path. - ''' - klabels, is_brkpt = [segments[0][0]], [False] - for i, (start, end) in enumerate(segments): - if start != klabels[-1]: - is_brkpt[-1] = True - klabels.append(start) - is_brkpt.append(False) - - klabels.append(end) - is_brkpt.append(i == len(segments) - 1) - - return klabels, is_brkpt - -def make_kstring(klabels: List[str], - is_brkpt: List[bool]) -> str: - ''' - Make the kpoint path in the str. For path in which there are break point, - will use the comma to distinguish - - Parameters - ---------- - klabels : List[str] - The list of special kpoint labels. - is_brkpt : List[bool] - The boolean list that helps distinguish the breakpoint of path. - - Returns - ------- - str - The kstring that ABACUS uses to represent the kpath. - ''' - out = '' - for lbl, brkpt in zip(klabels, is_brkpt): - out += lbl - if brkpt: - out += ',' - return out[:-1] # remove the last comma - -def make_klines(kpts, - is_brkpt, - n_interpl, - klabels) -> List[Dict[str, np.ndarray | str | int]]: - ''' - gather the information and output the list of dicts that organizes the - information easy for exporting the ABAUCS KLINES file. Each dictionary - contains the keys `coord`, `label`, `n`, where `coord` is the coordinate - of kpoint, `label` is the label of kpoint, and `n` is the number of - interpolation points between the next kpoint and itself (the next kpoint - included). - - Parameters - ---------- - kpts : np.ndarray - The array of kpoints. - is_brkpt : List[bool] - The boolean list that helps distinguish the breakpoint of path. - n_interpl : int - The number of interpolation points between the kpoints. - klabels : List[str] - The list of special kpoint labels. - - Returns - ------- - List[Dict[str, np.ndarray | str | int]] - The list of dictionaries that organizes the information easy for - exporting the ABAUCS KLINES file. - ''' - def fspawnk(c: np.ndarray, n: int, label: str) -> Dict[str, np.ndarray | str | int]: - return {'coord': c, 'label': label, 'n': n} - - return [fspawnk(c, 1 if is_brkpt[i] else n_interpl, klbl) - for i, (c, klbl) in enumerate(zip(kpts, klabels))] - -def kpathgen(atoms: Atoms) -> Tuple[str, Dict[str, List[float]]]: - ''' - Generate the k-path by default SeeK-Path flavor in the format - compatible with ase bandpath module - ''' - kpathseen = seekpath.get_path( - structure=(np.array(atoms.get_cell()), - atoms.get_scaled_positions(), - atoms.get_atomic_numbers()), - with_time_reversal=True - ) - - # convert the kpoint path to the format that is acceptable by ASE - kpathstr, is_brkpt = merge_ksgm(kpathseen['path']) - fklblfilter = lambda lbl: lbl if lbl != 'GAMMA' else 'G' - kpathstr = make_kstring([fklblfilter(lbl) for lbl in kpathstr], is_brkpt) - kpathstr = ''.join([k if k != 'GAMMA' else 'G' for k in kpathstr]) - # seekpath use 'GAMMA' as the gamma point symbol, while the ASE use 'G' - kspecial = {k: v for k, v in kpathseen['point_coords'].items() if k != 'GAMMA'} - kspecial['G'] = [0.0, 0.0, 0.0] - return kpathstr, kspecial - -# the following are for quick test on the silicon case -SILICON_KNODES = np.array([[0.0000000000, 0.0000000000, 0.0000000000], - [0.5000000000, 0.0000000000, 0.5000000000], - [0.6250000000, 0.2500000000, 0.6250000000], - [0.3750000000, 0.3750000000, 0.7500000000], - [0.0000000000, 0.0000000000, 0.0000000000], - [0.5000000000, 0.5000000000, 0.5000000000], - [0.5000000000, 0.2500000000, 0.7500000000], - [0.5000000000, 0.0000000000, 0.5000000000]]) -SILICON_KNAMES = ["G", "X", "X/U", "K", "G", "L", "W", "X"] -SILICON_NINTPL = [50, 50, 1, 50, 50, 50, 50, 1] # from the DeePTB example :) - -class TestKsampling(unittest.TestCase): - - def test_convert_kspacing(self): - # test-reference: ABACUS 3.8.4 implementation - from ase.geometry import cellpar_to_cell - - cell = cellpar_to_cell([4.22798145, 4.22798145, 4.22798145, 60, 60, 60]) - kpts = convert_kspacing_to_kpts(cell, 0.03*1.889725989) - # kspacing 0.03 Bohr-1, multiply 1.889725989 to convert to Angstrom-1 - self.assertEqual(kpts, (33, 33, 33)) - - def test_merge_ksgm(self): - klables, is_brkpt = merge_ksgm([('GAMMA', 'X'), ('X', 'U'), ('K', 'GAMMA'), - ('GAMMA', 'L'), ('L', 'W'), ('W', 'X')]) - self.assertEqual(klables, ['GAMMA', 'X', 'U', 'K','GAMMA', 'L', 'W', 'X']) - self.assertEqual(is_brkpt, [False, False, True, False, False, False, False, True]) - - def test_make_kstring(self): - fklblfilter = lambda lbl: lbl if lbl != 'GAMMA' else 'G' - klabels = ['GAMMA', 'X', 'U', 'K','GAMMA', 'L', 'W', 'X'] - klabels = [fklblfilter(lbl) for lbl in klabels] - is_brkpt = [False, False, True, False, False, False, False, True] - self.assertEqual(make_kstring(klabels, is_brkpt), 'GXU,KGLWX') - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/interfaces/ASE_interface/abacuslite/xtest.sh b/interfaces/ASE_interface/abacuslite/xtest.sh deleted file mode 100755 index 47640cfc28b..00000000000 --- a/interfaces/ASE_interface/abacuslite/xtest.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -set -e # Exit on error - -python3 core.py -python3 ./io/generalio.py -v -python3 ./io/legacyio.py -v -python3 ./io/latestio.py -v -python3 ./utils/ksampling.py -v - diff --git a/interfaces/ASE_interface/examples/bandstructure.py b/interfaces/ASE_interface/examples/bandstructure.py deleted file mode 100644 index 626638e21e9..00000000000 --- a/interfaces/ASE_interface/examples/bandstructure.py +++ /dev/null @@ -1,82 +0,0 @@ -''' -This example shows how to run a SCF calculation with ABACUS -of Si diamond structure. - -To run this example, please install the SeeK-path package: -``` -pip install seekpath -``` -. The SeeK-path package recommands you cite by the way posted here: -https://seekpath.materialscloud.io/ -''' -import shutil -from pathlib import Path # a more Pythonic alternative to the os.path -here = Path(__file__).parent -# to the directory where the pseudopotential and orbital files are stored -# In your case you change to the appropriate one -pporb = here.parent.parent.parent / 'tests' / 'PP_ORB' - -from ase.build import bulk -from abacuslite import Abacus, AbacusProfile -from abacuslite.utils.ksampling import kpathgen - -# AbacusProfile: the interface connecting the Abacus calculator instance -# with the file system and the enviroment -aprof = AbacusProfile( - command='mpirun -np 4 abacus', - pseudo_dir=pporb, - orbital_dir=pporb, - omp_num_threads=1, -) - -# Abacus: the calculator instance -jobdir = here / 'bandstructure' -abacus = Abacus( - profile=aprof, - directory=str(jobdir), - pseudopotentials={'Si': 'Si_ONCV_PBE-1.0.upf'}, - basissets={'Si': 'Si_gga_8au_100Ry_2s2p1d.orb'}, - inp={ - 'calculation': 'scf', - 'nspin': 1, - 'basis_type': 'lcao', - 'ks_solver': 'genelpa', - 'ecutwfc': 100, - 'symmetry': 1, - 'kspacing': 0.1 - } -) - -# get the structure, can also from the -# ``` -# from ase.io import read -# atoms = read(...) -# ``` -atoms = bulk('Si', 'diamond', a=5.43) - -# bind the atoms with the abacus -atoms.calc = abacus - -# perform the SCF calculation to get the converged wavefunction -print('SCF calculation get energy:', atoms.get_potential_energy()) - -kpathstr, kspecial = kpathgen(atoms) -# instantiate the bandpath -bandpath = atoms.cell.bandpath(path=kpathstr, - npoints=50, - special_points=kspecial) - -# derive the band structure calculator from SCF calculator -bscalc = atoms.calc.fixed_density(bandpath) -atoms.calc = bscalc -_ = atoms.get_potential_energy() # NSCF calculation will be performed - -bs = bscalc.band_structure() -bs.write('bandstructure.json') -# you can use the ase-cli to plot the JSON file later by: -# ``` -# ase band-structure bandstructure.json -r -10 15 -# ``` -bs.plot(emin=-10, emax=15, filename='bandstructure.png') - -shutil.rmtree(jobdir) \ No newline at end of file diff --git a/interfaces/ASE_interface/examples/cellrelax.py b/interfaces/ASE_interface/examples/cellrelax.py deleted file mode 100644 index a6b5cc06c5f..00000000000 --- a/interfaces/ASE_interface/examples/cellrelax.py +++ /dev/null @@ -1,76 +0,0 @@ -''' -This example shows how to perform a cell-ion joint relaxation with ABACUS -of Si diamond structure. -''' -import shutil -from pathlib import Path # a more Pythonic alternative to the os.path -here = Path(__file__).parent -# to the directory where the pseudopotential and orbital files are stored -# In your case you change to the appropriate one -pporb = here.parent.parent.parent / 'tests' / 'PP_ORB' - -from ase.build import bulk -from ase.filters import FrechetCellFilter -from ase.optimize import BFGS -from abacuslite import Abacus, AbacusProfile - -# AbacusProfile: the interface connecting the Abacus calculator instance -# with the file system and the enviroment -aprof = AbacusProfile( - command='mpirun -np 4 abacus', - pseudo_dir=pporb, - orbital_dir=pporb, - omp_num_threads=1, -) - -# Abacus: the calculator instance -jobdir = here / 'scf' -abacus = Abacus( - profile=aprof, - directory=str(jobdir), - pseudopotentials={'Si': 'Si_ONCV_PBE-1.0.upf'}, - basissets={'Si': 'Si_gga_8au_100Ry_2s2p1d.orb'}, - inp={ - 'calculation': 'scf', - 'nspin': 1, - 'basis_type': 'lcao', - 'ks_solver': 'genelpa', - 'ecutwfc': 100, - 'symmetry': 1, - 'kspacing': 0.1, - 'cal_force': 1, # let ABACUS calculate the forces - 'cal_stress': 1 # let ABACUS calculate the stress - } -) - -# get the structure, can also from the -# ``` -# from ase.io import read -# atoms = read(...) -# ``` -atoms = bulk('Si', 'diamond', a=5.43) - -# bind the atoms with the abacus -atoms.calc = abacus - -# print the energy before relaxation -print(f'Before: {atoms.get_potential_energy()}') -print(f'Cell: {atoms.get_cell()}') - -# perform the relaxation calculation -dyn = BFGS(FrechetCellFilter(atoms), logfile='-') -# or a fixed alpha, beta, gamma relaxation, if you needed -# dyn = BFGS(FrechetCellFilter( -# atoms, -# mask=[True, True, True, False, False, False] -# ), logfile='-') -# the first three mask elements are for the strain, -# the last three are for the shear -dyn.run(fmax=0.05) - -# print the energy after relaxation -print(f'After: {atoms.get_potential_energy()}') -print(f'Cell: {atoms.get_cell()}') - -# remove the temporary job directory (including all files inside) -shutil.rmtree(jobdir) diff --git a/interfaces/ASE_interface/examples/constraintmd.py b/interfaces/ASE_interface/examples/constraintmd.py deleted file mode 100644 index b28a781490c..00000000000 --- a/interfaces/ASE_interface/examples/constraintmd.py +++ /dev/null @@ -1,101 +0,0 @@ -import shutil -import tempfile -from pathlib import Path # a more Pythonic alternative to the os.path -here = Path(__file__).parent -# to the directory where the pseudopotential and orbital files are stored -# In your case you change to the appropriate one -pporb = here.parent.parent.parent / 'tests' / 'PP_ORB' - -import numpy as np -import matplotlib.pyplot as plt -from ase.io import read, Trajectory -from ase.md import Langevin -from ase.md.velocitydistribution import MaxwellBoltzmannDistribution -from ase.units import fs -from ase.constraints import FixCartesian -from abacuslite import AbacusProfile, Abacus - -aprof = AbacusProfile( - command='mpirun -np 16 abacus', - pseudo_dir=pporb, - orbital_dir=pporb, - omp_num_threads=1, -) -jobdir = here / 'constraintmd' -abacus = Abacus( - profile=aprof, - directory=str(jobdir), - pseudopotentials={ - 'C': 'C_ONCV_PBE-1.0.upf', - 'H': 'H_ONCV_PBE-1.0.upf', - 'F': 'F_ONCV_PBE-1.0.upf', - }, - basissets={ - 'C': 'C_gga_8au_100Ry_2s2p1d.orb', - 'H': 'H_gga_8au_100Ry_2s1p.orb', - 'F': 'F_gga_7au_100Ry_2s2p1d.orb', - }, - inp={ - 'calculation': 'scf', - 'nspin': 2, - 'basis_type': 'lcao', - 'ks_solver': 'genelpa', - 'ecutwfc': 40, - 'scf_thr': 1e-6, - 'symmetry': 1, - 'gamma_only': True, - 'init_chg': 'auto' # small trick, use the previous charge density - } -) - -mol = ''' 6 -CH4-F - C -0.00000000 -0.00000000 -1.50074532 - H 0.00000000 -1.01539888 -1.16330635 - H -0.87936122 0.50769944 -1.16330635 - H 0.87936122 0.50769944 -1.16330635 - H -0.00000000 -0.00000000 -2.57055225 - F 0.00000000 -0.00000000 0.88279136 -''' -with tempfile.NamedTemporaryFile(mode='w', suffix='.xyz') as f: - f.write(mol) - f.flush() - atoms = read(f.name) - -atoms.center(vacuum=5.0) # to reduce the computational cost -# view(atoms) - -# constraint the No.1, 5, 6 atoms' X and Y coordiantes so that -# they can only move along the z-axis -constraint = FixCartesian(a=[0, 4, 5], mask=(True, True, False)) -# apply -atoms.set_constraint(constraint) -MaxwellBoltzmannDistribution(atoms, temperature_K=300) - -atoms.calc = abacus -dyn = Langevin(atoms, - timestep=1.0 * fs, - temperature_K=300, - friction=0.004, - logfile='-', - trajectory='constraintmd.traj') -dyn.run(5) - -# let's see if the X, Y coordinates of No.1, 5, and 6 atoms are really -# fixed -with Trajectory('constraintmd.traj') as traj: - traj = np.array([atoms.get_positions() for atoms in traj]) - -# transpose from (nframe, natom, 3) to (natom, nframe, 3) -traj = traj.transpose(1, 0, 2) - -# plot the trajectory of No.1, 5, and 6 atoms -plt.plot(traj[0, :, 2], label='C') -plt.plot(traj[4, :, 2], label='H') -plt.plot(traj[5, :, 2], label='F') -plt.xlabel('Step') -plt.ylabel('Z (Å)') -plt.legend() -plt.show() - -shutil.rmtree(jobdir) \ No newline at end of file diff --git a/interfaces/ASE_interface/examples/dos.py b/interfaces/ASE_interface/examples/dos.py deleted file mode 100644 index 39a5f12fa84..00000000000 --- a/interfaces/ASE_interface/examples/dos.py +++ /dev/null @@ -1,65 +0,0 @@ -''' -This example shows how to run a SCF calculation with ABACUS -of Si diamond structure. -''' -import shutil -from pathlib import Path # a more Pythonic alternative to the os.path -here = Path(__file__).parent -# to the directory where the pseudopotential and orbital files are stored -# In your case you change to the appropriate one -pporb = here.parent.parent.parent / 'tests' / 'PP_ORB' - -import matplotlib.pyplot as plt -from ase.build import bulk -from ase.dft import DOS -from abacuslite import Abacus, AbacusProfile - -aprof = AbacusProfile( - command='mpirun -np 4 abacus', - pseudo_dir=pporb, - orbital_dir=pporb, - omp_num_threads=1, -) - -jobdir = here / 'scf' -abacus = Abacus( - profile=aprof, - directory=str(jobdir), - pseudopotentials={'Si': 'Si_ONCV_PBE-1.0.upf'}, - basissets={'Si': 'Si_gga_8au_100Ry_2s2p1d.orb'}, - inp={ - 'calculation': 'scf', - 'nspin': 1, - 'basis_type': 'lcao', - 'ks_solver': 'genelpa', - 'ecutwfc': 100, - 'symmetry': 1, - 'kspacing': 0.1 - } -) - -# get the structure, can also from the -# ``` -# from ase.io import read -# atoms = read(...) -# ``` -atoms = bulk('Si', 'diamond', a=5.43) - -# bind the atoms with the abacus -atoms.calc = abacus - -# calculate! -print(atoms.get_potential_energy()) - -doscalc = DOS(atoms.calc, width=0.1) -e, dos = doscalc.get_energies(), doscalc.get_dos() - -plt.plot(e, dos) -plt.xlim(-5, 5) -plt.xlabel('E - E_f (eV)') -plt.ylabel('DOS') -plt.title('DOS of Si Diamond') -plt.show() - -# remove the temporary job directory (including all files inside) -shutil.rmtree(jobdir) diff --git a/interfaces/ASE_interface/examples/md.py b/interfaces/ASE_interface/examples/md.py deleted file mode 100644 index 2fc2bbefbb8..00000000000 --- a/interfaces/ASE_interface/examples/md.py +++ /dev/null @@ -1,72 +0,0 @@ -'''this example recap the example of MD in abacus example -directory, but there the CSVR thermostat is used instead -of those implemented in ABACUS. - -In ASE, the CSVR thermostat is named as the Bussi -''' -import shutil -from pathlib import Path # a more Pythonic alternative to the os.path -here = Path(__file__).parent -# to the directory where the pseudopotential and orbital files are stored -# In your case you change to the appropriate one -pporb = here.parent.parent.parent / 'tests' / 'PP_ORB' - -import numpy as np -from ase.atoms import Atoms -from ase.md import Bussi -from ase.md.velocitydistribution import MaxwellBoltzmannDistribution -from ase.units import fs -from abacuslite import Abacus, AbacusProfile - -cell = np.eye(3) * 5.43090251 -taud = [ - [0.00000000, 0.00000000, 0.00000000], - [0.00000000, 0.50000000, 0.50000000], - [0.50000000, 0.00000000, 0.50000000], - [0.50000000, 0.50000000, 0.00000000], - [0.25000000, 0.25000000, 0.25000000], - [0.25000000, 0.75000000, 0.75000000], - [0.75000000, 0.25000000, 0.75000000], - [0.75000000, 0.75000000, 0.25000000], -] -atoms = Atoms(symbols=['Si' for _ in range(8)], - scaled_positions=taud, - cell=cell, - pbc=True) - -aprof = AbacusProfile( - command='mpirun -np 4 abacus', - pseudo_dir=pporb, - orbital_dir=pporb, - omp_num_threads=1, -) - -jobdir = here / 'md' -abacus = Abacus( - profile=aprof, - directory=str(jobdir), - pseudopotentials={'Si': 'Si_ONCV_PBE-1.0.upf'}, - basissets={'Si': 'Si_gga_8au_100Ry_2s2p1d.orb'}, - inp={ - 'calculation': 'scf', # still use SCF here because the MD is driven by ASE - 'nspin': 1, - 'basis_type': 'lcao', - 'ks_solver': 'genelpa', - 'ecutwfc': 100, - 'symmetry': 1, - 'kspacing': 0.25 # highly unconverged, just for demo - } -) - -atoms.calc = abacus -# initialize the velocities, necessary for CSVR -MaxwellBoltzmannDistribution(atoms, temperature_K=300) - -dyn = Bussi(atoms, - timestep=1*fs, - temperature_K=300, - taut=10*fs, - logfile='-') # let's see the trajectory -dyn.run(2) - -shutil.rmtree(jobdir) diff --git a/interfaces/ASE_interface/examples/metadynamics.py b/interfaces/ASE_interface/examples/metadynamics.py deleted file mode 100644 index 5595d131436..00000000000 --- a/interfaces/ASE_interface/examples/metadynamics.py +++ /dev/null @@ -1,146 +0,0 @@ -''' -To run the metadynamics, you need to configure the plumed correctly: - -conda install -c conda-forge plumed=2.8.2=mpi_openmpi_hb0545ae_0 -conda install -c conda-forge py-plumed - -You may be also interested in a newer version of plumed, a possible solution is -to search `plumed` at conda website: -https://anaconda.org/channels/conda-forge/packages/plumed/files -in which, up to 2026/02/25, the latest version is -``` -linux-64/plumed-2.9.2-mpi_openmpi_h02da92d_0.conda, -``` -you can install it with: -conda install -c conda-forge plumed=2.9.2=mpi_openmpi_h02da92d_0 - -we do not recommend the default version of plumed installed by conda, which -is nompi-labelled, may cause segmentation fault error during the MTD run. - -In this example, we will run a metadynamics simulation to explore the -substitution reaction, during which the famous Walden inversion -(https://en.wikipedia.org/wiki/Walden_inversion) happens: -``` -CH4 + F -> CH3F + H -``` -. Because the F is a radical, we set nspin 2 throughout the simulation. -The structure is like - H H - \ / - H - C F· - | - H -, thus the best Collective Variable (CV) is the difference between the -H-C bond length and the C-F bond length. - -CSVR thermostat is used to maintain the temperature during the simulation. -''' - -import shutil -import tempfile -from pathlib import Path # a more Pythonic alternative to the os.path -here = Path(__file__).parent -# to the directory where the pseudopotential and orbital files are stored -# In your case you change to the appropriate one -pporb = here.parent.parent.parent / 'tests' / 'PP_ORB' - -from ase.io import read -from ase.md import Bussi -from ase.md.velocitydistribution import MaxwellBoltzmannDistribution -from ase.units import ( - fs, - kJ as _kJ, - mol as _mol, -) -_ps = 1000 * fs -from ase.constraints import FixCartesian -from ase.calculators.plumed import Plumed -from abacuslite import AbacusProfile, Abacus - -aprof = AbacusProfile( - command='mpirun -np 16 abacus', - pseudo_dir=pporb, - orbital_dir=pporb, - omp_num_threads=1, -) -jobdir = here / 'constraintmd' -abacus = Abacus( - profile=aprof, - directory=str(jobdir), - pseudopotentials={ - 'C': 'C_ONCV_PBE-1.0.upf', - 'H': 'H_ONCV_PBE-1.0.upf', - 'F': 'F_ONCV_PBE-1.0.upf', - }, - basissets={ - 'C': 'C_gga_8au_100Ry_2s2p1d.orb', - 'H': 'H_gga_8au_100Ry_2s1p.orb', - 'F': 'F_gga_7au_100Ry_2s2p1d.orb', - }, - inp={ - 'calculation': 'scf', - 'nspin': 2, - 'basis_type': 'lcao', - 'ks_solver': 'genelpa', - 'ecutwfc': 40, - 'scf_thr': 1e-6, - 'symmetry': 1, - 'gamma_only': True, - 'init_chg': 'auto' # small trick, use the previous charge density - } -) - -ch4f = ''' 6 -ABACUS ASE Plugin Metadynamics example structure - C -0.00000000 -0.00000000 -1.50074532 - F 0.00000000 -0.00000000 0.88279136 - H 0.00000000 -1.01539888 -1.16330635 - H -0.87936122 0.50769944 -1.16330635 - H 0.87936122 0.50769944 -1.16330635 - H -0.00000000 -0.00000000 -2.57055225 -''' -with tempfile.NamedTemporaryFile(mode='w', suffix='.xyz') as f: - f.write(ch4f) - f.flush() - atoms = read(f.name) - -atoms.center(vacuum=5.0) # to reduce the computational cost - -# constraint the No.5, 6 atoms' X and Y coordiantes so that -# they can only move along the z-axis, also fix the atom C's -# all components -atoms.set_constraint([FixCartesian(a=[4, 5], mask=(True, True, False)), - FixCartesian(a=[0])]) -MaxwellBoltzmannDistribution(atoms, temperature_K=300) - -setup = [# define the unit within the PLUMED runtime - f'UNITS LENGTH=A TIME={1/_ps} ENERGY={_mol/_kJ}', - # define the two bond lengths - 'd1: DISTANCE ATOMS=1,5', - 'd2: DISTANCE ATOMS=1,6', - # define the CV as the difference between the two bond lengths - 'c1: MATHEVAL ARG=d1,d2 VAR=a,b FUNC=a-b PERIODIC=NO', - # add walls to confine the position of H and F atoms - # such that the C-H bond will have length between 0.5 and 2.0, - # and the C-F bond will have length between 1.0 and 3.0 - 'lwall: LOWER_WALLS ARG=d1,d2 AT=0.5,1.0 KAPPA=150.0,150.0 EXP=2,2', - 'uwall: UPPER_WALLS ARG=d1,d2 AT=2.0,3.0 KAPPA=150.0,150.0 EXP=2,2', - # setup the metadynamics simulation - 'metad: METAD ARG=c1 PACE=5 HEIGHT=0.2 SIGMA=0.05 FILE=HILLS TEMP=300', - 'PRINT STRIDE=1 ARG=d1,d2,c1 FILE=COLVAR'] - -atoms.calc = Plumed(calc=abacus, - input=setup, - timestep=1.0 * fs, - atoms=atoms, - kT=0.1) - -dyn = Bussi(atoms, - timestep=1.0 * fs, - temperature_K=300, - taut=10.0 * fs, - trajectory='metadynamics.traj', - logfile='-') - -dyn.run(20) -shutil.rmtree(jobdir) diff --git a/interfaces/ASE_interface/examples/neb.py b/interfaces/ASE_interface/examples/neb.py deleted file mode 100644 index 1118fcadc19..00000000000 --- a/interfaces/ASE_interface/examples/neb.py +++ /dev/null @@ -1,111 +0,0 @@ -''' -PbTiO3 ferroelectric inversion energy barrier - -Learn how to use the NEB module in ASE, please refer to the online manual at: -https://ase-lib.org/examples_generated/tutorials/neb_idpp.html -''' -from pathlib import Path -here = Path(__file__).parent - -import numpy as np -from ase.io import Trajectory -from ase.atoms import Atoms -from ase.optimize import FIRE -from ase.mep import NEB -import matplotlib.pyplot as plt -from abacuslite import Abacus, AbacusProfile - -pporb = here.parent.parent.parent / 'tests' / 'PP_ORB' - -elem = ['Ti', 'Pb', 'O', 'O', 'O'] -taud = np.array([ - [0.5, 0.5, 0.5948316037314115], - [0.0, 0.0, 0.1235879499999999], - [0.0, 0.5, 0.5094847864489368], - [0.5, 0.0, 0.5094847864489368], - [0.5, 0.5, 0.0088672395150394], -]) -cell = np.array([ - [3.8795519, 0.0000000, 0.00000000], - [0.0000000, 3.8795519, 0.00000000], - [0.0000000, 0.0000000, 4.28588762], -]) - -# we have relaxed with the parameters above :) -up = Atoms(elem, cell=cell, scaled_positions=taud) - -# get the polarisation inversed by inversing the Ti atoms -taud = np.array([ - [0.5, 0.5, 0.6508136593687969], - [0.0, 0.0, 0.1235879499999999], - [0.0, 0.5, 0.7348401327639794], - [0.5, 0.0, 0.7348401327639794], - [0.5, 0.5, 0.2364165087650052], -]) -dw = Atoms(elem, cell=cell, scaled_positions=taud) - -aprof = AbacusProfile( - command='mpirun -np 8 abacus_2p', - pseudo_dir=pporb, - orbital_dir=pporb, - omp_num_threads=1 -) -pseudopotentials = { - 'Ti': 'Ti_ONCV_PBE-1.0.upf', - 'Pb': 'Pb_ONCV_PBE-1.0.upf', - 'O' : 'O_ONCV_PBE-1.0.upf', -} -basissets = { - 'Ti': 'Ti_gga_8au_100Ry_4s2p2d1f.orb', - 'Pb': 'Pb_gga_7au_100Ry_2s2p2d1f.orb', - 'O' : 'O_gga_7au_100Ry_2s2p1d.orb', -} -inp = { - 'profile': aprof, - 'pseudopotentials': pseudopotentials, - 'basissets': basissets, - 'inp': { - 'basis_type': 'lcao', - 'symmetry': 1, - 'kspacing': 0.25, # Oops! - 'init_chg': 'auto', - 'cal_force': 1, - } -} - -n_replica = 7 # the ini and fin images included. 7 is acceptable for production -replica = [] -for irep in range(n_replica): - image = up.copy() if irep <= (n_replica // 2) else dw.copy() - # attach the calculator to each image, so that we can run the optimization - image.calc = Abacus(**inp, directory=here / f'neb-{irep}') - replica.append(image) - -neb = NEB(replica, - k=0.05, # too high value is hard to converge - climb=False, # use True in production run, though CI-NEB is harder to converge - parallel=True) -neb.interpolate('idpp') - -qn = FIRE(neb, trajectory=here / 'neb.traj') -qn.run(fmax=0.05) - -energies = [] -# get the energy profile along the reaction path -with Trajectory(here / 'neb.traj') as traj: - replica = traj[-7:] # the last NEB frames - for i, rep in enumerate(replica): - rep: Atoms # type hint - # the energies of the initial and the final state - # are not calculated, here we calculate them - rep.calc = Abacus(**inp, directory=here / f'neb-{i}') - energies.append(rep.get_potential_energy()) - -energies = np.array(energies) -# plot the energy profile -plt.plot(energies - energies[0], 'o-') -plt.xlabel('NEB image index') -plt.ylabel('Total energies (eV)') -plt.title('Energy profile along the reaction path') -plt.savefig(here / 'energy_profile.png', dpi=300) -plt.close() diff --git a/interfaces/ASE_interface/examples/relax.py b/interfaces/ASE_interface/examples/relax.py deleted file mode 100644 index ac769055a40..00000000000 --- a/interfaces/ASE_interface/examples/relax.py +++ /dev/null @@ -1,67 +0,0 @@ -''' -This example shows how to perform a ion relaxation with ABACUS -of Si diamond structure. -''' -import shutil -from pathlib import Path # a more Pythonic alternative to the os.path -here = Path(__file__).parent -# to the directory where the pseudopotential and orbital files are stored -# In your case you change to the appropriate one -pporb = here.parent.parent.parent / 'tests' / 'PP_ORB' - -from ase.build import bulk -from ase.optimize import BFGS -from abacuslite import Abacus, AbacusProfile - -# AbacusProfile: the interface connecting the Abacus calculator instance -# with the file system and the enviroment -aprof = AbacusProfile( - command='mpirun -np 8 abacus', - pseudo_dir=pporb, - orbital_dir=pporb, - omp_num_threads=1, -) - -# Abacus: the calculator instance -jobdir = here / 'relax' -abacus = Abacus( - profile=aprof, - directory=str(jobdir), - pseudopotentials={'Si': 'Si_ONCV_PBE-1.0.upf'}, - basissets={'Si': 'Si_gga_8au_100Ry_2s2p1d.orb'}, - inp={ - 'calculation': 'scf', - 'nspin': 1, - 'basis_type': 'lcao', - 'ks_solver': 'genelpa', - 'ecutwfc': 100, - 'symmetry': 1, - 'kspacing': 0.1, - 'cal_force': 1 # let ABACUS calculate the forces - } -) - -# get the structure, can also from the -# ``` -# from ase.io import read -# atoms = read(...) -# ``` -atoms = bulk('Si', 'diamond', a=5.43) -# displacement the atoms a little bit -atoms.rattle(stdev=0.1) - -# bind the atoms with the abacus -atoms.calc = abacus - -# print the energy before relaxation -print(f'Before: {atoms.get_potential_energy()}') - -# perform the relaxation calculation -dyn = BFGS(atoms, logfile='-') # let's print to screen, observe the trajectory -dyn.run(fmax=0.05) - -# print the energy after relaxation -print(f'After: {atoms.get_potential_energy()}') - -# remove the temporary job directory (including all files inside) -shutil.rmtree(jobdir) diff --git a/interfaces/ASE_interface/examples/scf.py b/interfaces/ASE_interface/examples/scf.py deleted file mode 100644 index 9e3fdc4f228..00000000000 --- a/interfaces/ASE_interface/examples/scf.py +++ /dev/null @@ -1,56 +0,0 @@ -''' -This example shows how to run a SCF calculation with ABACUS -of Si diamond structure. -''' -import shutil -from pathlib import Path # a more Pythonic alternative to the os.path -here = Path(__file__).parent -# to the directory where the pseudopotential and orbital files are stored -# In your case you change to the appropriate one -pporb = here.parent.parent.parent / 'tests' / 'PP_ORB' - -from ase.build import bulk -from abacuslite import Abacus, AbacusProfile - -# AbacusProfile: the interface connecting the Abacus calculator instance -# with the file system and the enviroment -aprof = AbacusProfile( - command='mpirun -np 4 abacus', - pseudo_dir=pporb, - orbital_dir=pporb, - omp_num_threads=1, -) - -# Abacus: the calculator instance -jobdir = here / 'scf' -abacus = Abacus( - profile=aprof, - directory=str(jobdir), - pseudopotentials={'Si': 'Si_ONCV_PBE-1.0.upf'}, - basissets={'Si': 'Si_gga_8au_100Ry_2s2p1d.orb'}, - inp={ - 'calculation': 'scf', - 'nspin': 1, - 'basis_type': 'lcao', - 'ks_solver': 'genelpa', - 'ecutwfc': 100, - 'symmetry': 1, - 'kspacing': 0.1 - } -) - -# get the structure, can also from the -# ``` -# from ase.io import read -# atoms = read(...) -# ``` -atoms = bulk('Si', 'diamond', a=5.43) - -# bind the atoms with the abacus -atoms.calc = abacus - -# calculate! -print(atoms.get_potential_energy()) - -# remove the temporary job directory (including all files inside) -shutil.rmtree(jobdir) diff --git a/interfaces/ASE_interface/examples/soc.py b/interfaces/ASE_interface/examples/soc.py deleted file mode 100644 index cdbbd04d6e4..00000000000 --- a/interfaces/ASE_interface/examples/soc.py +++ /dev/null @@ -1,63 +0,0 @@ -'''this example shows how to perform the noncolinear -spin-orbit coupling calculation''' -import shutil -from pathlib import Path -here = Path(__file__).parent - -pporb = here.parent.parent.parent / 'tests' / 'PP_ORB' - -import numpy as np -from ase.atoms import Atoms -from abacuslite import Abacus, AbacusProfile - -'''SPECIAL: ase does not support the noncolinear spin yet -till 2026/3/24, see ase/outputs.py:L154-155, in which the -magmom cannot be set as the vector, so we release the -datatype of magmom and magmoms by ourself''' -from ase.outputs import _defineprop, all_outputs -del all_outputs['magmom'] -del all_outputs['magmoms'] -_defineprop('magmom', float, shape=3) # re-define the magmom can be set as the vector -_defineprop('magmoms', float, shape=('natoms', 3)) - -fe = Atoms(symbols=['Fe'] * 2, - scaled_positions=[[0., 0., 0.], [0.5, 0.5, 0.5]], - magmoms=np.array([[0, 0, 1], [0, 0, 1]]), - cell=np.eye(3) * 5.2, - pbc=True) - -aprof = AbacusProfile( - command='mpirun -np 2 abacus', - pseudo_dir=pporb, - orbital_dir=pporb, - omp_num_threads=1 -) - -jobdir = here / 'soc' -abacus = Abacus( - profile=aprof, - directory=jobdir, - pseudopotentials={'Fe': 'Fe.upf'}, - basissets={'Fe': 'Fe_gga_6au_100Ry_4s2p2d1f.orb'}, - inp={ - 'basis_type': 'lcao', - 'nspin': 4, - 'lspinorb': True, - 'noncolin': True, - 'out_mul': True, - 'scf_nmax': 10 # not serious setting! - }, - kpts={ - 'mode': 'mp-sampling', - 'gamma-centered': True, - 'nk': (2, 2, 2), - 'kshift': (0, 0, 0) - } -) - -fe.calc = abacus -print(f'Fe (non-colinear): {fe.get_potential_energy()} eV') -for ife, m in enumerate(fe.calc.results['magmoms']): - print(f'Fe{ife}: ({",".join([f"{mi:10.6f}" for mi in m])}) uB') - -shutil.rmtree(jobdir) \ No newline at end of file diff --git a/interfaces/ASE_interface/examples/socketio.py b/interfaces/ASE_interface/examples/socketio.py deleted file mode 100644 index e3087453ffc..00000000000 --- a/interfaces/ASE_interface/examples/socketio.py +++ /dev/null @@ -1,157 +0,0 @@ -""" -This example validates and benchmarks ABACUS socket I/O from ASE. - -ASE runs as the i-PI socket server and ABACUS runs as the socket client. -ABACUS keeps calculation=scf and enables socket_driver internally. - -The script checks two PR-review relevant points: -1. socket SCF gives the same energy and forces as a normal non-socket SCF; -2. repeated socket calculations avoid relaunching ABACUS and are faster than - the normal FileIO calculator for a sequence of SCF force evaluations. - -The i-PI protocol does not carry element symbols. AbacusSocketIO handles -the required STRU/socket atom-order alignment internally and returns forces -in the original ASE Atoms order. -""" -import os -import shutil -import time -from pathlib import Path - -import numpy as np -from ase import Atoms -from abacuslite import Abacus, AbacusProfile, AbacusSocketIO - -here = Path(__file__).parent -pporb = here.parent.parent.parent / 'tests' / 'PP_ORB' - -aprof = AbacusProfile( - command=os.environ.get('ABACUS_COMMAND', 'mpirun -np 4 abacus'), - pseudo_dir=pporb, - orbital_dir=pporb, - omp_num_threads=1, -) - -common_kwargs = { - 'pseudopotentials': {'Si': 'Si_ONCV_PBE-1.0.upf'}, - 'basissets': {'Si': 'Si_gga_8au_100Ry_2s2p1d.orb'}, - 'inp': { - 'calculation': 'scf', - 'nspin': 1, - 'basis_type': 'lcao', - 'ks_solver': 'scalapack_gvx', - 'ecutwfc': 30, - 'symmetry': 0, - 'kspacing': 0.5, - 'scf_thr': 1e-8, - 'scf_nmax': 40, - 'chg_extrap': 'atomic', - 'cal_force': 1, - }, -} - -base_atoms = Atoms( - 'Si2', - positions=[[0.0, 0.0, 0.0], [1.25, 1.25, 1.25]], - cell=[5.43, 5.43, 5.43], - pbc=True, -) - - -def clean(directory): - shutil.rmtree(directory, ignore_errors=True) - - -def run_fileio(atoms, directory): - clean(directory) - calc = Abacus(profile=aprof, directory=str(directory), **common_kwargs) - atoms = atoms.copy() - atoms.calc = calc - forces = atoms.get_forces() - energy = atoms.get_potential_energy() - return energy, forces - - -def run_socketio(atoms, directory, socket_name): - clean(directory) - calc = AbacusSocketIO( - profile=aprof, - directory=str(directory), - unixsocket=socket_name, - timeout=120, - **common_kwargs, - ) - atoms = atoms.copy() - with calc: - atoms.calc = calc - energy = atoms.get_potential_energy() - forces = atoms.get_forces() - return energy, forces - - -def displaced_structures(): - structures = [] - for scale in (0.00, 0.03, -0.02, 0.05): - atoms = base_atoms.copy() - atoms.positions[1] += scale - structures.append(atoms) - return structures - - -fileio_dir = here / 'socketio_fileio_scf' -socket_dir = here / 'socketio_socket_scf' -bench_fileio_dir = here / 'socketio_bench_fileio' -bench_socket_dir = here / 'socketio_bench_socket' - -try: - reference_energy, reference_forces = run_fileio(base_atoms, fileio_dir) - socket_energy, socket_forces = run_socketio( - base_atoms, socket_dir, 'abacus_si_check') - - energy_diff = abs(socket_energy - reference_energy) - force_diff = np.max(np.abs(socket_forces - reference_forces)) - print(f'FileIO SCF energy: {reference_energy:.12f} eV') - print(f'Socket SCF energy: {socket_energy:.12f} eV') - print(f'|dE|: {energy_diff:.3e} eV') - print(f'max |dF|: {force_diff:.3e} eV/Angstrom') - assert energy_diff < 1e-4 - assert force_diff < 1e-5 - - structures = displaced_structures() - - clean(bench_fileio_dir) - fileio_calc = Abacus( - profile=aprof, - directory=str(bench_fileio_dir), - **common_kwargs, - ) - t0 = time.perf_counter() - for atoms in structures: - atoms = atoms.copy() - atoms.calc = fileio_calc - atoms.get_forces() - fileio_seconds = time.perf_counter() - t0 - - clean(bench_socket_dir) - socket_calc = AbacusSocketIO( - profile=aprof, - directory=str(bench_socket_dir), - unixsocket='abacus_si_bench', - timeout=120, - **common_kwargs, - ) - t0 = time.perf_counter() - with socket_calc as calc: - for atoms in structures: - atoms = atoms.copy() - atoms.calc = calc - atoms.get_forces() - socket_seconds = time.perf_counter() - t0 - - speedup = fileio_seconds / socket_seconds - print(f'FileIO repeated SCF force time: {fileio_seconds:.2f} s') - print(f'Socket repeated SCF force time: {socket_seconds:.2f} s') - print(f'Socket speedup vs FileIO: {speedup:.2f}x') -finally: - for directory in (fileio_dir, socket_dir, bench_fileio_dir, bench_socket_dir): - clean(directory) diff --git a/interfaces/ASE_interface/pyproject.toml b/interfaces/ASE_interface/pyproject.toml deleted file mode 100644 index 8f84373e0af..00000000000 --- a/interfaces/ASE_interface/pyproject.toml +++ /dev/null @@ -1,24 +0,0 @@ -[build-system] -requires = ["setuptools", "wheel"] -build-backend = "setuptools.build_meta" - -[project] -name = "abacuslite" -version = "1.0.0" -description = "An ABACUS light-weighted plugin implementation of ase calculator" -readme = "README.md" -authors = [ - { name = "Yuyang Ji" }, - { name = "Zhenxiong Shen" }, - { name = "Yike Huang" }, - { name = "Zhaoqing Liu" } -] -requires-python = ">=3.10" -dependencies = [ - "ase>=3.22.0", - "seekpath" -] - -[tool.setuptools.packages.find] -where = ["."] -include = ["abacuslite*"] \ No newline at end of file diff --git a/interfaces/ASE_interface/abacuslite/io/testfiles/nspin4-gamma-mddump b/interfaces/ASE_interface/tests/fixtures/native_md/MD_dump similarity index 100% rename from interfaces/ASE_interface/abacuslite/io/testfiles/nspin4-gamma-mddump rename to interfaces/ASE_interface/tests/fixtures/native_md/MD_dump diff --git a/interfaces/ASE_interface/abacuslite/io/testfiles/nspin4-gamma-eigocc b/interfaces/ASE_interface/tests/fixtures/native_md/eig_occ.txt similarity index 100% rename from interfaces/ASE_interface/abacuslite/io/testfiles/nspin4-gamma-eigocc rename to interfaces/ASE_interface/tests/fixtures/native_md/eig_occ.txt diff --git a/interfaces/ASE_interface/abacuslite/io/testfiles/pw-symm0-nspin4-gamma-md b/interfaces/ASE_interface/tests/fixtures/native_md/running_md.log.txt similarity index 100% rename from interfaces/ASE_interface/abacuslite/io/testfiles/pw-symm0-nspin4-gamma-md rename to interfaces/ASE_interface/tests/fixtures/native_md/running_md.log.txt diff --git a/interfaces/ASE_interface/tests/test_external_provenance.py b/interfaces/ASE_interface/tests/test_external_provenance.py index 3373bc5c37c..28d0237da4e 100644 --- a/interfaces/ASE_interface/tests/test_external_provenance.py +++ b/interfaces/ASE_interface/tests/test_external_provenance.py @@ -27,7 +27,7 @@ def test_source_shadow_is_rejected(self): shadow.touch() installed.touch() with self.assertRaisesRegex(AssertionError, 'source shadow'): - verify_locations(shadow, installed, source) + verify_locations(shadow, installed, root / 'checkout') def test_wrong_distribution_is_rejected(self): with tempfile.TemporaryDirectory() as root: diff --git a/interfaces/ASE_interface/tests/test_native_md_fixture.py b/interfaces/ASE_interface/tests/test_native_md_fixture.py index d665548d9ea..1058476e11b 100644 --- a/interfaces/ASE_interface/tests/test_native_md_fixture.py +++ b/interfaces/ASE_interface/tests/test_native_md_fixture.py @@ -11,15 +11,15 @@ class NativeMDOutputTest(unittest.TestCase): def setUp(self): - # Reuse ABACUS' existing two-frame raw output fixture as data only. - source = Path(__file__).resolve().parents[1] / 'abacuslite' / 'io' / 'testfiles' + # Engine-owned copy of a two-frame raw output fixture. + source = Path(__file__).resolve().parent / 'fixtures' / 'native_md' temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) self.fixture = Path(temporary.name) for name, target in ( - ('pw-symm0-nspin4-gamma-md', 'running_md.log'), - ('nspin4-gamma-eigocc', 'eig_occ.txt'), - ('nspin4-gamma-mddump', 'MD_dump'), + ('running_md.log.txt', 'running_md.log'), + ('eig_occ.txt', 'eig_occ.txt'), + ('MD_dump', 'MD_dump'), ): shutil.copyfile(source / name, self.fixture / target) diff --git a/interfaces/ASE_interface/tests/verify_external.py b/interfaces/ASE_interface/tests/verify_external.py index abd128ef4e4..7f05dbccedc 100644 --- a/interfaces/ASE_interface/tests/verify_external.py +++ b/interfaces/ASE_interface/tests/verify_external.py @@ -1,24 +1,23 @@ -"""Prove the candidate lane imports the installed wheel, not a checkout.""" +"""Prove the harness imports the installed wheel, not a checkout.""" import argparse from importlib import metadata from pathlib import Path -def verify_locations(imported, installed, bundled): +def verify_locations(imported, installed, checkout): imported = Path(imported).resolve() installed = Path(installed).resolve() - bundled = Path(bundled).resolve() - if imported.is_relative_to(bundled): - raise AssertionError(f'source shadow: {imported} is under {bundled}') + checkout = Path(checkout).resolve() + if imported.is_relative_to(checkout): + raise AssertionError(f'source shadow: {imported} is under {checkout}') if imported != installed: raise AssertionError(f'import is not from installed distribution: {imported} != {installed}') def main(): parser = argparse.ArgumentParser() - parser.add_argument('--bundled-root', type=Path, required=True) - parser.add_argument('--candidate-root', type=Path, required=True) + parser.add_argument('--checkout-root', type=Path, required=True) args = parser.parse_args() import abacuslite @@ -26,9 +25,7 @@ def main(): distribution = metadata.distribution('abacuslite') installed = distribution.locate_file('abacuslite/__init__.py') imported = Path(abacuslite.__file__) - verify_locations(imported, installed, args.bundled_root) - if imported.resolve().is_relative_to(args.candidate_root.resolve()): - raise AssertionError(f'candidate source shadow: {imported}') + verify_locations(imported, installed, args.checkout_root) if not installed.is_file(): raise AssertionError(f'installed distribution lacks abacuslite package: {installed}') print(f'abacuslite distribution: {distribution.metadata["Name"]} {distribution.version}') diff --git a/interfaces/README b/interfaces/README index 4776efad34b..13fd9c46d93 100644 --- a/interfaces/README +++ b/interfaces/README @@ -14,6 +14,11 @@ simulations. The interfaces here are designed to: preprocessing -> ABACUS calculation -> post-analysis) - Standardize integration methods to ensure reproducibility and ease of use +The ASE calculator is maintained externally as the +[`abacuslite`](https://github.com/QuantumMisaka/abacuslite) package. +`ASE_interface/` in this repository contains only the engine-side +compatibility harness used by ABACUS CI. + ## Usage Guidelines - For each software interface, refer to the dedicated README file in the diff --git a/source/source_relax/socket_utils.cpp b/source/source_relax/socket_utils.cpp index ccdfa38b369..090bfd0f830 100644 --- a/source/source_relax/socket_utils.cpp +++ b/source/source_relax/socket_utils.cpp @@ -73,7 +73,7 @@ void throw_if_any_rank_failed(int local_failed, std::string local_message) std::string properties_extra(const ComputedFrame& frame) { std::ostringstream extra; - extra << "{\"schema\":\"abacus.socket.properties.v1\",\"present\":[\"energy\""; + extra << "{\"schema\":\"" << kPropertiesSchema << "\",\"present\":[\"energy\""; if (frame.forces_present) { extra << ",\"forces\""; diff --git a/source/source_relax/socket_utils.h b/source/source_relax/socket_utils.h index ccac9761967..42b82730fa3 100644 --- a/source/source_relax/socket_utils.h +++ b/source/source_relax/socket_utils.h @@ -22,6 +22,7 @@ constexpr double kInverseRelativeTolerance = 64.0; constexpr double kStressAbsoluteTolerance = 1.0e-10; constexpr double kStressRelativeTolerance = 1.0e-8; constexpr std::int32_t kMaxInitBytes = static_cast(1048576); +constexpr const char* kPropertiesSchema = "abacus.socket.properties.v1"; enum class DriverState { diff --git a/source/source_relax/unittests/test_socket_driver.cpp b/source/source_relax/unittests/test_socket_driver.cpp index 95b338cd388..05e7a5feb35 100644 --- a/source/source_relax/unittests/test_socket_driver.cpp +++ b/source/source_relax/unittests/test_socket_driver.cpp @@ -1,4 +1,5 @@ #include "source_relax/socket_driver.h" +#include "source_relax/socket_utils.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -488,6 +489,17 @@ TEST(SocketDriverTest, NonconvergedFrameIsPublishedWithMetadata) EXPECT_THAT(response.extra, testing::HasSubstr("\"scf_converged\":false")); } +TEST(SocketDriverTest, PropertiesMetadataDeclaresVersionedSchema) +{ + SocketUtils::ComputedFrame frame; + const std::string extra = SocketUtils::properties_extra(frame); + + // A breaking schema change must introduce a new version in both ABACUS and + // abacuslite; the adapter rejects unknown schemas. + EXPECT_THAT(extra, testing::HasSubstr( + "\"schema\":\"abacus.socket.properties.v1\"")); +} + TEST(SocketDriverTest, EnergyOnlyFrameMarksForceAndStressAbsent) { ForceResponse response; From 6b37c6b1c5e168540540f11f80974298cc64ff42 Mon Sep 17 00:00:00 2001 From: QuantumMisaka Date: Mon, 19 Oct 2026 00:47:08 +0800 Subject: [PATCH 10/12] docs(ase): use GitHub link for engine harness --- docs/advanced/interface/ase.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/advanced/interface/ase.md b/docs/advanced/interface/ase.md index 0755baf7e57..b79a63a22d6 100644 --- a/docs/advanced/interface/ase.md +++ b/docs/advanced/interface/ase.md @@ -27,7 +27,8 @@ python -m pip install abacuslite `abacuslite` owns the Python calculator, ABACUS input/output readers and user examples. The ABACUS repository keeps only the engine-side compatibility -harness in [`interfaces/ASE_interface/tests`](../../../interfaces/ASE_interface/README.md). +harness in +[`interfaces/ASE_interface/tests`](https://github.com/deepmodeling/abacus-develop/tree/develop/interfaces/ASE_interface/tests). ABACUS CI installs the published plugin, or a reviewed candidate ref, and runs that harness against the executable built from the current source. Manual CI dispatch can select the candidate ref. From 5598a2ffa47f9e87fc71728c9974282101ae24d9 Mon Sep 17 00:00:00 2001 From: QuantumMisaka Date: Mon, 19 Oct 2026 13:02:45 +0800 Subject: [PATCH 11/12] docs(ase): separate ASE-driven MD from ABACUS native MD The retained paragraph claimed that starting MD from Python is unsupported, but the engine harness now requires native_md.py (engine runs its own MD, plugin reads the trajectory) alongside the ASE-driven Langevin case. State both validated paths and keep calculation types outside the plugin surface out of scope. --- docs/advanced/interface/ase.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/advanced/interface/ase.md b/docs/advanced/interface/ase.md index b79a63a22d6..66a326eebee 100644 --- a/docs/advanced/interface/ase.md +++ b/docs/advanced/interface/ase.md @@ -113,7 +113,17 @@ with tempfile.TemporaryDirectory() as tmpdir: ## Perform Calculations -In the new implementation, we limit the range of functionalties supported to mainly include the necessary ones, such as the SCF calculation, the energy and force/stress evaluation. The other features, such as starting the molecule dynamics directly in ABACUS from Python, is not supported anymore. Instead, it is encouraged to use the ASE tools to perform the molecule dynamics. +In the new implementation, the supported surface is the one the external plugin +exposes: SCF calculations, energy and force/stress evaluation, ASE-driven +relaxation and molecular dynamics through the ASE optimizers and integrators, +and ABACUS native MD, in which the engine runs its own MD integrator and the +plugin reads the resulting trajectory. Both molecular-dynamics paths are +validated rather than assumed: the engine-side harness runs the ASE-driven +Langevin case (`md.py`) and the native-MD case (`native_md.py`) as required +compatibility cases in both the develop and the legacy output-format lanes. +Calculation types the plugin does not expose remain outside the supported +surface, and for simulations whose integrator you control the ASE tools remain +the recommended path. See the [abacuslite examples](https://github.com/QuantumMisaka/abacuslite/tree/main/examples) for complete scripts. From 1bf602a0b75800087a269e4f2935538093e699a1 Mon Sep 17 00:00:00 2001 From: QuantumMisaka Date: Mon, 19 Oct 2026 13:02:46 +0800 Subject: [PATCH 12/12] fix(ase): reclaim isolated process group after a failed leader exit capture() only signalled the isolated group on timeout. When a case exited nonzero while a descendant stayed in the group, the runner returned and later cases could start beside the leftover. Check the group after any exit, escalate SIGTERM to SIGKILL, and reap it. Add a regression where the leader exits nonzero before a SIGTERM-ignoring child. --- .../tests/run_engine_compatibility.py | 43 ++++++++++++---- .../tests/test_portable_runner.py | 50 +++++++++++++++++++ 2 files changed, 84 insertions(+), 9 deletions(-) diff --git a/interfaces/ASE_interface/tests/run_engine_compatibility.py b/interfaces/ASE_interface/tests/run_engine_compatibility.py index 4e707dc200d..cc948143f8e 100644 --- a/interfaces/ASE_interface/tests/run_engine_compatibility.py +++ b/interfaces/ASE_interface/tests/run_engine_compatibility.py @@ -69,6 +69,26 @@ def validate_cases(fmt, results): raise ValueError(f"required case failed: {item['script']}: {item}") +def group_members_alive(pid): + """Report whether the isolated process group still has live members.""" + try: + os.killpg(pid, 0) + except ProcessLookupError: + return False + except PermissionError: + return True + return True + + +def signal_group(pid, sig): + """Signal the whole isolated group; missing groups are not an error.""" + try: + os.killpg(pid, sig) + except ProcessLookupError: + return False + return True + + def capture(command, *, cwd, env, log, timeout): start = time.monotonic() timed_out = False @@ -79,21 +99,26 @@ def capture(command, *, cwd, env, log, timeout): process.wait(timeout=timeout) except subprocess.TimeoutExpired: timed_out = True - try: - os.killpg(process.pid, signal.SIGTERM) - except ProcessLookupError: - pass + signal_group(process.pid, signal.SIGTERM) try: process.wait(timeout=10) except subprocess.TimeoutExpired: pass - # The leader can exit on SIGTERM while a child ignores it. The - # process group remains alive, so always finish the timeout with - # SIGKILL rather than treating leader exit as group shutdown. + # The leader can exit normally, with a nonzero status, or on SIGTERM + # while a descendant still runs in the isolated group. Reclaim the + # whole group whenever members remain, not only when the leader timed + # out; otherwise the next case would start alongside the leftover. + if group_members_alive(process.pid): + signal_group(process.pid, signal.SIGTERM) try: - os.killpg(process.pid, signal.SIGKILL) - except ProcessLookupError: + process.wait(timeout=10) + except subprocess.TimeoutExpired: pass + # A child that ignores SIGTERM keeps the group alive after the + # leader is gone, so finish with SIGKILL instead of treating + # leader exit as group shutdown. + if group_members_alive(process.pid): + signal_group(process.pid, signal.SIGKILL) process.wait(timeout=10) return {'returncode': process.returncode, 'timed_out': timed_out, 'seconds': round(time.monotonic() - start, 3)} diff --git a/interfaces/ASE_interface/tests/test_portable_runner.py b/interfaces/ASE_interface/tests/test_portable_runner.py index a06c84fd72d..64d785975cb 100644 --- a/interfaces/ASE_interface/tests/test_portable_runner.py +++ b/interfaces/ASE_interface/tests/test_portable_runner.py @@ -115,6 +115,56 @@ def test_timeout_kills_sigterm_ignoring_child_after_leader_exits(self): except ProcessLookupError: pass + def test_nonzero_leader_exit_reclaims_remaining_child(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + pid_file = root / 'child.pid' + child_code = ('import os,signal,sys,time; ' + 'signal.signal(signal.SIGTERM, signal.SIG_IGN); ' + 'open(sys.argv[1], "w").write(str(os.getpid())); ' + 'time.sleep(60)') + leader_code = ('import os,sys,time,subprocess; ' + 'subprocess.Popen([sys.executable, "-c", ' + 'sys.argv[1], sys.argv[2]]); ' + 'deadline=time.time()+10\n' + 'while not os.path.exists(sys.argv[2]) and ' + 'time.time()