import logging
import ase
import numpy as np
from clease.datastructures import MCStep
from .mc_observer import MCObserver
logger = logging.getLogger(__name__)
[docs]
class BandGapObserver(MCObserver):
"""
Observer that can be attached to a MC run, to track the band gap of a
structure.
It is possible to use the same calculator object as for the energy prediction
if the ECIs are build with the same clusters leading to identical correlation functions.
In the case of seperate clusters settings, an additional seperate calculator should be
passed to the observer.
Parameters:
atoms : ase.Atoms (Pointer)
Atoms object used for MC
bandgap_eci :
ECI coefficients for the band gap.
separate_calc :
A separate Clease optional calculator to use if the bandgap ECIs were fitted
using different cluster settings than the energy calculator.
If provided, correlation functions will be computed from this calculator
instead of from atoms.calc. If None (default), uses the same calculator
as the MC simulation, assuming both energy and bandgap use identical
cluster definitions.
verbose : bool
If True, prints messages when new extreme band gaps are found.
"""
name = "BandGapObserver"
def __init__(
self,
atoms: ase.Atoms,
bandgap_eci: dict,
seperate_calc=None,
verbose: bool = False,
):
super().__init__()
self.atoms = atoms
self.bandgap_eci = bandgap_eci
# self.track_structure = track_structure
self.verbose = verbose
# Storage for band gap values
self.bandgap_history = []
self.seperate_calc = seperate_calc
[docs]
def reset(self) -> None:
"""Reset all tracked values."""
self.bandgap_history = []
@property
def calc(self):
"""
Get the calculator object.
"""
return self.atoms.calc
[docs]
def observe_step(self, mc_step: MCStep) -> None:
"""
Calculate and store the band gap for the current structure.
Parameters
----------
mc_step : MCStep
Information about the latest MC step
"""
# Get CF from seperate calculator or from the atoms object
if self.seperate_calc is not None:
# Match the separate calculator's atoms symbols
# to match the current MC configuration
self.seperate_calc.atoms.symbols[:] = self.atoms.symbols
# force recalculation of CFs with the new symbols
self.seperate_calc.update_cf(None)
cf = self.seperate_calc.get_cf()
else:
cf = self.calc.get_cf()
# Validate that all bandgap ECI clusters exist in the CF
missing_clusters = set(self.bandgap_eci.keys()) - set(cf.keys())
if missing_clusters:
raise ValueError(
f"Bandgap ECIs contain clusters not in correlation functions: {missing_clusters}. "
"Check that the cluster settings match between energy and bandgap fits."
)
# Calculate bandgap using CIs.
self.bandgap = sum(
ci_value * cf[cluster_name] for cluster_name, ci_value in self.bandgap_eci.items()
)
# Store in history
self.bandgap_history.append(self.bandgap)
# Print if verbose
if self.verbose:
msg = f"Current bandgap: {self.bandgap:.6f}"
print(msg)
# Simple save function to txt file
[docs]
def save(self, fname: str = "bandgap_evolution.txt") -> None:
"""
Save the band gap evolution to a txt file.
"""
np.savetxt(fname, self.bandgap_history, delimiter=",")
logger.info("Band gap evolution data saved to %s.", fname)