Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for having non-displaced atoms in Phonopy routines #2110

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
d2aa51a
initial
tomdemeyere May 4, 2024
24608c9
done
tomdemeyere May 5, 2024
d6deffc
fix
tomdemeyere May 5, 2024
1f31546
fix?
tomdemeyere May 5, 2024
a7b3a0c
suggestions
tomdemeyere May 8, 2024
38a4230
pre-commit auto-fixes
pre-commit-ci[bot] May 8, 2024
7430b8a
tests
tomdemeyere May 8, 2024
6939b90
Merge remote-tracking branch 'origin/phonopy_fixed_atoms' into phonop…
tomdemeyere May 8, 2024
54c4e5f
initial
tomdemeyere May 10, 2024
b7789d2
Merge branch 'main' into phonopy_fixed_atoms_v2
tomdemeyere May 10, 2024
5536691
pre-commit auto-fixes
pre-commit-ci[bot] May 10, 2024
93a66a6
some changes
tomdemeyere May 10, 2024
75b8f9f
some changes
tomdemeyere May 10, 2024
56fc1ac
pre-commit auto-fixes
pre-commit-ci[bot] May 10, 2024
b98984e
some changes
tomdemeyere May 10, 2024
15f4469
Merge remote-tracking branch 'origin/phonopy_fixed_atoms_v2' into pho…
tomdemeyere May 10, 2024
e715946
Merge branch 'main' into phonopy_fixed_atoms_v2
Andrew-S-Rosen May 12, 2024
f39bf76
Merge branch 'main' into phonopy_fixed_atoms_v2
Andrew-S-Rosen Jun 5, 2024
2225c9d
pre-commit auto-fixes
pre-commit-ci[bot] Jun 5, 2024
0d06540
Merge branch 'main' into phonopy_fixed_atoms_v2
Andrew-S-Rosen Jun 12, 2024
8817af3
pre-commit auto-fixes
pre-commit-ci[bot] Jun 12, 2024
923ca89
Update phonons.py
Andrew-S-Rosen Jun 12, 2024
2f3e943
Update phonons.py
Andrew-S-Rosen Jun 12, 2024
1d710db
pre-commit auto-fixes
pre-commit-ci[bot] Jun 12, 2024
7329f9b
Update phonons.py
Andrew-S-Rosen Jun 12, 2024
e102d40
fix
Andrew-S-Rosen Jun 12, 2024
98d0274
fix
Andrew-S-Rosen Jun 12, 2024
4f1027e
Update phonons.py
Andrew-S-Rosen Jun 12, 2024
0dec274
Merge branch 'main' into phonopy_fixed_atoms_v2
Andrew-S-Rosen Jun 20, 2024
5f99ce4
Merge branch 'main' into phonopy_fixed_atoms_v2
Andrew-S-Rosen Jun 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion src/quacc/atoms/phonons.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

if has_phonopy:
from phonopy import Phonopy
from phonopy.structure.cells import get_supercell

if TYPE_CHECKING:
from ase.atoms import Atoms
Expand All @@ -25,7 +26,7 @@
@requires(has_phonopy, "Phonopy not installed.")
def get_phonopy(
atoms: Atoms,
min_lengths: float | tuple[float, float, float] | None = None,
min_lengths: float | tuple[float, float, float] | None = 20.0,
supercell_matrix: (
tuple[tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]] | None
) = None,
Expand Down Expand Up @@ -91,3 +92,32 @@ def phonopy_atoms_to_ase_atoms(phonpy_atoms: PhonopyAtoms) -> Atoms:
"""
pmg_structure = get_pmg_structure(phonpy_atoms)
return pmg_structure.to_ase_atoms()


def get_atoms_supercell_by_phonopy(
atoms: Atoms,
supercell_matrix: tuple[
tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]
],
) -> Atoms:
Comment on lines +97 to +102
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be ideal to have a simple unit test for this specific function.

"""
Get the supercell of an ASE atoms object using a supercell matrix.

Parameters
----------
atoms
ASE atoms object.
supercell_matrix
The supercell matrix to use. If specified, it will override any
value specified by `min_lengths`.
Comment on lines +111 to +112
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no min_lengths in this function. I think we can just ignore that part. Also, it must be specified since it is a positional argument.

Returns
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a line break before Returns.

