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

feature/SOF-7427 feat: ASE distance norm calculator #160

Merged
merged 23 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
cf6244c
feat: add ASE calculator with docstring and required forces
VsevolodX Sep 2, 2024
51e3e87
Merge branch 'feature/SOF-7426' into feature/SOF-7427
VsevolodX Sep 2, 2024
e695e33
update: add constraints
VsevolodX Sep 3, 2024
f1f4eaa
update: optimize code
VsevolodX Sep 3, 2024
db91642
chore: import from 3rd party
VsevolodX Sep 3, 2024
65d5843
Merge branch 'feature/SOF-7426' into feature/SOF-7427
VsevolodX Sep 12, 2024
7c2890f
Merge branch 'feature/SOF-7426' into feature/SOF-7427
VsevolodX Sep 13, 2024
fad3997
Merge branch 'feature/SOF-7426' into feature/SOF-7427
VsevolodX Sep 13, 2024
ce42e81
chore: docstring
VsevolodX Sep 13, 2024
c4daf74
chore: rename norm -> energy
VsevolodX Sep 14, 2024
708ff39
Merge branch 'feature/SOF-7426' into feature/SOF-7427
VsevolodX Sep 16, 2024
0daf4ce
update: use m calculator
VsevolodX Sep 16, 2024
81dd780
update: adjustemtns in calculator
VsevolodX Sep 16, 2024
4761da8
chore: small adjustmnets
VsevolodX Sep 17, 2024
3617623
feat: add rigid xy constraints with help from o1
VsevolodX Sep 17, 2024
56b5bf9
chore: remove unused imports
VsevolodX Sep 17, 2024
4079d8a
chore: add types
VsevolodX Sep 17, 2024
736be41
chore: arbitrary types allowed
VsevolodX Sep 17, 2024
9a8db13
Merge branch 'main' into feature/SOF-7427
VsevolodX Sep 17, 2024
09b2acd
update: inherit from constraint class
VsevolodX Sep 17, 2024
aaf4946
chore: move constraints to a separate file
VsevolodX Sep 17, 2024
da49cf4
update: isolate ase-related calculator
VsevolodX Sep 19, 2024
5aa6454
update: isolate filtering
VsevolodX Sep 19, 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
4 changes: 2 additions & 2 deletions src/py/mat3ra/made/tools/calculate/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from typing import Optional
from typing import List, Optional, Union

from ...material import Material
from ..analyze import get_surface_area
from ..build.interface.utils import get_slab
from ..convert import decorator_convert_material_args_kwargs_to_atoms
from ..convert import decorator_convert_material_args_kwargs_to_atoms, from_ase
from ..third_party import ASEAtoms, ASECalculator, ASECalculatorEMT
from .interaction_functions import sum_of_inverse_distances_squared

Expand Down
133 changes: 133 additions & 0 deletions src/py/mat3ra/made/tools/calculate/ase/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
from typing import List, Optional, Union

import numpy as np
from mat3ra.made.material import Material

from ...convert import from_ase
from ...convert.utils import INTERFACE_LABELS_MAP, InterfacePartsEnum
from ...third_party import ASEAtoms, ASECalculator, ASEFixAtoms, ASEFixedPlane, ase_all_changes
from ..calculators import InterfaceMaterialCalculator, MaterialCalculator
from .constraints import RigidFilmXYInterfaceConstraint


def get_interface_part_indices(atoms: ASEAtoms, part: InterfacePartsEnum) -> List[int]:
"""
Get the indices of the atoms in the interface part.

Args:
atoms (ASEAtoms): The ASE Atoms object.
part (str): The interface part to get the indices of.

Returns:
List[int]: The indices of the atoms in the interface part.
"""
return [i for i, tag in enumerate(atoms.get_tags()) if tag == INTERFACE_LABELS_MAP[part]]


