From 226d4cd8cf5c95e117f3bf0625a016eab68db006 Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Fri, 28 Nov 2025 16:39:49 +0100 Subject: [PATCH 01/13] WIP: Simple ase interface It works, but it cannot be configured yet. --- sphinx_parser/calculator.py | 41 +++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 sphinx_parser/calculator.py diff --git a/sphinx_parser/calculator.py b/sphinx_parser/calculator.py new file mode 100644 index 0000000..a036d23 --- /dev/null +++ b/sphinx_parser/calculator.py @@ -0,0 +1,41 @@ +from pathlib import Path +from sphinx_parser.output import SphinxLogParser +from ase.calculators.calculator import FileIOCalculator, StandardProfile, FileIORules + +from sphinx_parser.input import sphinx +from sphinx_parser.ase import get_structure_group +from sphinx_parser.toolkit import to_sphinx +from sphinx_parser.potential import get_paw_from_structure + + +class SphinxDft(FileIOCalculator): + implemented_properties = ['energy', 'forces'] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs, profile=StandardProfile(command="sphinx")) + self.fileio_rules = FileIORules(stdout_name='log.sx') + + def write_input(self, atoms, properties=None, system_changes=None): + super().write_input(atoms, properties, system_changes) + + struct_group, spin_lst = get_structure_group(atoms) + main_group = sphinx.main(scfDiag=sphinx.main.scfDiag(maxSteps=10, blockCCG={}), evalForces=sphinx.main.evalForces('forces.txt')) + pawPot_group = get_paw_from_structure(atoms) + basis_group = sphinx.basis(eCut=25, kPoint=sphinx.basis.kPoint(coords=3 * [0.5])) + paw_group = sphinx.PAWHamiltonian(xc=1, spinPolarized=False, ekt=0.2) + initial_guess_group = sphinx.initialGuess( + waves=sphinx.initialGuess.waves(lcao=sphinx.initialGuess.waves.lcao()), rho=sphinx.initialGuess.rho(atomicOrbitals=True) + ) + + input_sx = sphinx( + pawPot=pawPot_group, structure=struct_group, main=main_group, basis=basis_group, PAWHamiltonian=paw_group, initialGuess=initial_guess_group + ) + + cwd = self.directory + with open(Path(cwd) / "input.sx", "w") as f: + f.write(to_sphinx(input_sx)) + + def read_results(self): + parser = SphinxLogParser.load_from_path(Path(self.directory) / "log.sx") + self.results['energy'] = parser.get_energy_free()[-1][-1] + self.results['forces'] = parser.get_forces()[-1] From d7895440cbbe6f2fcb99b86800b240d4df48dbf2 Mon Sep 17 00:00:00 2001 From: Sam Waseda Date: Sun, 30 Nov 2025 20:46:13 +0100 Subject: [PATCH 02/13] black etc --- sphinx_parser/calculator.py | 43 +++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/sphinx_parser/calculator.py b/sphinx_parser/calculator.py index a036d23..29b4a21 100644 --- a/sphinx_parser/calculator.py +++ b/sphinx_parser/calculator.py @@ -1,34 +1,45 @@ from pathlib import Path -from sphinx_parser.output import SphinxLogParser -from ase.calculators.calculator import FileIOCalculator, StandardProfile, FileIORules +from ase.calculators.calculator import FileIOCalculator, FileIORules, StandardProfile -from sphinx_parser.input import sphinx from sphinx_parser.ase import get_structure_group -from sphinx_parser.toolkit import to_sphinx +from sphinx_parser.input import sphinx +from sphinx_parser.output import SphinxLogParser from sphinx_parser.potential import get_paw_from_structure +from sphinx_parser.toolkit import to_sphinx class SphinxDft(FileIOCalculator): - implemented_properties = ['energy', 'forces'] - + implemented_properties = ["energy", "forces"] + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs, profile=StandardProfile(command="sphinx")) - self.fileio_rules = FileIORules(stdout_name='log.sx') + self.fileio_rules = FileIORules(stdout_name="log.sx") def write_input(self, atoms, properties=None, system_changes=None): super().write_input(atoms, properties, system_changes) - - struct_group, spin_lst = get_structure_group(atoms) - main_group = sphinx.main(scfDiag=sphinx.main.scfDiag(maxSteps=10, blockCCG={}), evalForces=sphinx.main.evalForces('forces.txt')) + + struct_group = get_structure_group(atoms)[0] + main_group = sphinx.main( + scfDiag=sphinx.main.scfDiag(maxSteps=10, blockCCG={}), + evalForces=sphinx.main.evalForces("forces.txt"), + ) pawPot_group = get_paw_from_structure(atoms) - basis_group = sphinx.basis(eCut=25, kPoint=sphinx.basis.kPoint(coords=3 * [0.5])) + basis_group = sphinx.basis( + eCut=25, kPoint=sphinx.basis.kPoint(coords=3 * [0.5]) + ) paw_group = sphinx.PAWHamiltonian(xc=1, spinPolarized=False, ekt=0.2) initial_guess_group = sphinx.initialGuess( - waves=sphinx.initialGuess.waves(lcao=sphinx.initialGuess.waves.lcao()), rho=sphinx.initialGuess.rho(atomicOrbitals=True) + waves=sphinx.initialGuess.waves(lcao=sphinx.initialGuess.waves.lcao()), + rho=sphinx.initialGuess.rho(atomicOrbitals=True), ) - + input_sx = sphinx( - pawPot=pawPot_group, structure=struct_group, main=main_group, basis=basis_group, PAWHamiltonian=paw_group, initialGuess=initial_guess_group + pawPot=pawPot_group, + structure=struct_group, + main=main_group, + basis=basis_group, + PAWHamiltonian=paw_group, + initialGuess=initial_guess_group, ) cwd = self.directory @@ -37,5 +48,5 @@ def write_input(self, atoms, properties=None, system_changes=None): def read_results(self): parser = SphinxLogParser.load_from_path(Path(self.directory) / "log.sx") - self.results['energy'] = parser.get_energy_free()[-1][-1] - self.results['forces'] = parser.get_forces()[-1] + self.results["energy"] = parser.get_energy_free()[-1][-1] + self.results["forces"] = parser.get_forces()[-1] From 153bf4a951ed08e13129d9ad3671b46959204d53 Mon Sep 17 00:00:00 2001 From: Sam Waseda Date: Sun, 30 Nov 2025 20:49:35 +0100 Subject: [PATCH 03/13] ruff --- sphinx_parser/calculator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sphinx_parser/calculator.py b/sphinx_parser/calculator.py index 29b4a21..67d036a 100644 --- a/sphinx_parser/calculator.py +++ b/sphinx_parser/calculator.py @@ -1,4 +1,5 @@ from pathlib import Path + from ase.calculators.calculator import FileIOCalculator, FileIORules, StandardProfile from sphinx_parser.ase import get_structure_group From 92e8fe9cbe6505f571ab516ad02664be35793291 Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Thu, 19 Mar 2026 16:01:18 +0000 Subject: [PATCH 04/13] Use usual filename Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- sphinx_parser/calculator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx_parser/calculator.py b/sphinx_parser/calculator.py index 67d036a..4cc6bb9 100644 --- a/sphinx_parser/calculator.py +++ b/sphinx_parser/calculator.py @@ -14,7 +14,7 @@ class SphinxDft(FileIOCalculator): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs, profile=StandardProfile(command="sphinx")) - self.fileio_rules = FileIORules(stdout_name="log.sx") + self.fileio_rules = FileIORules(stdout_name="sphinx.log") def write_input(self, atoms, properties=None, system_changes=None): super().write_input(atoms, properties, system_changes) @@ -48,6 +48,6 @@ def write_input(self, atoms, properties=None, system_changes=None): f.write(to_sphinx(input_sx)) def read_results(self): - parser = SphinxLogParser.load_from_path(Path(self.directory) / "log.sx") + parser = SphinxLogParser.load_from_path(Path(self.directory) / "sphinx.log") self.results["energy"] = parser.get_energy_free()[-1][-1] self.results["forces"] = parser.get_forces()[-1] From 991b689b18b2b2b34f72beb5438c88077ee6b11b Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Thu, 19 Mar 2026 16:06:46 +0000 Subject: [PATCH 05/13] Simplify input writing Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- sphinx_parser/calculator.py | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/sphinx_parser/calculator.py b/sphinx_parser/calculator.py index 4cc6bb9..8ea03c7 100644 --- a/sphinx_parser/calculator.py +++ b/sphinx_parser/calculator.py @@ -4,6 +4,7 @@ from sphinx_parser.ase import get_structure_group from sphinx_parser.input import sphinx +from sphinx_parser.jobs import set_base_parameters from sphinx_parser.output import SphinxLogParser from sphinx_parser.potential import get_paw_from_structure from sphinx_parser.toolkit import to_sphinx @@ -19,29 +20,9 @@ def __init__(self, *args, **kwargs): def write_input(self, atoms, properties=None, system_changes=None): super().write_input(atoms, properties, system_changes) - struct_group = get_structure_group(atoms)[0] - main_group = sphinx.main( - scfDiag=sphinx.main.scfDiag(maxSteps=10, blockCCG={}), - evalForces=sphinx.main.evalForces("forces.txt"), - ) - pawPot_group = get_paw_from_structure(atoms) - basis_group = sphinx.basis( - eCut=25, kPoint=sphinx.basis.kPoint(coords=3 * [0.5]) - ) - paw_group = sphinx.PAWHamiltonian(xc=1, spinPolarized=False, ekt=0.2) - initial_guess_group = sphinx.initialGuess( - waves=sphinx.initialGuess.waves(lcao=sphinx.initialGuess.waves.lcao()), - rho=sphinx.initialGuess.rho(atomicOrbitals=True), - ) - - input_sx = sphinx( - pawPot=pawPot_group, - structure=struct_group, - main=main_group, - basis=basis_group, - PAWHamiltonian=paw_group, - initialGuess=initial_guess_group, - ) + # Reuse the shared helper to build the Sphinx input structure, + # avoiding divergence from the defaults defined in jobs.set_base_parameters. + input_sx = set_base_parameters(atoms) cwd = self.directory with open(Path(cwd) / "input.sx", "w") as f: From f80d900ef76a25eb9b99a786a0b3552804f6fc07 Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Thu, 19 Mar 2026 16:09:45 +0000 Subject: [PATCH 06/13] Convert units Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Marvin Poul --- sphinx_parser/calculator.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sphinx_parser/calculator.py b/sphinx_parser/calculator.py index 8ea03c7..3af3411 100644 --- a/sphinx_parser/calculator.py +++ b/sphinx_parser/calculator.py @@ -1,6 +1,7 @@ from pathlib import Path from ase.calculators.calculator import FileIOCalculator, FileIORules, StandardProfile +from ase.units import Hartree, Bohr from sphinx_parser.ase import get_structure_group from sphinx_parser.input import sphinx @@ -30,5 +31,6 @@ def write_input(self, atoms, properties=None, system_changes=None): def read_results(self): parser = SphinxLogParser.load_from_path(Path(self.directory) / "sphinx.log") - self.results["energy"] = parser.get_energy_free()[-1][-1] - self.results["forces"] = parser.get_forces()[-1] + self.results["energy"] = parser.get_energy_free()[-1][-1] * Hartree + forces_au = parser.get_forces()[-1] + self.results["forces"] = forces_au * Hartree / Bohr From eb838a338c51704fc6db4080827f0461043e4301 Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Mon, 20 Apr 2026 19:56:27 +0000 Subject: [PATCH 07/13] Make executable configurable --- sphinx_parser/calculator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx_parser/calculator.py b/sphinx_parser/calculator.py index 3af3411..b08e45d 100644 --- a/sphinx_parser/calculator.py +++ b/sphinx_parser/calculator.py @@ -14,8 +14,8 @@ class SphinxDft(FileIOCalculator): implemented_properties = ["energy", "forces"] - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs, profile=StandardProfile(command="sphinx")) + def __init__(self, *args, command: str = "sphinx", **kwargs): + super().__init__(*args, **kwargs, profile=StandardProfile(command=command)) self.fileio_rules = FileIORules(stdout_name="sphinx.log") def write_input(self, atoms, properties=None, system_changes=None): From 38f9a9672e770bf9bc70695905e797c3b5b2c6b2 Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Mon, 20 Apr 2026 16:01:01 -0400 Subject: [PATCH 08/13] Add injectable potentials to allow per-element PAW path and potType override Adds an optional `potentials: dict[str, {potential, potType}]` parameter to `get_paw_from_structure`, `get_paw_from_chemical_symbols`, `set_base_parameters`, and `SphinxDft.__init__`. Elements absent from the dict fall back to the existing `get_potential_path` / AtomPAW default. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 89 ++++++++++++++++++++++++++++++++++++ sphinx_parser/calculator.py | 8 ++-- sphinx_parser/jobs.py | 3 +- sphinx_parser/potential.py | 31 ++++++++----- tests/unit/test_jobs.py | 12 +++++ tests/unit/test_potential.py | 53 +++++++++++++++++++++ 6 files changed, 178 insertions(+), 18 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d438567 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,89 @@ +# sphinx_parser + +Python interface for the [Sphinx DFT code](https://sxrepo.mpie.de) — a high-performance density functional theory (DFT) package developed at the Max Planck Institute. + +## What this project does + +- Provides a Python API to construct Sphinx input files programmatically +- Integrates with ASE (Atomic Simulation Environment) as a standard `FileIOCalculator` +- Auto-generates input classes from a YAML specification (`sphinx_parser/src/input_data.yml`) +- Parses Sphinx output files (logs, energies, forces) +- Handles unit conversions between Hartree/Bohr (Sphinx) and eV/Å (ASE) + +## Data flow + +``` +input_data.yml → generator.py → input.py (generated, do not hand-edit) + ↓ + jobs.py (convenience wrappers) + ↓ + toolkit.py (serialisation to .sx format) + ↓ + calculator.py or manual file write + ↓ + input.sx → [Sphinx binary] + ↓ + sphinx.log / energy.dat / forces.sx + ↓ + output.py (SphinxLogParser) +``` + +## Key files + +| File | Role | +|------|------| +| `sphinx_parser/input.py` | Auto-generated input classes — **do not edit by hand** | +| `sphinx_parser/src/input_data.yml` | Source-of-truth YAML spec for all Sphinx input parameters | +| `sphinx_parser/src/generator.py` | Reads the YAML and regenerates `input.py` | +| `sphinx_parser/calculator.py` | ASE `FileIOCalculator` subclass (`SphinxDft`) | +| `sphinx_parser/jobs.py` | High-level helpers (`set_base_parameters`, `apply_minimization`) | +| `sphinx_parser/toolkit.py` | Low-level formatting (`to_sphinx`, `format_value`, `fill_values`) | +| `sphinx_parser/ase.py` | ASE ↔ Sphinx structure conversion | +| `sphinx_parser/output.py` | `SphinxLogParser` — parses log files into result dicts | +| `sphinx_parser/potential.py` | PAW potential lookup (VASP and JTH formats) | + +## Regenerating input.py + +```bash +python sphinx_parser/src/generator.py +``` + +Run this after editing `input_data.yml`. + +## Tests + +```bash +python -m pytest tests/ +``` + +- `tests/unit/` — unit tests for jobs, output parsing, generator +- `tests/integration/` — integration tests including README examples +- `tests/static/` — static reference data + +CI runs on Python 3.11–3.14 via GitHub Actions. + +## Code style + +```bash +black sphinx_parser/ +ruff check sphinx_parser/ +``` + +Both are enforced in CI. + +## Dependencies + +- **numpy**, **ase** — core scientific stack +- **semantikon**, **pint** — unit handling +- **h5py** — HDF5 I/O +- **pyyaml** — YAML parsing + +## Active branch: `calculator` + +This branch adds `calculator.py` — the new ASE `FileIOCalculator` interface. It is not yet on `main`. + +## Notes + +- Units inside Sphinx are Hartree / Bohr; the calculator layer converts to eV / Å for ASE. +- Atom ordering in Sphinx input must group species together; `sphinx_parser/ase.py` handles this reordering. +- Magnetic moments and constraints are also handled in `ase.py`. diff --git a/sphinx_parser/calculator.py b/sphinx_parser/calculator.py index b08e45d..b3d7593 100644 --- a/sphinx_parser/calculator.py +++ b/sphinx_parser/calculator.py @@ -3,27 +3,25 @@ from ase.calculators.calculator import FileIOCalculator, FileIORules, StandardProfile from ase.units import Hartree, Bohr -from sphinx_parser.ase import get_structure_group -from sphinx_parser.input import sphinx from sphinx_parser.jobs import set_base_parameters from sphinx_parser.output import SphinxLogParser -from sphinx_parser.potential import get_paw_from_structure from sphinx_parser.toolkit import to_sphinx class SphinxDft(FileIOCalculator): implemented_properties = ["energy", "forces"] - def __init__(self, *args, command: str = "sphinx", **kwargs): + def __init__(self, *args, command: str = "sphinx", potentials=None, **kwargs): super().__init__(*args, **kwargs, profile=StandardProfile(command=command)) self.fileio_rules = FileIORules(stdout_name="sphinx.log") + self.potentials = potentials def write_input(self, atoms, properties=None, system_changes=None): super().write_input(atoms, properties, system_changes) # Reuse the shared helper to build the Sphinx input structure, # avoiding divergence from the defaults defined in jobs.set_base_parameters. - input_sx = set_base_parameters(atoms) + input_sx = set_base_parameters(atoms, potentials=self.potentials) cwd = self.directory with open(Path(cwd) / "input.sx", "w") as f: diff --git a/sphinx_parser/jobs.py b/sphinx_parser/jobs.py index 6758f39..19d4d42 100644 --- a/sphinx_parser/jobs.py +++ b/sphinx_parser/jobs.py @@ -10,6 +10,7 @@ def set_base_parameters( maxSteps: int = 30, ekt: float = 0.2, k_point_coords: list = [0.5, 0.5, 0.5], + potentials=None, ): """ Set the base parameters for the sphinx input file @@ -32,7 +33,7 @@ def set_base_parameters( maxSteps=maxSteps, blockCCG=sphinx.main.scfDiag.blockCCG() ) ) - pawPot_group = get_paw_from_structure(structure) + pawPot_group = get_paw_from_structure(structure, potentials) basis_group = sphinx.basis( eCut=eCut, kPoint=sphinx.basis.kPoint(coords=k_point_coords) ) diff --git a/sphinx_parser/potential.py b/sphinx_parser/potential.py index 6fd2b48..3c25458 100644 --- a/sphinx_parser/potential.py +++ b/sphinx_parser/potential.py @@ -7,20 +7,27 @@ from sphinx_parser.input import sphinx -def get_paw_from_structure(structure): - return get_paw_from_chemical_symbols(structure.get_chemical_symbols()) - +def get_paw_from_structure(structure, potentials=None): + return get_paw_from_chemical_symbols(structure.get_chemical_symbols(), potentials) + + +def get_paw_from_chemical_symbols(chemical_symbols, potentials=None): + def _species(element): + if potentials and element in potentials: + entry = potentials[element] + return sphinx.pawPot.species( + potential=str(entry["potential"]), + potType=entry["potType"], + element=element, + ) + return sphinx.pawPot.species( + potential=get_potential_path(element), + potType="AtomPAW", + element=element, + ) -def get_paw_from_chemical_symbols(chemical_symbols): return sphinx.pawPot( - species=[ - sphinx.pawPot.species( - potential=get_potential_path(c), - potType="AtomPAW", - element=c, - ) - for c in np.unique(chemical_symbols) - ] + species=[_species(c) for c in np.unique(chemical_symbols)] ) diff --git a/tests/unit/test_jobs.py b/tests/unit/test_jobs.py index 0d5ac0e..0508e55 100644 --- a/tests/unit/test_jobs.py +++ b/tests/unit/test_jobs.py @@ -1,4 +1,5 @@ import unittest +from pathlib import Path from ase.build import bulk @@ -13,6 +14,17 @@ def test_magnetic_bulk(self): structure = bulk("Al", cubic=True) self.assertFalse("atomicSpin" in to_sphinx(set_base_parameters(structure))) + def test_potentials_forwarded(self): + structure = bulk("Al", cubic=True) + custom_path = Path("/custom/Al_GGA.atomicdata") + input_sx = set_base_parameters( + structure, + potentials={"Al": {"potential": custom_path, "potType": "VaspPAW"}}, + ) + output = to_sphinx(input_sx) + self.assertIn(str(custom_path), output) + self.assertIn("VaspPAW", output) + def test_calc_minimize(self): structure = bulk("Fe", cubic=True) input_sx = set_base_parameters(structure) diff --git a/tests/unit/test_potential.py b/tests/unit/test_potential.py index 54c1991..f157e7a 100644 --- a/tests/unit/test_potential.py +++ b/tests/unit/test_potential.py @@ -1,10 +1,12 @@ import os import unittest +from pathlib import Path from sphinx_parser.potential import ( _is_jth_potential, _is_vasp_potential, _remove_hash_tag, + get_paw_from_chemical_symbols, get_potential_path, ) @@ -19,6 +21,57 @@ def setUpClass(cls): def test_path_exists(self): self.assertTrue(os.path.exists(get_potential_path("Ag"))) + # Repeated species blocks are stored as "species", "species___0", ... + @staticmethod + def _all_species(paw_dict): + return [v for k, v in paw_dict.items() if k == "species" or k.startswith("species___")] + + def test_injected_potential_used(self): + custom_path = Path("/custom/pots/Ag_custom.atomicdata") + result = get_paw_from_chemical_symbols( + ["Ag"], + potentials={"Ag": {"potential": custom_path, "potType": "VaspPAW"}}, + ) + species = self._all_species(result) + self.assertEqual(len(species), 1) + self.assertEqual(species[0]["potential"], f'"{custom_path}"') + self.assertEqual(species[0]["potType"], '"VaspPAW"') + + def test_injected_potType_respected(self): + custom_path = Path("/pots/Fe_GGA.atomicdata") + result = get_paw_from_chemical_symbols( + ["Fe"], + potentials={"Fe": {"potential": custom_path, "potType": "AtomPAW"}}, + ) + self.assertEqual(self._all_species(result)[0]["potType"], '"AtomPAW"') + + def test_fallback_when_element_not_in_potentials(self): + # Ag is not in the injected dict, so it must fall back to the env path + result = get_paw_from_chemical_symbols( + ["Ag"], + potentials={"Fe": {"potential": Path("/pots/Fe.atomicdata"), "potType": "AtomPAW"}}, + ) + species = self._all_species(result) + self.assertEqual(len(species), 1) + self.assertIn(get_potential_path("Ag"), species[0]["potential"]) + self.assertEqual(species[0]["potType"], '"AtomPAW"') + + def test_fallback_when_potentials_none(self): + result = get_paw_from_chemical_symbols(["Ag"], potentials=None) + self.assertIn(get_potential_path("Ag"), self._all_species(result)[0]["potential"]) + + def test_mixed_injected_and_fallback(self): + custom_path = Path("/pots/Fe_custom.atomicdata") + result = get_paw_from_chemical_symbols( + ["Ag", "Fe"], + potentials={"Fe": {"potential": custom_path, "potType": "VaspPAW"}}, + ) + by_element = {s["element"].strip('"'): s for s in self._all_species(result)} + self.assertIn(str(custom_path), by_element["Fe"]["potential"]) + self.assertEqual(by_element["Fe"]["potType"], '"VaspPAW"') + self.assertIn(get_potential_path("Ag"), by_element["Ag"]["potential"]) + self.assertEqual(by_element["Ag"]["potType"], '"AtomPAW"') + def test_is_xyz_potential(self): with open(os.path.join(self.file_path, "potentials", "Ag_POTCAR"), "r") as f: file_content = f.read() From fb23aa68976b322bffab6215ea24ee8d21a3542e Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Mon, 20 Apr 2026 16:10:12 -0400 Subject: [PATCH 09/13] Add docstring to SphinxDft calculator Co-Authored-By: Claude Sonnet 4.6 --- sphinx_parser/calculator.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/sphinx_parser/calculator.py b/sphinx_parser/calculator.py index b3d7593..8aa8d8d 100644 --- a/sphinx_parser/calculator.py +++ b/sphinx_parser/calculator.py @@ -9,6 +9,35 @@ class SphinxDft(FileIOCalculator): + """ASE FileIOCalculator interface for the Sphinx DFT code. + + Writes a Sphinx input file, runs the Sphinx binary, and parses the + resulting log for energy and forces. + + Args: + command (str): Sphinx executable name or path. Defaults to "sphinx". + potentials (dict[str, dict] | None): Optional per-element PAW potential + overrides. Each key is an element symbol; each value is a dict with: + - ``potential`` (Path | str): path to the potential file. + - ``potType`` (str): potential type, e.g. ``"AtomPAW"`` or + ``"VASP"``. + Elements absent from this dict fall back to the JTH-GGA-PBE + potentials found under ``$CONDA_PREFIX``. + *args, **kwargs: Forwarded to :class:`ase.calculators.calculator.FileIOCalculator`. + + Example:: + + calc = SphinxDft(directory="./run") + + calc = SphinxDft( + directory="./run", + potentials={ + "Fe": {"potential": Path("/pots/Fe_GGA.atomicdata"), "potType": "AtomPAW"}, + "Al": {"potential": Path("/pots/Al_VASP.atomicdata"), "potType": "VASP"}, + }, + ) + """ + implemented_properties = ["energy", "forces"] def __init__(self, *args, command: str = "sphinx", potentials=None, **kwargs): From 87c269b34f07560887c361ce37ac46892cc5e476 Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Mon, 20 Apr 2026 20:11:26 +0000 Subject: [PATCH 10/13] Apply suggestion from @pmrv --- sphinx_parser/calculator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx_parser/calculator.py b/sphinx_parser/calculator.py index 8aa8d8d..4678328 100644 --- a/sphinx_parser/calculator.py +++ b/sphinx_parser/calculator.py @@ -33,7 +33,7 @@ class SphinxDft(FileIOCalculator): directory="./run", potentials={ "Fe": {"potential": Path("/pots/Fe_GGA.atomicdata"), "potType": "AtomPAW"}, - "Al": {"potential": Path("/pots/Al_VASP.atomicdata"), "potType": "VASP"}, + "Al": {"potential": Path("/pots/Al_VASP.POTCAR"), "potType": "VASP"}, }, ) """ From 7f1d4fb964fb5379f1b94c2cb52e7bb4e86794a1 Mon Sep 17 00:00:00 2001 From: Marvin Poul Date: Tue, 21 Apr 2026 13:48:26 +0000 Subject: [PATCH 11/13] Update sphinx_parser/potential.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- sphinx_parser/potential.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sphinx_parser/potential.py b/sphinx_parser/potential.py index 3c25458..758bce6 100644 --- a/sphinx_parser/potential.py +++ b/sphinx_parser/potential.py @@ -8,7 +8,9 @@ def get_paw_from_structure(structure, potentials=None): - return get_paw_from_chemical_symbols(structure.get_chemical_symbols(), potentials) + return get_paw_from_chemical_symbols( + structure.get_chemical_symbols(), potentials=potentials + ) def get_paw_from_chemical_symbols(chemical_symbols, potentials=None): From 77670121360b4edae2e8d0a6fd31e8ae7584ed75 Mon Sep 17 00:00:00 2001 From: Sam Waseda Date: Sun, 26 Apr 2026 21:20:46 +0200 Subject: [PATCH 12/13] run black --- sphinx_parser/potential.py | 4 +--- tests/unit/test_potential.py | 14 +++++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/sphinx_parser/potential.py b/sphinx_parser/potential.py index 758bce6..07b1a09 100644 --- a/sphinx_parser/potential.py +++ b/sphinx_parser/potential.py @@ -28,9 +28,7 @@ def _species(element): element=element, ) - return sphinx.pawPot( - species=[_species(c) for c in np.unique(chemical_symbols)] - ) + return sphinx.pawPot(species=[_species(c) for c in np.unique(chemical_symbols)]) def get_potential_path(element: str): diff --git a/tests/unit/test_potential.py b/tests/unit/test_potential.py index f157e7a..0921113 100644 --- a/tests/unit/test_potential.py +++ b/tests/unit/test_potential.py @@ -24,7 +24,11 @@ def test_path_exists(self): # Repeated species blocks are stored as "species", "species___0", ... @staticmethod def _all_species(paw_dict): - return [v for k, v in paw_dict.items() if k == "species" or k.startswith("species___")] + return [ + v + for k, v in paw_dict.items() + if k == "species" or k.startswith("species___") + ] def test_injected_potential_used(self): custom_path = Path("/custom/pots/Ag_custom.atomicdata") @@ -49,7 +53,9 @@ def test_fallback_when_element_not_in_potentials(self): # Ag is not in the injected dict, so it must fall back to the env path result = get_paw_from_chemical_symbols( ["Ag"], - potentials={"Fe": {"potential": Path("/pots/Fe.atomicdata"), "potType": "AtomPAW"}}, + potentials={ + "Fe": {"potential": Path("/pots/Fe.atomicdata"), "potType": "AtomPAW"} + }, ) species = self._all_species(result) self.assertEqual(len(species), 1) @@ -58,7 +64,9 @@ def test_fallback_when_element_not_in_potentials(self): def test_fallback_when_potentials_none(self): result = get_paw_from_chemical_symbols(["Ag"], potentials=None) - self.assertIn(get_potential_path("Ag"), self._all_species(result)[0]["potential"]) + self.assertIn( + get_potential_path("Ag"), self._all_species(result)[0]["potential"] + ) def test_mixed_injected_and_fallback(self): custom_path = Path("/pots/Fe_custom.atomicdata") From 7a8bb26811341a3fbb711d297ac5cde71ee3155b Mon Sep 17 00:00:00 2001 From: Sam Waseda Date: Sun, 26 Apr 2026 21:23:32 +0200 Subject: [PATCH 13/13] ruff sort --- sphinx_parser/calculator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx_parser/calculator.py b/sphinx_parser/calculator.py index 4678328..e150794 100644 --- a/sphinx_parser/calculator.py +++ b/sphinx_parser/calculator.py @@ -1,7 +1,7 @@ from pathlib import Path from ase.calculators.calculator import FileIOCalculator, FileIORules, StandardProfile -from ase.units import Hartree, Bohr +from ase.units import Bohr, Hartree from sphinx_parser.jobs import set_base_parameters from sphinx_parser.output import SphinxLogParser