Source code for pantea.simulation.monte_carlo

from dataclasses import replace
from typing import Protocol

import jax.numpy as jnp
import numpy as np

from pantea.atoms.structure import Structure
from pantea.logger import logger
from pantea.simulation.system import System
from pantea.types import Array
from pantea.units import units

KB: float = units.BOLTZMANN_CONSTANT


[docs]class PotentialInterface(Protocol): def __call__(self, structure: Structure) -> Array: ...
[docs] def compute_forces(self, structure: Structure) -> Array: ...
[docs]class MCSimulator: def __init__( self, translate_step: float, target_temperature: float, movements_per_step: int = 1, seed: int = 12345, ) -> None: """ A Monte Carlo simulator to predict atom configurations for a given temperature. :param translate_step: maximum translate step in Bohr unit :type translate_step: float :param target_temperature: target temperature for the system :type target_temperature: float :param atomic_masses: atomic mass of atoms in the input structure, defaults to None :type atomic_masses: Optional[Array], optional :param movements_per_step: number of random movements per step :type movements_per_step: int, default to 1 :param seed: seed for generating random atom movements, defaults to 12345 :type seed: int, optional It must be noted that this MC simulator modifies atom `positions` and `total_energy` of Structure inside the system. This simulator is intended to be used for small systems of atoms during the potential training in order to generate new dataset. """ logger.debug(f"Initializing {self.__class__.__name__}") self.translate_step = translate_step self.target_temperature = target_temperature self.movements_per_step = movements_per_step self.step: int = 0 np.random.seed(seed)
[docs] def simulate_one_step(self, system: System) -> None: """Update parameters for next time step.""" self.metropolis_algorithm(system) self.step += 1
[docs] def metropolis_algorithm(self, system: System) -> None: """Update atom positions based on metropolis algorithm.""" displacements = np.random.uniform( low=-self.translate_step, high=self.translate_step, size=(self.movements_per_step, 3), ) atom_indices = np.random.randint( low=0, high=system.natoms, size=(self.movements_per_step,) ) new_positions = system.positions.at[atom_indices].add(displacements) new_structure = replace( system.structure, positions=new_positions ) # shallow copy new_energy = system.potential(new_structure) energy = system.structure.total_energy accept: bool = False if new_energy <= energy: accept = True else: prob = jnp.exp(-(new_energy - energy) / (KB * self.target_temperature)) accept = True if prob >= np.random.uniform(0.0, 1.0) else False if accept: system.structure.total_energy = new_energy system.structure.positions = new_structure.positions
[docs] def repr_physical_params(self, system: System) -> str: """Represent current physical parameters.""" return f"{self.step:<10} Epot[Ha]:{system.get_potential_energy():<15.10f} "