class FilmSubstrateDistanceASECalculator(ASECalculator):
"""
ASE calculator that calculates the interaction energy between a film and substrate in an interface material.

Args:
shadowing_radius (float): The radius for atom to shadow underlying from being considered surface, in Angstroms.
force_constant (float): The force constant for the finite difference approximation of the forces.
is_substrate_fixed (bool): Whether to fix the substrate atoms.
is_z_axis_fixed (bool): Whether to fix atoms movement in the z direction.
symprec (float): The symmetry precision for the ASE calculator. This parameter determines the tolerance
for symmetry operations, affecting the identification of equivalent atoms and the overall
symmetry of the system. For more details, refer to the ASE documentation:
https://wiki.fysik.dtu.dk/ase/ase/constraints.html#ase.constraints.FixSymmetry

Example usage:
```python
from ase.optimize import BFGS
atoms = to_ase(material)
calc = SurfaceDistanceCalculator(shadowing_radius=2.5)

atoms.calc = calc
opt = BFGS(atoms)
opt.run(fmax=0.05)
```
Args:
shadowing_radius (float): Radius for atoms shadowing underlying from being treated as a surface, in Angstroms.
force_constant (float): The force constant for the finite difference approximation of the
Note:
Built following https://wiki.fysik.dtu.dk/ase/development/calculators.html

The calculate method is responsible for computing the energy and forces (if requested).
Forces are estimated using a finite difference method, which is a simple approximation
and might not be the most accurate or efficient for all cases.
"""

implemented_properties = ["energy", "forces"]

def __init__(
self,
shadowing_radius: float = 2.5,
is_substrate_fixed: bool = True,
is_z_axis_fixed: bool = True,
symprec: float = 0.01,
material_calculator: Union[MaterialCalculator, InterfaceMaterialCalculator] = InterfaceMaterialCalculator(),
**kwargs,
):
super().__init__(**kwargs)
self.shadowing_radius = shadowing_radius
self.fix_substrate = is_substrate_fixed
self.fix_z = is_z_axis_fixed
self.symprec = symprec
self.material_calculator = material_calculator

def _add_constraints(self, atoms: ASEAtoms) -> ASEAtoms:
constraints: List[Union[ASEFixAtoms, ASEFixedPlane, RigidFilmXYInterfaceConstraint]] = []
if self.fix_substrate:
substrate_indices = get_interface_part_indices(atoms, InterfacePartsEnum.SUBSTRATE)
constraints.append(ASEFixAtoms(indices=substrate_indices))
if self.fix_z:
all_indices = list(range(len(atoms)))
constraints.append(ASEFixedPlane(indices=all_indices, direction=[0, 0, 1]))

film_indices = get_interface_part_indices(atoms, InterfacePartsEnum.FILM)
if film_indices:
constraints.append(RigidFilmXYInterfaceConstraint(film_indices=film_indices))

atoms.set_constraint(constraints)
return atoms

def _calculate_forces(self, atoms: ASEAtoms, energy: float) -> np.ndarray:
forces = np.zeros((len(atoms), 3))
dx = 0.01
for i in range(len(atoms)):
for j in range(3):
atoms_plus = atoms.copy()
atoms_plus.positions[i, j] += dx
material_plus = Material(from_ase(atoms_plus))
energy_plus = self.material_calculator.get_energy(material_plus)

forces[i, j] = -(energy_plus - energy) / dx

return forces

def calculate(self, atoms: Optional[ASEAtoms] = None, properties=["energy"], system_changes=ase_all_changes):
if atoms is None:
atoms = self.atoms.copy()

atoms = self._add_constraints(atoms)
constraints = atoms.constraints

super().calculate(atoms, properties, system_changes)
material = Material(from_ase(atoms))
energy = self.material_calculator.get_energy(material)

self.results = {"energy": energy}

if "forces" in properties:
forces = self._calculate_forces(atoms, energy)
for constraint in constraints:
constraint.adjust_forces(atoms, forces)
self.results["forces"] = forces

def get_potential_energy(self, atoms=None, force_consistent=False):
return self.get_property("energy", atoms)

def get_forces(self, atoms=None):
return self.get_property("forces", atoms)
91 changes: 91 additions & 0 deletions src/py/mat3ra/made/tools/calculate/ase/constraints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from typing import List, Optional