-------
Atoms
ASE atoms object of the supercell.
"""

return phonopy_atoms_to_ase_atoms(
get_supercell(
get_phonopy_structure(Structure.from_ase_atoms(atoms)), supercell_matrix
)
)
89 changes: 62 additions & 27 deletions src/quacc/recipes/common/phonons.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@
from importlib.util import find_spec
from typing import TYPE_CHECKING

from ase.atoms import Atoms
from monty.dev import requires

from quacc import job, subflow
from quacc.atoms.phonons import get_phonopy, phonopy_atoms_to_ase_atoms
from quacc.atoms.phonons import (
get_atoms_supercell_by_phonopy,
get_phonopy,
phonopy_atoms_to_ase_atoms,
)
from quacc.runners.phonons import PhonopyRunner
from quacc.schemas.phonons import summarize_phonopy

Expand All @@ -18,21 +23,17 @@
if TYPE_CHECKING:
from typing import Any

from ase.atoms import Atoms

from quacc import Job
from quacc.schemas._aliases.phonons import PhononSchema

if has_phonopy:
from phonopy import Phonopy


@subflow
@requires(has_phonopy, "Phonopy must be installed. Run `pip install quacc[phonons]`")
@requires(has_seekpath, "Seekpath must be installed. Run `pip install quacc[phonons]`")
def phonon_subflow(
atoms: Atoms,
displaced_atoms: Atoms,
force_job: Job,
non_displaced_atoms: Atoms | None = None,
symprec: float = 1e-4,
min_lengths: float | tuple[float, float, float] | None = 20.0,
supercell_matrix: (
Expand All @@ -48,12 +49,22 @@ def phonon_subflow(
"""
Calculate phonon properties.

In Quacc the ASE constraints can be used to fix atoms. These atoms will
not be displaced during the phonon calculation. This will greatly reduce
the computational cost of the calculation. However, this is an important
approximation and should be used with caution.
Comment on lines +52 to +55
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is relevant anymore.


Parameters
----------
atoms
displaced_atoms
Atoms object with calculator attached.
force_job
The static job to calculate the forces.
non_displaced_atoms
Additional atoms to add to the supercells i.e. fixed atoms.
These atoms will not be displaced during the phonon calculation.
Useful for adsorbates on surfaces with weak coupling etc.
Copy link
Member