import numpy as np
from pydantic import BaseModel

from ...third_party import ASEAtoms


class InterfaceConstraint(BaseModel):
"""
Based on https://wiki.fysik.dtu.dk/ase/ase/constraints.html#making-your-own-constraint-class
"""

class Config:
arbitrary_types_allowed = True

def adjust_positions(self, atoms: ASEAtoms, new_positions: np.ndarray) -> None:
"""
Adjust the positions of the atoms in the system.

Args:
atoms (ASEAtoms): The ASE Atoms object.
new_positions (numpy.ndarray): The new positions to adjust in place.
"""
raise NotImplementedError

def adjust_forces(self, forces: np.ndarray) -> None:
"""
Adjust the forces on the atoms in the system.

Args:
forces (numpy.ndarray): The forces array to adjust in place.
"""
raise NotImplementedError


class RigidFilmXYInterfaceConstraint(InterfaceConstraint):
"""
Custom constraint to allow only rigid translation in x and y for film atoms.
"""

film_indices: List[int] = []
initial_positions: Optional[np.ndarray] = None
reference_center: Optional[np.ndarray] = None

def adjust_positions(self, atoms: ASEAtoms, new_positions: np.ndarray) -> None:
"""
Adjust the positions of film atoms to maintain rigidity in x and y.

Args:
atoms (ase.Atoms): The ASE Atoms object.
new_positions (numpy.ndarray): The new positions to adjust in place.
"""
if self.initial_positions is None:
self.initial_positions = atoms.positions[self.film_indices].copy()
self.reference_center = self.initial_positions.mean(axis=0)

current_center = new_positions[self.film_indices].mean(axis=0)
desired_translation = current_center - self.reference_center
desired_translation[2] = 0.0

# Update positions to maintain rigid body translation in x and y
for i, idx in enumerate(self.film_indices):
new_positions[idx, 0] = self.initial_positions[i, 0] + desired_translation[0]
new_positions[idx, 1] = self.initial_positions[i, 1] + desired_translation[1]

def adjust_forces(self, forces: np.ndarray) -> None:
"""
Adjust the forces on film atoms to ensure rigidity.

Args:
forces (numpy.ndarray): The forces array to adjust in place.
"""
if self.initial_positions is None:
return # No adjustment needed if initial positions are not set

# Project forces onto x and y for collective translation
net_force_x = np.sum(forces[self.film_indices, 0])
net_force_y = np.sum(forces[self.film_indices, 1])

# Distribute the net force equally to all film atoms to maintain rigidity
num_film_atoms = len(self.film_indices)
if num_film_atoms == 0:
return # Avoid division by zero

distributed_force_x = net_force_x / num_film_atoms
distributed_force_y = net_force_y / num_film_atoms

for idx in self.film_indices:
forces[idx, 0] = distributed_force_x
forces[idx, 1] = distributed_force_y
6 changes: 6 additions & 0 deletions src/py/mat3ra/made/tools/third_party.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
from ase.build import add_vacuum as ase_add_vacuum
from ase.build.supercells import make_supercell as ase_make_supercell
from ase.calculators.calculator import Calculator as ASECalculator
from ase.calculators.calculator import all_changes as ase_all_changes
from ase.calculators.emt import EMT as ASECalculatorEMT
from ase.constraints import FixAtoms as ASEFixAtoms
from ase.constraints import FixedPlane as ASEFixedPlane
from pymatgen.analysis.defects.core import Interstitial as PymatgenInterstitial
from pymatgen.analysis.defects.core import Substitution as PymatgenSubstitution
from pymatgen.analysis.defects.core import Vacancy as PymatgenVacancy
Expand All @@ -25,6 +28,9 @@
"ASEAtoms",
"ASECalculator",
"ASECalculatorEMT",
"ASEFixAtoms",
"ASEFixedPlane",
"ase_all_changes",
"PymatgenLattice",
"PymatgenStructure",
"PymatgenIStructure",
Expand Down
Loading