@Andrew-S-Rosen Andrew-S-Rosen May 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was my point of confusion. I had thought you were implying that additional atoms would be for the adsorbates (I now see that's not the case). In reality, it would be for the surface of course.

Copy link
Contributor Author

@tomdemeyere tomdemeyere May 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I completely see where this confusion can get from, this is why I am not entirely sure about the current name.

Although this forces the users to stop and think before using this important approximation

EDIT: yes, the doc does not help as well

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One downside of fixed_atoms is that it potentially implies that they are sites within atoms (the first positional argument) that are fixed, whereas in reality this is a separate Atoms object. That's one reason why I like additional_atoms since it makes it clear that this is a separate Atoms object, although one would understand that if they read the type hint. Of course, additional_atoms doesn't inherently specify anything about them being fixed until you read the docstring.

I think either could be okay, personally.

Important approximation, use with caution.
symprec
Precision for symmetry detection.
min_lengths
Expand All @@ -80,6 +91,27 @@ def phonon_subflow(
Dictionary of results from [quacc.schemas.phonons.summarize_phonopy][]
"""

non_displaced_atoms = non_displaced_atoms or Atoms()

phonopy = get_phonopy(
displaced_atoms,
min_lengths=min_lengths,
supercell_matrix=supercell_matrix,
symprec=symprec,
displacement=displacement,
phonopy_kwargs=phonopy_kwargs,
)

if non_displaced_atoms:
non_displaced_atoms = get_atoms_supercell_by_phonopy(
non_displaced_atoms, phonopy.supercell_matrix
)

supercells = [
phonopy_atoms_to_ase_atoms(s) + non_displaced_atoms
for s in phonopy.supercells_with_displacements
]

@subflow
def _get_forces_subflow(supercells: list[Atoms]) -> list[dict]:
return [
Expand All @@ -89,17 +121,25 @@ def _get_forces_subflow(supercells: list[Atoms]) -> list[dict]:
@job
def _thermo_job(
atoms: Atoms,
phonopy: Phonopy,
phonopy,
force_job_results: list[dict],
t_step: float,
t_min: float,
t_max: float,
additional_fields: dict[str, Any] | None,
t_step,
t_min,
t_max,
additional_fields,
Comment on lines +124 to +129
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume you just removed the type hints for brevity's sake since they are basically implied elsewhere? I was also thinking about this since it is an internal function. Although, if we at some point enabled static type checking, having the type hints would ensure there isn't an error. I doubt we will be able to get to the point where we enable static type checking though... certainly not anytime soon.

) -> PhononSchema:
parameters = force_job_results[-1].get("parameters")
forces = [output["results"]["forces"] for output in force_job_results]
forces = [
output["results"]["forces"][: len(phonopy.supercell)]
for output in force_job_results
]
phonopy_results = PhonopyRunner().run_phonopy(
phonopy, forces, t_step=t_step, t_min=t_min, t_max=t_max
phonopy,
forces,
symmetrize=bool(non_displaced_atoms),
t_step=t_step,
t_min=t_min,
t_max=t_max,
)

return summarize_phonopy(
Expand All @@ -110,18 +150,13 @@ def _thermo_job(
additional_fields=additional_fields,
)

phonopy = get_phonopy(
atoms,
min_lengths=min_lengths,
supercell_matrix=supercell_matrix,
symprec=symprec,
displacement=displacement,
phonopy_kwargs=phonopy_kwargs,
)
supercells = [
phonopy_atoms_to_ase_atoms(s) for s in phonopy.supercells_with_displacements
]
force_job_results = _get_forces_subflow(supercells)
return _thermo_job(
atoms, phonopy, force_job_results, t_step, t_min, t_max, additional_fields
displaced_atoms,
phonopy,
force_job_results,
t_step,
t_min,
t_max,
additional_fields,
)
17 changes: 12 additions & 5 deletions src/quacc/recipes/emt/phonons.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@

@flow
def phonon_flow(
atoms: Atoms,
displaced_atoms: Atoms,
symprec: float = 1e-4,
min_lengths: float | tuple[float, float, float] | None = 20.0,
supercell_matrix: (
tuple[tuple[int, int, int], tuple[int, int, int], tuple[int, int, int]] | None
) = None,
displacement: float = 0.01,
non_displaced_atoms: Atoms | None = None,
t_step: float = 10,
t_min: float = 0,
t_max: float = 1000,
Expand All @@ -50,7 +51,7 @@ def phonon_flow(

Parameters
----------
atoms
displaced_atoms
Atoms object
symprec
Precision for symmetry detection.
Expand All @@ -61,6 +62,11 @@ def phonon_flow(
value specified by `min_lengths`.
displacement
Atomic displacement (A).
non_displaced_atoms
Additional atoms to add to the supercells i.e. fixed atoms.
These atoms will not be displaced during the phonon calculation.
Useful for adsorbates on surfaces with weak coupling etc.
Important approximation, use with caution.
t_step
Temperature step (K).
t_min
Expand Down Expand Up @@ -90,15 +96,16 @@ def phonon_flow(
param_swaps=job_params,
decorators=job_decorators,
)
if run_relax:
atoms = relax_job_(atoms)["atoms"]
if run_relax and not non_displaced_atoms:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a little bit problematic, another option is to send both displaced and non_displaced and optimise them and separate them again...

Copy link
Member

@Andrew-S-Rosen Andrew-S-Rosen May 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I mean, I guess we can ask if we really even need a relaxation step beforehand? Presumably one could just call relax_job() before calling the flow and it would be exactly the same (although without a tight force tolerance).

If there's a desire to keep this though, then yeah I think the only route would be to relax displaced_atoms+non_displaced_atoms as a single combined_atoms and then pass in combined_atoms[:len(displaced_atoms)] etc. to phonon_subflow. It does, admittedly, complicate things a bit because it seems weird to relax something that is called "non-displaced", but... 🤷

displaced_atoms = relax_job_(displaced_atoms)["atoms"]

return phonon_subflow(
atoms,
displaced_atoms,
static_job_,
symprec=symprec,
min_lengths=min_lengths,
supercell_matrix=supercell_matrix,
non_displaced_atoms=non_displaced_atoms,
displacement=displacement,
t_step=t_step,
t_min=t_min,
Expand Down
8 changes: 8 additions & 0 deletions src/quacc/runners/phonons.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def run_phonopy(
self,
phonon: Phonopy,
forces: NDArray,
symmetrize: bool = False,
t_step: float = 10,
t_min: float = 0,
t_max: float = 1000,
Expand All @@ -50,6 +51,8 @@ def run_phonopy(
Phonopy object
forces
Forces on the atoms
symmetrize
Whether to symmetrize the force constants
t_step
Temperature step
t_min
Expand All @@ -66,6 +69,11 @@ def run_phonopy(
# Run phonopy
phonon.forces = forces
phonon.produce_force_constants()

if symmetrize:
phonon.symmetrize_force_constants()
phonon.symmetrize_force_constants_by_space_group()

phonon.run_mesh(with_eigenvectors=True)
phonon.run_total_dos()
phonon.run_thermal_properties(t_step=t_step, t_max=t_max, t_min=t_min)
Expand Down
19 changes: 14 additions & 5 deletions tests/core/atoms/test_phonons.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,35 @@

import numpy as np
from ase.build import bulk
from ase.constraints import FixAtoms
from numpy.testing import assert_almost_equal, assert_array_equal

from quacc.atoms.phonons import get_phonopy


def test_get_phonopy():
atoms = bulk("Cu")
phonopy = get_phonopy(atoms)
phonopy, _ = get_phonopy(atoms)
assert_array_equal(phonopy.supercell_matrix, [[1, 0, 0], [0, 1, 0], [0, 0, 1]])

phonopy = get_phonopy(atoms, min_lengths=5)
phonopy, _ = get_phonopy(atoms, min_lengths=5)
assert_array_equal(phonopy.supercell_matrix, [[2, 0, 0], [0, 2, 0], [0, 0, 2]])

phonopy = get_phonopy(atoms, min_lengths=[5, 10, 5])
phonopy, _ = get_phonopy(atoms, min_lengths=[5, 10, 5])
assert_array_equal(phonopy.supercell_matrix, [[2, 0, 0], [0, 4, 0], [0, 0, 2]])

phonopy = get_phonopy(atoms, displacement=1)
phonopy, _ = get_phonopy(atoms, displacement=1)
assert_almost_equal(
phonopy.displacements, [[0, 0.0, np.sqrt(2) / 2, np.sqrt(2) / 2]]
)

phonopy = get_phonopy(atoms, symprec=1e-8)
phonopy, _ = get_phonopy(atoms, symprec=1e-8)
assert phonopy.symmetry.tolerance == 1e-8

atoms = bulk("Cu") * (2, 2, 2)

atoms.set_constraint(FixAtoms(indices=[0, 1, 2, 3]))

phonopy, fixed_atoms = get_phonopy(atoms, min_lengths=5)

assert len(fixed_atoms) == 4
22 changes: 20 additions & 2 deletions tests/core/recipes/emt_recipes/test_emt_phonons.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
pytest.importorskip("phonopy")
pytest.importorskip("seekpath")

from ase.build import bulk
from ase.build import bulk, molecule

from quacc.recipes.emt.phonons import phonon_flow

Expand Down Expand Up @@ -70,4 +70,22 @@ def test_phonon_flow_v4(tmp_path, monkeypatch):
assert output["results"]["thermal_properties"]["temperatures"][-1] == 1000
assert output["results"]["force_constants"].shape == (8, 8, 3, 3)
assert "mesh_properties" in output["results"]
assert output["atoms"] != atoms
assert output["atoms"] == atoms
Copy link
Member

@Andrew-S-Rosen Andrew-S-Rosen May 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the test failure in CI, it looks like the original atoms object is getting mutated somewhere (since it has an EMT calculator attached)? That doesn't seem ideal... 😅

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was related to #2230.



def test_phonon_flow_fixed(tmp_path, monkeypatch):
monkeypatch.chdir(tmp_path)
atoms = molecule("H2", vacuum=20.0)
output = phonon_flow(atoms, run_relax=True, min_lengths=5.0)

atoms1 = molecule("H2", vacuum=20.0)
atoms2 = molecule("H2", vacuum=20.0)

atoms2.positions += [10, 10, 10]

output_fixed = phonon_flow(atoms1, non_displaced_atoms=atoms2, min_lengths=5.0)

# Should be very close but not exactly the same
assert output["results"]["mesh_properties"]["frequencies"] == pytest.approx(
output_fixed["results"]["mesh_properties"]["frequencies"], rel=0.0, abs=1e-5
)
Loading