wmaee.codes.pyiron.pyiron_CHGNet_job

  1# Metadata
  2__author__ = "David Holec, Amin Sakic"
  3__maintainer__ = "David Holec"
  4__email__ = "david.holec@unileoben.ac.at"
  5__date__ = "2024-12-19"
  6__version__ = "0.11"
  7__license__ = "MIT"
  8
  9from pyiron_atomistics.atomistics.job.atomistic import AtomisticGenericJob
 10from pyiron_atomistics import pyiron_to_ase, ase_to_pyiron
 11from pyiron_base import GenericParameters
 12from ase.io import write, read
 13from os.path import join
 14import pandas as pd
 15from ase import Atoms
 16from ase.io.trajectory import Trajectory
 17import pickle
 18import numpy as np
 19from typing import Literal, Optional
 20
 21
 22class CHGNet(AtomisticGenericJob):
 23    """
 24    Custom job class for using CHGNet within the pyiron framework.
 25
 26    This class provides methods to perform static calculations, structure relaxation,
 27    and molecular dynamics simulations using CHGNet.
 28
 29    Attributes
 30    ----------
 31    structure : pyiron.atomistics.structure.atoms.Atoms
 32        Structure associated with the job.
 33    input : CalcParams
 34        Input parameters for the CHGNet calculations.
 35    """
 36
 37
 38    def __init__(self, project, job_name):
 39        """
 40        Initialize the CHGNet job.
 41
 42        Parameters
 43        ----------
 44        project : pyiron.Project
 45            The pyiron project in which the job resides.
 46        job_name : str
 47            Name of the job.
 48        """
 49        super(CHGNet, self).__init__(project, job_name)
 50        self.structure = None
 51        self.input = CalcParams()
 52        if self.project_hdf5.file_exists:
 53            self.input.from_hdf(self.project_hdf5, group_name='input')
 54        self._executable_activate(codename='chgnet')            
 55
 56    @property
 57    def fmax(self) -> float:
 58        """
 59        Get the maximum force tolerance for ionic relaxation.
 60
 61        Returns
 62        -------
 63        float
 64            Maximum force tolerance (eV/Å).
 65        """
 66        return self._generic_input['ionic_force_tolerance']
 67
 68    @fmax.setter
 69    def fmax(self, fmax: float):
 70        """
 71        Set the maximum force tolerance for ionic relaxation.
 72
 73        Parameters
 74        ----------
 75        fmax : float
 76            Maximum force tolerance (eV/Å).
 77        """
 78        self._generic_input['ionic_force_tolerance'] = fmax
 79
 80    @property
 81    def max_iter(self) -> int:
 82        """
 83        Get the maximum number of iterations for relaxation.
 84
 85        Returns
 86        -------
 87        int
 88            Maximum number of iterations.
 89        """
 90        return self._generic_input['n_print']
 91
 92    @max_iter.setter
 93    def max_iter(self, max_iter: int):
 94        """
 95        Set the maximum number of iterations for relaxation.
 96
 97        Parameters
 98        ----------
 99        max_iter : int
100            Maximum number of iterations.
101        """
102        self._generic_input['n_print'] = max_iter
103
104        
105    def calc_static(
106        self,
107        on_isolated_atoms: Literal["ignore", "warn", "error"] = "warn",
108    ):        
109        """
110        Perform a static calculation.
111
112        Parameters
113        ----------
114        on_isolated_atoms : {'ignore', 'warn', 'error'}, optional
115            Behavior for isolated atoms (default is 'warn').
116        """
117        self.calc_minimize(max_iter=0, relax_cell=False, on_isolated_atoms=on_isolated_atoms)
118        self._generic_input["calc_mode"] = "static"   
119       
120    def calc_minimize(
121        self,
122        ionic_force_tolerance: float = 0.1,
123        max_iter: int = 500,
124        n_print: int = 1,
125        relax_cell: bool | None = True,
126        save_path: str | None = 'relax.traj',
127        on_isolated_atoms: Literal["ignore", "warn", "error"] = "warn",
128    ):
129        """
130        Perform a structure relaxation (geometry optimization). More CHGNet parameters can be specified by 
131        explicitly adding them to `job.input` dictionary, see
132        [https://chgnet.lbl.gov/api#class-structoptimizer](https://chgnet.lbl.gov/api#class-structoptimizer).
133
134        Parameters
135        ----------
136        ionic_force_tolerance : float, optional
137            Convergence criterion for forces (default is 0.1 eV/Å).
138        max_iter : int, optional
139            Maximum number of iterations (default is 500).
140        n_print : int, optional
141            Frequency of logging output (default is 1).
142        relax_cell : bool or None, optional
143            Whether to allow relaxation of the simulation cell (default is True).
144        save_path : str or None, optional
145            Path to save the trajectory file (default is 'relax.traj').
146        on_isolated_atoms : {'ignore', 'warn', 'error'}, optional
147            Behavior for isolated atoms (default is 'warn').
148        """
149        super().calc_minimize(
150            ionic_energy_tolerance=0,
151            ionic_force_tolerance=ionic_force_tolerance,
152            max_iter=max_iter,            
153        )       
154        self.input['relax_cell'] = relax_cell
155        self.input['save_path'] = save_path
156        self.input['verbose'] = True
157        self.input['assign_magmoms'] = True                          
158        self.input['on_isolated_atoms'] = on_isolated_atoms 
159        self._generic_input['n_print'] = n_print
160        self._generic_input['ionic_force_tolerance'] = ionic_force_tolerance        
161    
162    def _write_calc_minimize(self):
163        """
164        Write the calculation script for performing structure relaxation.
165
166        This method generates a Python script that uses CHGNet for structure
167        relaxation and writes it to a file in the working directory.
168        """
169        params = ['model=chgnet']
170        allowed_params = ['optimizer_class', 'use_device', 'on_isolated_atoms']
171        for p in self.input.keys():
172            v = self.input.get(p)
173            if p in allowed_params and not v == None:
174                if type(v) == str:
175                    params.append(p + "=\"" + v + "\"")
176                else:
177                    params.append(f'{p}={self.input.get(p)}')
178        model_params = ','.join(params)
179
180        params = []
181        params += [f"fmax={self._generic_input['ionic_force_tolerance']},steps={self._generic_input['max_iter']}"]
182        params += [f"loginterval={self._generic_input['n_print']}"]
183        allowed_params = ['relax_cell', 'ase_filter', 'save_path', 'crystal_feas_save_path']        
184        for p in self.input.keys():
185            v = self.input.get(p)
186            if p in allowed_params and not v == None:
187                if type(v) == str:
188                    params.append(p + "=\"" + v + "\"")
189                else:
190                    params.append(f'{p}={self.input.get(p)}')
191        params = ','.join(params)
192        fix_imports = ''
193        fixes = []
194        if 'fix_imports' in self.input.keys():
195            fix_imports = self.input.get('fix_imports')
196        if 'fixes' in self.input.keys():
197            f = self.input.get('fixes')
198            if isinstance(f, str):                
199                fixes = [self.input.get('fixes')]
200            elif isinstance(f, list):
201                fixes = f
202            else:
203                self.logger.warning('Fixes are neither string or array of strings. Ignoring')         
204        script = [
205            'from chgnet.model import CHGNet, StructOptimizer',
206            'from ase.io import read',
207            'from ase.io.cif import write_cif',
208            'from ase.io.trajectory import Trajectory',
209            'from pymatgen.io.ase import AseAtomsAdaptor',
210            'chgnet = CHGNet.load()',
211            fix_imports,
212            'struct = read("structure.cif")']
213        for f in fixes:
214            script.append(f'struct.set_constraint({f})')
215        script += [            
216            'relaxer = StructOptimizer(' + model_params + ')',
217            'result = relaxer.relax(struct,' + params + ')',
218            'final_structure = result["final_structure"]',
219            'ase_structure = AseAtomsAdaptor.get_atoms(final_structure)',
220            'write_cif("final_structure.cif", ase_structure)'
221        ]
222        with open(join(self.working_directory, 'calc_script.py'), 'w') as f:
223            f.writelines("\n".join(script))
224            
225    def calc_md(
226        self,
227        temperature=300,
228        pressure=None,
229        n_ionic_steps=100,
230        time_step=2.0,  # in femto-seconds
231        n_print=10,
232        temperature_damping_timescale=100.0, # taut: float | None = None,        
233        pressure_damping_timescale=None, # taup: float | None = None,        
234        trajectory="md_run.traj",
235        logfile="md_run.log",        
236        on_isolated_atoms: Literal["ignore", "warn", "error"] = "warn",
237        starting_temperature: int | None = None,
238        ensemble: str = "nvt",
239    ):        
240        """
241        Set up and perform a molecular dynamics (MD) simulation. More CHGNet parameters can be specified by 
242        explicitly adding them to `job.input` dictionary, see
243        [https://chgnet.lbl.gov/api#class-moleculardynamics](https://chgnet.lbl.gov/api#class-moleculardynamics).
244
245        Parameters
246        ----------
247        temperature : float, optional
248            Target temperature of the simulation in Kelvin (default is 300 K).
249        pressure : float or None, optional
250            Target pressure for the simulation in bar (default is None).
251        n_ionic_steps : int, optional
252            Number of ionic steps to simulate (default is 100).
253        time_step : float, optional
254            Time step for the simulation in femtoseconds (default is 2.0 fs).
255        n_print : int, optional
256            Frequency of printing simulation logs (default is 10).
257        temperature_damping_timescale : float, optional
258            Timescale for temperature damping in femtoseconds (default is 100.0 fs).
259        pressure_damping_timescale : float or None, optional
260            Timescale for pressure damping in femtoseconds (default is None).
261        trajectory : str, optional
262            Name of the output trajectory file (default is 'md_run.traj').
263        logfile : str, optional
264            Name of the output log file (default is 'md_run.log').
265        on_isolated_atoms : {'ignore', 'warn', 'error'}, optional
266            Behavior for isolated atoms (default is 'warn').
267        starting_temperature : int or None, optional
268            Initial temperature of the simulation (default is the target temperature).
269        ensemble : str, optional
270            Ensemble type, e.g., 'nvt' or 'npt' (default is 'nvt').
271        """
272        super().calc_md(
273            temperature=temperature,
274            pressure=pressure,
275            n_ionic_steps=n_ionic_steps,
276            time_step=time_step,
277            n_print=n_print,
278            temperature_damping_timescale=temperature_damping_timescale,
279            pressure_damping_timescale=pressure_damping_timescale,            
280        )
281        self.input['ensemble'] = ensemble
282        self.input['trajectory'] = trajectory
283        self.input['logfile'] = logfile
284        self.input['on_isolated_atoms'] = on_isolated_atoms
285        if starting_temperature==None:
286            starting_temperature=temperature
287        self.input['starting_temperature'] = starting_temperature
288
289    def _write_calc_md(self):        
290        """
291        Write the calculation script for performing molecular dynamics (MD).
292
293        This method generates a Python script that uses CHGNet for running MD
294        simulations and writes it to a file in the working directory.
295        """
296        params = []
297        params += [f"temperature={self._generic_input['temperature']},pressure={self._generic_input['pressure']},timestep={self._generic_input['time_step']}"]
298        params += [f"loginterval={self._generic_input['n_print']},taut={self._generic_input['temperature_damping_timescale']},taup={self._generic_input['pressure_damping_timescale']}"]
299        allowed_params = ['ensemble', 'thermostat', 'starting_temperature', 'bulk_modulus', 'crystal_feas_logfile', 'append_trajectory', 'use_device', 'trajectory', 'logfile', 'on_isolated_atoms']                   
300        for p in self.input.keys():
301            v = self.input.get(p)
302            if p in allowed_params and not v==None:
303                if type(v)==str:
304                    params.append(p+"=\""+v+"\"")
305                else:
306                    params.append(f'{p}={self.input.get(p)}')
307        params = ','.join(params)
308        fix_imports = ''
309        fixes = []
310        if 'fix_imports' in self.input.keys():
311            fix_imports = self.input.get('fix_imports')
312        if 'fixes' in self.input.keys():
313            f = self.input.get('fixes')
314            if isinstance(f, str):                
315                fixes = [self.input.get('fixes')]
316            elif isinstance(f, list):
317                fixes = f
318            else:
319                self.logger.warning('Fixes are neither string or array of strings. Ignoring') 
320                    
321        script = [
322            'from chgnet.model import CHGNet, MolecularDynamics',
323            'chgnet = CHGNet.load()',
324            'from ase.io import read',
325            'from ase.io.cif import write_cif',
326            fix_imports,
327            'struct = read("structure.cif")']
328        for f in fixes:
329            script.append(f'struct.set_constraint({f})')
330        script += [
331            'md = MolecularDynamics(atoms=struct,model=chgnet,'+params+')',
332            f'md.run({self._generic_input["n_ionic_steps"]})',            
333            'traj = read(f"md_run.traj", index=":")',
334            'write_cif("final_structure.cif", traj[-1])'
335        ]
336        with open(join(self.working_directory, 'calc_script.py'), 'w') as f:
337            f.writelines("\n".join(script))
338
339    def write_input(self) -> None:
340        """
341        Write input files required for the CHGNet calculation.
342
343        This function writes the structure file (`structure.cif`), stores the input 
344        dictionary to the HDF5 format, and delegates further writing depending on the 
345        calculation mode (MD or static/minimize).
346
347        Returns
348        -------
349        None
350        """
351        write(join(self.working_directory, 'structure.cif'), pyiron_to_ase(self.structure))
352        self.input.to_hdf(self.project_hdf5, group_name='input')
353        if self._generic_input['calc_mode'] == 'md':
354            self._write_calc_md()
355        else:
356            # calc_mode = static is treated as minimize with only a single ioninc step
357            self._write_calc_minimize()
358
359
360    def _parse_calc_static(self, output: str = 'error.out', skip: int = 3) -> Optional[pd.DataFrame]:
361        """
362        Parse the output of a static calculation.
363
364        This function reads the static calculation output file and returns the 
365        corresponding DataFrame containing the optimization step and energy information.
366
367        Parameters
368        ----------
369        output : str, optional
370            The name of the output file to parse, by default 'error.out'.
371        skip : int, optional
372            The number of rows to skip at the start of the file, by default 3.
373
374        Returns
375        -------
376        pd.DataFrame or None
377            A DataFrame containing the optimization results, or None if parsing fails.
378        """
379        try:
380            header = pd.read_csv(
381                join(self.working_directory, output), 
382                skiprows=skip, 
383                nrows=0, 
384                sep=r'\s+'
385            )
386            df = pd.read_csv(
387                join(self.working_directory, output), 
388                skiprows=skip+1, 
389                sep=r'\s+',
390                header=None
391            )
392            df.columns = ['optimizer_class'] + list(header.columns[:])
393            return df
394        except:
395            self.logger.warning(f'Cannot parse output from {output} file.')
396            return None
397
398
399    def _parse_calc_md(self, output: str = 'md_run.log') -> Optional[pd.DataFrame]:
400        """
401        Parse the output of a molecular dynamics (MD) simulation.
402
403        This function reads the MD log file and returns the corresponding DataFrame 
404        containing time, total energy, potential energy, kinetic energy, and temperature.
405
406        Parameters
407        ----------
408        output : str, optional
409            The name of the MD log file to parse, by default 'md_run.log'.
410
411        Returns
412        -------
413        pd.DataFrame or None
414            A DataFrame containing the MD simulation results, or None if parsing fails.
415        """
416        try:            
417            df = pd.read_csv(
418                join(self.working_directory, output), 
419                skiprows=1, 
420                sep=r'\s+'
421            )
422            df.columns = ['Time', 'Etot', 'Epot', 'Ekin', 'T']
423            return df
424        except:
425            self.logger.warning(f'Cannot parse output from {output} file.')
426            return None
427
428
429    def collect_output(self) -> None:
430        """
431        Collect and store the results of the calculation.
432
433        This function parses the final structure, trajectory, and other output properties,
434        and stores them in the HDF5 format. The structure is updated, and various properties 
435        such as energies, forces, and magnetization are saved to the output group.
436
437        Returns
438        -------
439        None
440        """
441                    
442        relaxed_ase_structure = read(join(self.working_directory, 'final_structure.cif'), format='cif')
443        relaxed_pyiron_structure = ase_to_pyiron(relaxed_ase_structure)
444
445        # Store the relaxed structure in the HDF5 group
446        with self.project_hdf5.open('output') as h5out:
447            h5out['structure'] = relaxed_pyiron_structure
448        self.structure = relaxed_pyiron_structure  # Update the job's structure
449
450        # Parse trajectory        
451        if self._generic_input['calc_mode'] in ['static', 'minimize']:
452            traj_file = self.input['save_path']
453            try:
454                with open(join(self.working_directory, traj_file), 'rb') as file:
455                    traj = pickle.load(file)                   
456                with self.project_hdf5.open('output/generic') as h5out:
457                    h5out['cells'] = np.array(traj['cell'][:-1])
458                    h5out['positions'] = np.array(traj['atom_positions'][:-1])
459                    h5out['stresses'] = np.array([[[s[0],s[5],s[4]],[s[5],s[1],s[3]],[s[4],s[3],s[2]]] for s in traj['stresses'][:-1]])
460                    h5out['forces'] = np.array([f for f in traj['forces'][:-1]])
461                with self.project_hdf5.open('output/dft') as h5out:
462                    h5out['magnetization'] = np.array([f for f in traj['magmoms'][:-1]])
463            except:
464                self.logger.warning(f'Cannot parse output from {traj_file} file.')
465        elif self._generic_input['calc_mode'] == 'md':            
466            traj_file = self.input['trajectory']
467            try:
468                traj = read(join(self.working_directory, traj_file), index=":")
469                with self.project_hdf5.open('output/generic') as h5out:
470                    h5out['cells'] = np.array([s.get_cell() for s in traj])
471                    h5out['positions'] = np.array([s.positions for s in traj])
472                    h5out['stresses'] = np.array([s.get_stress() for s in traj])
473                    h5out['forces'] = np.array([s.get_forces() for s in traj])
474            except:
475                self.logger.warning(f'Cannot parse output from {traj_file} file.')
476
477        # Parse other properties
478        if self._generic_input['calc_mode'] in ['static', 'minimize']:            
479            df = self._parse_calc_static()
480            with self.project_hdf5.open('output/generic') as h5out:
481                h5out['energy_pot'] = df['Energy'].values
482                h5out['energy_tot'] = df['Energy'].values
483                h5out['max_force'] = df['fmax'].values
484                h5out['steps'] = df['Step'].values
485        if self._generic_input['calc_mode'] == 'md':
486            df = self._parse_calc_md(self.input['logfile'])
487            with self.project_hdf5.open('output/generic') as h5out:
488                h5out['energy_pot'] = df['Epot'].values
489                h5out['energy_tot'] = df['Etot'].values                
490                h5out['energy_kin'] = df['Ekin'].values
491                h5out['temperature'] = df['T'].values
492                h5out['time'] = df['Time'].values
493                h5out['steps'] = df.index.values
494
495        self.status.finished = True
496
497
498    def get_structure(self, frame: int = -1) -> Atoms:
499        """
500        Retrieve the structure at a given frame from the calculation output.
501
502        This function returns the atomic structure (positions, cell, species) at the 
503        specified frame index. The frame index is adjusted to ensure it is valid.
504
505        Parameters
506        ----------
507        frame : int, optional
508            The frame index to retrieve. If negative, it is counted from the last frame, 
509            by default -1.
510
511        Returns
512        -------
513        Atoms
514            The ASE Atoms object representing the structure at the given frame.
515
516        Raises
517        ------
518        ValueError
519            If no structures are found in the job output.
520        IndexError
521            If the frame index is out of range.
522        """
523        num_structures = self.number_of_structures
524        if num_structures == 0:
525            raise ValueError("No structures found in the job output.")
526
527        # Ensure frame is valid
528        if frame < 0:
529            frame += num_structures
530        if frame >= num_structures:
531            raise IndexError(
532                f"Frame {frame} is out of range for {num_structures} structures.")
533
534        with self.project_hdf5.open('output/generic') as h5out:
535            positions = h5out["positions"][frame]
536            cell = h5out["cells"][frame]            
537        # Read positions and cell
538        with self.project_hdf5.open('output/structure') as h5out:
539            species = h5out["species"]
540            indices = h5out["indices"]
541
542        # Generate symbols list
543        symbols = [species[i] for i in indices]
544        return ase_to_pyiron(Atoms(symbols=symbols, positions=positions, cell=cell, pbc=True))
545
546
547
548class CalcParams(GenericParameters):
549    """
550    Class to manage the calculation parameters for CHGNet calculations.
551
552    This class extends from `GenericParameters` and is used to define and manage 
553    parameters specific to CHGNet calculations. It allows for initialization of 
554    parameters and stores them in a table, with a default table name of "chgnet". 
555    The class can be used to load and store various CHGNet calculation settings and 
556    other related configurations.
557
558    Parameters
559    ----------
560    table_name : str, optional
561        The name of the table where the parameters are stored. By default, it is set 
562        to "chgnet", but it can be customized if necessary.
563    
564    Methods
565    -------
566    __init__(self, table_name="chgnet")
567        Initializes the parameters for the calculation.
568    """
569    
570    def __init__(self, table_name: str = "chgnet") -> None:
571        """
572        Initialize the CalcParams object with the given table name.
573
574        Parameters
575        ----------
576        table_name : str, optional
577            The name of the table where the parameters are stored. By default, 
578            it is set to "chgnet".
579        """
580        super(CalcParams, self).__init__(
581            table_name=table_name,
582        )
class CHGNet(pyiron_atomistics.atomistics.job.atomistic.AtomisticGenericJob):
 23class CHGNet(AtomisticGenericJob):
 24    """
 25    Custom job class for using CHGNet within the pyiron framework.
 26
 27    This class provides methods to perform static calculations, structure relaxation,
 28    and molecular dynamics simulations using CHGNet.
 29
 30    Attributes
 31    ----------
 32    structure : pyiron.atomistics.structure.atoms.Atoms
 33        Structure associated with the job.
 34    input : CalcParams
 35        Input parameters for the CHGNet calculations.
 36    """
 37
 38
 39    def __init__(self, project, job_name):
 40        """
 41        Initialize the CHGNet job.
 42
 43        Parameters
 44        ----------
 45        project : pyiron.Project
 46            The pyiron project in which the job resides.
 47        job_name : str
 48            Name of the job.
 49        """
 50        super(CHGNet, self).__init__(project, job_name)
 51        self.structure = None
 52        self.input = CalcParams()
 53        if self.project_hdf5.file_exists:
 54            self.input.from_hdf(self.project_hdf5, group_name='input')
 55        self._executable_activate(codename='chgnet')            
 56
 57    @property
 58    def fmax(self) -> float:
 59        """
 60        Get the maximum force tolerance for ionic relaxation.
 61
 62        Returns
 63        -------
 64        float
 65            Maximum force tolerance (eV/Å).
 66        """
 67        return self._generic_input['ionic_force_tolerance']
 68
 69    @fmax.setter
 70    def fmax(self, fmax: float):
 71        """
 72        Set the maximum force tolerance for ionic relaxation.
 73
 74        Parameters
 75        ----------
 76        fmax : float
 77            Maximum force tolerance (eV/Å).
 78        """
 79        self._generic_input['ionic_force_tolerance'] = fmax
 80
 81    @property
 82    def max_iter(self) -> int:
 83        """
 84        Get the maximum number of iterations for relaxation.
 85
 86        Returns
 87        -------
 88        int
 89            Maximum number of iterations.
 90        """
 91        return self._generic_input['n_print']
 92
 93    @max_iter.setter
 94    def max_iter(self, max_iter: int):
 95        """
 96        Set the maximum number of iterations for relaxation.
 97
 98        Parameters
 99        ----------
100        max_iter : int
101            Maximum number of iterations.
102        """
103        self._generic_input['n_print'] = max_iter
104
105        
106    def calc_static(
107        self,
108        on_isolated_atoms: Literal["ignore", "warn", "error"] = "warn",
109    ):        
110        """
111        Perform a static calculation.
112
113        Parameters
114        ----------
115        on_isolated_atoms : {'ignore', 'warn', 'error'}, optional
116            Behavior for isolated atoms (default is 'warn').
117        """
118        self.calc_minimize(max_iter=0, relax_cell=False, on_isolated_atoms=on_isolated_atoms)
119        self._generic_input["calc_mode"] = "static"   
120       
121    def calc_minimize(
122        self,
123        ionic_force_tolerance: float = 0.1,
124        max_iter: int = 500,
125        n_print: int = 1,
126        relax_cell: bool | None = True,
127        save_path: str | None = 'relax.traj',
128        on_isolated_atoms: Literal["ignore", "warn", "error"] = "warn",
129    ):
130        """
131        Perform a structure relaxation (geometry optimization). More CHGNet parameters can be specified by 
132        explicitly adding them to `job.input` dictionary, see
133        [https://chgnet.lbl.gov/api#class-structoptimizer](https://chgnet.lbl.gov/api#class-structoptimizer).
134
135        Parameters
136        ----------
137        ionic_force_tolerance : float, optional
138            Convergence criterion for forces (default is 0.1 eV/Å).
139        max_iter : int, optional
140            Maximum number of iterations (default is 500).
141        n_print : int, optional
142            Frequency of logging output (default is 1).
143        relax_cell : bool or None, optional
144            Whether to allow relaxation of the simulation cell (default is True).
145        save_path : str or None, optional
146            Path to save the trajectory file (default is 'relax.traj').
147        on_isolated_atoms : {'ignore', 'warn', 'error'}, optional
148            Behavior for isolated atoms (default is 'warn').
149        """
150        super().calc_minimize(
151            ionic_energy_tolerance=0,
152            ionic_force_tolerance=ionic_force_tolerance,
153            max_iter=max_iter,            
154        )       
155        self.input['relax_cell'] = relax_cell
156        self.input['save_path'] = save_path
157        self.input['verbose'] = True
158        self.input['assign_magmoms'] = True                          
159        self.input['on_isolated_atoms'] = on_isolated_atoms 
160        self._generic_input['n_print'] = n_print
161        self._generic_input['ionic_force_tolerance'] = ionic_force_tolerance        
162    
163    def _write_calc_minimize(self):
164        """
165        Write the calculation script for performing structure relaxation.
166
167        This method generates a Python script that uses CHGNet for structure
168        relaxation and writes it to a file in the working directory.
169        """
170        params = ['model=chgnet']
171        allowed_params = ['optimizer_class', 'use_device', 'on_isolated_atoms']
172        for p in self.input.keys():
173            v = self.input.get(p)
174            if p in allowed_params and not v == None:
175                if type(v) == str:
176                    params.append(p + "=\"" + v + "\"")
177                else:
178                    params.append(f'{p}={self.input.get(p)}')
179        model_params = ','.join(params)
180
181        params = []
182        params += [f"fmax={self._generic_input['ionic_force_tolerance']},steps={self._generic_input['max_iter']}"]
183        params += [f"loginterval={self._generic_input['n_print']}"]
184        allowed_params = ['relax_cell', 'ase_filter', 'save_path', 'crystal_feas_save_path']        
185        for p in self.input.keys():
186            v = self.input.get(p)
187            if p in allowed_params and not v == None:
188                if type(v) == str:
189                    params.append(p + "=\"" + v + "\"")
190                else:
191                    params.append(f'{p}={self.input.get(p)}')
192        params = ','.join(params)
193        fix_imports = ''
194        fixes = []
195        if 'fix_imports' in self.input.keys():
196            fix_imports = self.input.get('fix_imports')
197        if 'fixes' in self.input.keys():
198            f = self.input.get('fixes')
199            if isinstance(f, str):                
200                fixes = [self.input.get('fixes')]
201            elif isinstance(f, list):
202                fixes = f
203            else:
204                self.logger.warning('Fixes are neither string or array of strings. Ignoring')         
205        script = [
206            'from chgnet.model import CHGNet, StructOptimizer',
207            'from ase.io import read',
208            'from ase.io.cif import write_cif',
209            'from ase.io.trajectory import Trajectory',
210            'from pymatgen.io.ase import AseAtomsAdaptor',
211            'chgnet = CHGNet.load()',
212            fix_imports,
213            'struct = read("structure.cif")']
214        for f in fixes:
215            script.append(f'struct.set_constraint({f})')
216        script += [            
217            'relaxer = StructOptimizer(' + model_params + ')',
218            'result = relaxer.relax(struct,' + params + ')',
219            'final_structure = result["final_structure"]',
220            'ase_structure = AseAtomsAdaptor.get_atoms(final_structure)',
221            'write_cif("final_structure.cif", ase_structure)'
222        ]
223        with open(join(self.working_directory, 'calc_script.py'), 'w') as f:
224            f.writelines("\n".join(script))
225            
226    def calc_md(
227        self,
228        temperature=300,
229        pressure=None,
230        n_ionic_steps=100,
231        time_step=2.0,  # in femto-seconds
232        n_print=10,
233        temperature_damping_timescale=100.0, # taut: float | None = None,        
234        pressure_damping_timescale=None, # taup: float | None = None,        
235        trajectory="md_run.traj",
236        logfile="md_run.log",        
237        on_isolated_atoms: Literal["ignore", "warn", "error"] = "warn",
238        starting_temperature: int | None = None,
239        ensemble: str = "nvt",
240    ):        
241        """
242        Set up and perform a molecular dynamics (MD) simulation. More CHGNet parameters can be specified by 
243        explicitly adding them to `job.input` dictionary, see
244        [https://chgnet.lbl.gov/api#class-moleculardynamics](https://chgnet.lbl.gov/api#class-moleculardynamics).
245
246        Parameters
247        ----------
248        temperature : float, optional
249            Target temperature of the simulation in Kelvin (default is 300 K).
250        pressure : float or None, optional
251            Target pressure for the simulation in bar (default is None).
252        n_ionic_steps : int, optional
253            Number of ionic steps to simulate (default is 100).
254        time_step : float, optional
255            Time step for the simulation in femtoseconds (default is 2.0 fs).
256        n_print : int, optional
257            Frequency of printing simulation logs (default is 10).
258        temperature_damping_timescale : float, optional
259            Timescale for temperature damping in femtoseconds (default is 100.0 fs).
260        pressure_damping_timescale : float or None, optional
261            Timescale for pressure damping in femtoseconds (default is None).
262        trajectory : str, optional
263            Name of the output trajectory file (default is 'md_run.traj').
264        logfile : str, optional
265            Name of the output log file (default is 'md_run.log').
266        on_isolated_atoms : {'ignore', 'warn', 'error'}, optional
267            Behavior for isolated atoms (default is 'warn').
268        starting_temperature : int or None, optional
269            Initial temperature of the simulation (default is the target temperature).
270        ensemble : str, optional
271            Ensemble type, e.g., 'nvt' or 'npt' (default is 'nvt').
272        """
273        super().calc_md(
274            temperature=temperature,
275            pressure=pressure,
276            n_ionic_steps=n_ionic_steps,
277            time_step=time_step,
278            n_print=n_print,
279            temperature_damping_timescale=temperature_damping_timescale,
280            pressure_damping_timescale=pressure_damping_timescale,            
281        )
282        self.input['ensemble'] = ensemble
283        self.input['trajectory'] = trajectory
284        self.input['logfile'] = logfile
285        self.input['on_isolated_atoms'] = on_isolated_atoms
286        if starting_temperature==None:
287            starting_temperature=temperature
288        self.input['starting_temperature'] = starting_temperature
289
290    def _write_calc_md(self):        
291        """
292        Write the calculation script for performing molecular dynamics (MD).
293
294        This method generates a Python script that uses CHGNet for running MD
295        simulations and writes it to a file in the working directory.
296        """
297        params = []
298        params += [f"temperature={self._generic_input['temperature']},pressure={self._generic_input['pressure']},timestep={self._generic_input['time_step']}"]
299        params += [f"loginterval={self._generic_input['n_print']},taut={self._generic_input['temperature_damping_timescale']},taup={self._generic_input['pressure_damping_timescale']}"]
300        allowed_params = ['ensemble', 'thermostat', 'starting_temperature', 'bulk_modulus', 'crystal_feas_logfile', 'append_trajectory', 'use_device', 'trajectory', 'logfile', 'on_isolated_atoms']                   
301        for p in self.input.keys():
302            v = self.input.get(p)
303            if p in allowed_params and not v==None:
304                if type(v)==str:
305                    params.append(p+"=\""+v+"\"")
306                else:
307                    params.append(f'{p}={self.input.get(p)}')
308        params = ','.join(params)
309        fix_imports = ''
310        fixes = []
311        if 'fix_imports' in self.input.keys():
312            fix_imports = self.input.get('fix_imports')
313        if 'fixes' in self.input.keys():
314            f = self.input.get('fixes')
315            if isinstance(f, str):                
316                fixes = [self.input.get('fixes')]
317            elif isinstance(f, list):
318                fixes = f
319            else:
320                self.logger.warning('Fixes are neither string or array of strings. Ignoring') 
321                    
322        script = [
323            'from chgnet.model import CHGNet, MolecularDynamics',
324            'chgnet = CHGNet.load()',
325            'from ase.io import read',
326            'from ase.io.cif import write_cif',
327            fix_imports,
328            'struct = read("structure.cif")']
329        for f in fixes:
330            script.append(f'struct.set_constraint({f})')
331        script += [
332            'md = MolecularDynamics(atoms=struct,model=chgnet,'+params+')',
333            f'md.run({self._generic_input["n_ionic_steps"]})',            
334            'traj = read(f"md_run.traj", index=":")',
335            'write_cif("final_structure.cif", traj[-1])'
336        ]
337        with open(join(self.working_directory, 'calc_script.py'), 'w') as f:
338            f.writelines("\n".join(script))
339
340    def write_input(self) -> None:
341        """
342        Write input files required for the CHGNet calculation.
343
344        This function writes the structure file (`structure.cif`), stores the input 
345        dictionary to the HDF5 format, and delegates further writing depending on the 
346        calculation mode (MD or static/minimize).
347
348        Returns
349        -------
350        None
351        """
352        write(join(self.working_directory, 'structure.cif'), pyiron_to_ase(self.structure))
353        self.input.to_hdf(self.project_hdf5, group_name='input')
354        if self._generic_input['calc_mode'] == 'md':
355            self._write_calc_md()
356        else:
357            # calc_mode = static is treated as minimize with only a single ioninc step
358            self._write_calc_minimize()
359
360
361    def _parse_calc_static(self, output: str = 'error.out', skip: int = 3) -> Optional[pd.DataFrame]:
362        """
363        Parse the output of a static calculation.
364
365        This function reads the static calculation output file and returns the 
366        corresponding DataFrame containing the optimization step and energy information.
367
368        Parameters
369        ----------
370        output : str, optional
371            The name of the output file to parse, by default 'error.out'.
372        skip : int, optional
373            The number of rows to skip at the start of the file, by default 3.
374
375        Returns
376        -------
377        pd.DataFrame or None
378            A DataFrame containing the optimization results, or None if parsing fails.
379        """
380        try:
381            header = pd.read_csv(
382                join(self.working_directory, output), 
383                skiprows=skip, 
384                nrows=0, 
385                sep=r'\s+'
386            )
387            df = pd.read_csv(
388                join(self.working_directory, output), 
389                skiprows=skip+1, 
390                sep=r'\s+',
391                header=None
392            )
393            df.columns = ['optimizer_class'] + list(header.columns[:])
394            return df
395        except:
396            self.logger.warning(f'Cannot parse output from {output} file.')
397            return None
398
399
400    def _parse_calc_md(self, output: str = 'md_run.log') -> Optional[pd.DataFrame]:
401        """
402        Parse the output of a molecular dynamics (MD) simulation.
403
404        This function reads the MD log file and returns the corresponding DataFrame 
405        containing time, total energy, potential energy, kinetic energy, and temperature.
406
407        Parameters
408        ----------
409        output : str, optional
410            The name of the MD log file to parse, by default 'md_run.log'.
411
412        Returns
413        -------
414        pd.DataFrame or None
415            A DataFrame containing the MD simulation results, or None if parsing fails.
416        """
417        try:            
418            df = pd.read_csv(
419                join(self.working_directory, output), 
420                skiprows=1, 
421                sep=r'\s+'
422            )
423            df.columns = ['Time', 'Etot', 'Epot', 'Ekin', 'T']
424            return df
425        except:
426            self.logger.warning(f'Cannot parse output from {output} file.')
427            return None
428
429
430    def collect_output(self) -> None:
431        """
432        Collect and store the results of the calculation.
433
434        This function parses the final structure, trajectory, and other output properties,
435        and stores them in the HDF5 format. The structure is updated, and various properties 
436        such as energies, forces, and magnetization are saved to the output group.
437
438        Returns
439        -------
440        None
441        """
442                    
443        relaxed_ase_structure = read(join(self.working_directory, 'final_structure.cif'), format='cif')
444        relaxed_pyiron_structure = ase_to_pyiron(relaxed_ase_structure)
445
446        # Store the relaxed structure in the HDF5 group
447        with self.project_hdf5.open('output') as h5out:
448            h5out['structure'] = relaxed_pyiron_structure
449        self.structure = relaxed_pyiron_structure  # Update the job's structure
450
451        # Parse trajectory        
452        if self._generic_input['calc_mode'] in ['static', 'minimize']:
453            traj_file = self.input['save_path']
454            try:
455                with open(join(self.working_directory, traj_file), 'rb') as file:
456                    traj = pickle.load(file)                   
457                with self.project_hdf5.open('output/generic') as h5out:
458                    h5out['cells'] = np.array(traj['cell'][:-1])
459                    h5out['positions'] = np.array(traj['atom_positions'][:-1])
460                    h5out['stresses'] = np.array([[[s[0],s[5],s[4]],[s[5],s[1],s[3]],[s[4],s[3],s[2]]] for s in traj['stresses'][:-1]])
461                    h5out['forces'] = np.array([f for f in traj['forces'][:-1]])
462                with self.project_hdf5.open('output/dft') as h5out:
463                    h5out['magnetization'] = np.array([f for f in traj['magmoms'][:-1]])
464            except:
465                self.logger.warning(f'Cannot parse output from {traj_file} file.')
466        elif self._generic_input['calc_mode'] == 'md':            
467            traj_file = self.input['trajectory']
468            try:
469                traj = read(join(self.working_directory, traj_file), index=":")
470                with self.project_hdf5.open('output/generic') as h5out:
471                    h5out['cells'] = np.array([s.get_cell() for s in traj])
472                    h5out['positions'] = np.array([s.positions for s in traj])
473                    h5out['stresses'] = np.array([s.get_stress() for s in traj])
474                    h5out['forces'] = np.array([s.get_forces() for s in traj])
475            except:
476                self.logger.warning(f'Cannot parse output from {traj_file} file.')
477
478        # Parse other properties
479        if self._generic_input['calc_mode'] in ['static', 'minimize']:            
480            df = self._parse_calc_static()
481            with self.project_hdf5.open('output/generic') as h5out:
482                h5out['energy_pot'] = df['Energy'].values
483                h5out['energy_tot'] = df['Energy'].values
484                h5out['max_force'] = df['fmax'].values
485                h5out['steps'] = df['Step'].values
486        if self._generic_input['calc_mode'] == 'md':
487            df = self._parse_calc_md(self.input['logfile'])
488            with self.project_hdf5.open('output/generic') as h5out:
489                h5out['energy_pot'] = df['Epot'].values
490                h5out['energy_tot'] = df['Etot'].values                
491                h5out['energy_kin'] = df['Ekin'].values
492                h5out['temperature'] = df['T'].values
493                h5out['time'] = df['Time'].values
494                h5out['steps'] = df.index.values
495
496        self.status.finished = True
497
498
499    def get_structure(self, frame: int = -1) -> Atoms:
500        """
501        Retrieve the structure at a given frame from the calculation output.
502
503        This function returns the atomic structure (positions, cell, species) at the 
504        specified frame index. The frame index is adjusted to ensure it is valid.
505
506        Parameters
507        ----------
508        frame : int, optional
509            The frame index to retrieve. If negative, it is counted from the last frame, 
510            by default -1.
511
512        Returns
513        -------
514        Atoms
515            The ASE Atoms object representing the structure at the given frame.
516
517        Raises
518        ------
519        ValueError
520            If no structures are found in the job output.
521        IndexError
522            If the frame index is out of range.
523        """
524        num_structures = self.number_of_structures
525        if num_structures == 0:
526            raise ValueError("No structures found in the job output.")
527
528        # Ensure frame is valid
529        if frame < 0:
530            frame += num_structures
531        if frame >= num_structures:
532            raise IndexError(
533                f"Frame {frame} is out of range for {num_structures} structures.")
534
535        with self.project_hdf5.open('output/generic') as h5out:
536            positions = h5out["positions"][frame]
537            cell = h5out["cells"][frame]            
538        # Read positions and cell
539        with self.project_hdf5.open('output/structure') as h5out:
540            species = h5out["species"]
541            indices = h5out["indices"]
542
543        # Generate symbols list
544        symbols = [species[i] for i in indices]
545        return ase_to_pyiron(Atoms(symbols=symbols, positions=positions, cell=cell, pbc=True))

Custom job class for using CHGNet within the pyiron framework.

This class provides methods to perform static calculations, structure relaxation, and molecular dynamics simulations using CHGNet.

Attributes
  • structure (pyiron.atomistics.structure.atoms.Atoms): Structure associated with the job.
  • input (CalcParams): Input parameters for the CHGNet calculations.
CHGNet(project, job_name)
39    def __init__(self, project, job_name):
40        """
41        Initialize the CHGNet job.
42
43        Parameters
44        ----------
45        project : pyiron.Project
46            The pyiron project in which the job resides.
47        job_name : str
48            Name of the job.
49        """
50        super(CHGNet, self).__init__(project, job_name)
51        self.structure = None
52        self.input = CalcParams()
53        if self.project_hdf5.file_exists:
54            self.input.from_hdf(self.project_hdf5, group_name='input')
55        self._executable_activate(codename='chgnet')            

Initialize the CHGNet job.

Parameters
  • project (pyiron.Project): The pyiron project in which the job resides.
  • job_name (str): Name of the job.
structure
147    @property
148    def structure(self):
149        """
150
151        Returns:
152
153        """
154        return self._structure

Returns:

input
fmax: float
57    @property
58    def fmax(self) -> float:
59        """
60        Get the maximum force tolerance for ionic relaxation.
61
62        Returns
63        -------
64        float
65            Maximum force tolerance (eV/Å).
66        """
67        return self._generic_input['ionic_force_tolerance']

Get the maximum force tolerance for ionic relaxation.

Returns
  • float: Maximum force tolerance (eV/Å).
max_iter: int
81    @property
82    def max_iter(self) -> int:
83        """
84        Get the maximum number of iterations for relaxation.
85
86        Returns
87        -------
88        int
89            Maximum number of iterations.
90        """
91        return self._generic_input['n_print']

Get the maximum number of iterations for relaxation.

Returns
  • int: Maximum number of iterations.
def calc_static(self, on_isolated_atoms: Literal['ignore', 'warn', 'error'] = 'warn'):
106    def calc_static(
107        self,
108        on_isolated_atoms: Literal["ignore", "warn", "error"] = "warn",
109    ):        
110        """
111        Perform a static calculation.
112
113        Parameters
114        ----------
115        on_isolated_atoms : {'ignore', 'warn', 'error'}, optional
116            Behavior for isolated atoms (default is 'warn').
117        """
118        self.calc_minimize(max_iter=0, relax_cell=False, on_isolated_atoms=on_isolated_atoms)
119        self._generic_input["calc_mode"] = "static"   

Perform a static calculation.

Parameters
  • on_isolated_atoms ({'ignore', 'warn', 'error'}, optional): Behavior for isolated atoms (default is 'warn').
def calc_minimize( self, ionic_force_tolerance: float = 0.1, max_iter: int = 500, n_print: int = 1, relax_cell: bool | None = True, save_path: str | None = 'relax.traj', on_isolated_atoms: Literal['ignore', 'warn', 'error'] = 'warn'):
121    def calc_minimize(
122        self,
123        ionic_force_tolerance: float = 0.1,
124        max_iter: int = 500,
125        n_print: int = 1,
126        relax_cell: bool | None = True,
127        save_path: str | None = 'relax.traj',
128        on_isolated_atoms: Literal["ignore", "warn", "error"] = "warn",
129    ):
130        """
131        Perform a structure relaxation (geometry optimization). More CHGNet parameters can be specified by 
132        explicitly adding them to `job.input` dictionary, see
133        [https://chgnet.lbl.gov/api#class-structoptimizer](https://chgnet.lbl.gov/api#class-structoptimizer).
134
135        Parameters
136        ----------
137        ionic_force_tolerance : float, optional
138            Convergence criterion for forces (default is 0.1 eV/Å).
139        max_iter : int, optional
140            Maximum number of iterations (default is 500).
141        n_print : int, optional
142            Frequency of logging output (default is 1).
143        relax_cell : bool or None, optional
144            Whether to allow relaxation of the simulation cell (default is True).
145        save_path : str or None, optional
146            Path to save the trajectory file (default is 'relax.traj').
147        on_isolated_atoms : {'ignore', 'warn', 'error'}, optional
148            Behavior for isolated atoms (default is 'warn').
149        """
150        super().calc_minimize(
151            ionic_energy_tolerance=0,
152            ionic_force_tolerance=ionic_force_tolerance,
153            max_iter=max_iter,            
154        )       
155        self.input['relax_cell'] = relax_cell
156        self.input['save_path'] = save_path
157        self.input['verbose'] = True
158        self.input['assign_magmoms'] = True                          
159        self.input['on_isolated_atoms'] = on_isolated_atoms 
160        self._generic_input['n_print'] = n_print
161        self._generic_input['ionic_force_tolerance'] = ionic_force_tolerance        

Perform a structure relaxation (geometry optimization). More CHGNet parameters can be specified by explicitly adding them to job.input dictionary, see https://chgnet.lbl.gov/api#class-structoptimizer.

Parameters
  • ionic_force_tolerance (float, optional): Convergence criterion for forces (default is 0.1 eV/Å).
  • max_iter (int, optional): Maximum number of iterations (default is 500).
  • n_print (int, optional): Frequency of logging output (default is 1).
  • relax_cell (bool or None, optional): Whether to allow relaxation of the simulation cell (default is True).
  • save_path (str or None, optional): Path to save the trajectory file (default is 'relax.traj').
  • on_isolated_atoms ({'ignore', 'warn', 'error'}, optional): Behavior for isolated atoms (default is 'warn').
def calc_md( self, temperature=300, pressure=None, n_ionic_steps=100, time_step=2.0, n_print=10, temperature_damping_timescale=100.0, pressure_damping_timescale=None, trajectory='md_run.traj', logfile='md_run.log', on_isolated_atoms: Literal['ignore', 'warn', 'error'] = 'warn', starting_temperature: int | None = None, ensemble: str = 'nvt'):
226    def calc_md(
227        self,
228        temperature=300,
229        pressure=None,
230        n_ionic_steps=100,
231        time_step=2.0,  # in femto-seconds
232        n_print=10,
233        temperature_damping_timescale=100.0, # taut: float | None = None,        
234        pressure_damping_timescale=None, # taup: float | None = None,        
235        trajectory="md_run.traj",
236        logfile="md_run.log",        
237        on_isolated_atoms: Literal["ignore", "warn", "error"] = "warn",
238        starting_temperature: int | None = None,
239        ensemble: str = "nvt",
240    ):        
241        """
242        Set up and perform a molecular dynamics (MD) simulation. More CHGNet parameters can be specified by 
243        explicitly adding them to `job.input` dictionary, see
244        [https://chgnet.lbl.gov/api#class-moleculardynamics](https://chgnet.lbl.gov/api#class-moleculardynamics).
245
246        Parameters
247        ----------
248        temperature : float, optional
249            Target temperature of the simulation in Kelvin (default is 300 K).
250        pressure : float or None, optional
251            Target pressure for the simulation in bar (default is None).
252        n_ionic_steps : int, optional
253            Number of ionic steps to simulate (default is 100).
254        time_step : float, optional
255            Time step for the simulation in femtoseconds (default is 2.0 fs).
256        n_print : int, optional
257            Frequency of printing simulation logs (default is 10).
258        temperature_damping_timescale : float, optional
259            Timescale for temperature damping in femtoseconds (default is 100.0 fs).
260        pressure_damping_timescale : float or None, optional
261            Timescale for pressure damping in femtoseconds (default is None).
262        trajectory : str, optional
263            Name of the output trajectory file (default is 'md_run.traj').
264        logfile : str, optional
265            Name of the output log file (default is 'md_run.log').
266        on_isolated_atoms : {'ignore', 'warn', 'error'}, optional
267            Behavior for isolated atoms (default is 'warn').
268        starting_temperature : int or None, optional
269            Initial temperature of the simulation (default is the target temperature).
270        ensemble : str, optional
271            Ensemble type, e.g., 'nvt' or 'npt' (default is 'nvt').
272        """
273        super().calc_md(
274            temperature=temperature,
275            pressure=pressure,
276            n_ionic_steps=n_ionic_steps,
277            time_step=time_step,
278            n_print=n_print,
279            temperature_damping_timescale=temperature_damping_timescale,
280            pressure_damping_timescale=pressure_damping_timescale,            
281        )
282        self.input['ensemble'] = ensemble
283        self.input['trajectory'] = trajectory
284        self.input['logfile'] = logfile
285        self.input['on_isolated_atoms'] = on_isolated_atoms
286        if starting_temperature==None:
287            starting_temperature=temperature
288        self.input['starting_temperature'] = starting_temperature

Set up and perform a molecular dynamics (MD) simulation. More CHGNet parameters can be specified by explicitly adding them to job.input dictionary, see https://chgnet.lbl.gov/api#class-moleculardynamics.

Parameters
  • temperature (float, optional): Target temperature of the simulation in Kelvin (default is 300 K).
  • pressure (float or None, optional): Target pressure for the simulation in bar (default is None).
  • n_ionic_steps (int, optional): Number of ionic steps to simulate (default is 100).
  • time_step (float, optional): Time step for the simulation in femtoseconds (default is 2.0 fs).
  • n_print (int, optional): Frequency of printing simulation logs (default is 10).
  • temperature_damping_timescale (float, optional): Timescale for temperature damping in femtoseconds (default is 100.0 fs).
  • pressure_damping_timescale (float or None, optional): Timescale for pressure damping in femtoseconds (default is None).
  • trajectory (str, optional): Name of the output trajectory file (default is 'md_run.traj').
  • logfile (str, optional): Name of the output log file (default is 'md_run.log').
  • on_isolated_atoms ({'ignore', 'warn', 'error'}, optional): Behavior for isolated atoms (default is 'warn').
  • starting_temperature (int or None, optional): Initial temperature of the simulation (default is the target temperature).
  • ensemble (str, optional): Ensemble type, e.g., 'nvt' or 'npt' (default is 'nvt').
def write_input(self) -> None:
340    def write_input(self) -> None:
341        """
342        Write input files required for the CHGNet calculation.
343
344        This function writes the structure file (`structure.cif`), stores the input 
345        dictionary to the HDF5 format, and delegates further writing depending on the 
346        calculation mode (MD or static/minimize).
347
348        Returns
349        -------
350        None
351        """
352        write(join(self.working_directory, 'structure.cif'), pyiron_to_ase(self.structure))
353        self.input.to_hdf(self.project_hdf5, group_name='input')
354        if self._generic_input['calc_mode'] == 'md':
355            self._write_calc_md()
356        else:
357            # calc_mode = static is treated as minimize with only a single ioninc step
358            self._write_calc_minimize()

Write input files required for the CHGNet calculation.

This function writes the structure file (structure.cif), stores the input dictionary to the HDF5 format, and delegates further writing depending on the calculation mode (MD or static/minimize).

Returns
  • None
def collect_output(self) -> None:
430    def collect_output(self) -> None:
431        """
432        Collect and store the results of the calculation.
433
434        This function parses the final structure, trajectory, and other output properties,
435        and stores them in the HDF5 format. The structure is updated, and various properties 
436        such as energies, forces, and magnetization are saved to the output group.
437
438        Returns
439        -------
440        None
441        """
442                    
443        relaxed_ase_structure = read(join(self.working_directory, 'final_structure.cif'), format='cif')
444        relaxed_pyiron_structure = ase_to_pyiron(relaxed_ase_structure)
445
446        # Store the relaxed structure in the HDF5 group
447        with self.project_hdf5.open('output') as h5out:
448            h5out['structure'] = relaxed_pyiron_structure
449        self.structure = relaxed_pyiron_structure  # Update the job's structure
450
451        # Parse trajectory        
452        if self._generic_input['calc_mode'] in ['static', 'minimize']:
453            traj_file = self.input['save_path']
454            try:
455                with open(join(self.working_directory, traj_file), 'rb') as file:
456                    traj = pickle.load(file)                   
457                with self.project_hdf5.open('output/generic') as h5out:
458                    h5out['cells'] = np.array(traj['cell'][:-1])
459                    h5out['positions'] = np.array(traj['atom_positions'][:-1])
460                    h5out['stresses'] = np.array([[[s[0],s[5],s[4]],[s[5],s[1],s[3]],[s[4],s[3],s[2]]] for s in traj['stresses'][:-1]])
461                    h5out['forces'] = np.array([f for f in traj['forces'][:-1]])
462                with self.project_hdf5.open('output/dft') as h5out:
463                    h5out['magnetization'] = np.array([f for f in traj['magmoms'][:-1]])
464            except:
465                self.logger.warning(f'Cannot parse output from {traj_file} file.')
466        elif self._generic_input['calc_mode'] == 'md':            
467            traj_file = self.input['trajectory']
468            try:
469                traj = read(join(self.working_directory, traj_file), index=":")
470                with self.project_hdf5.open('output/generic') as h5out:
471                    h5out['cells'] = np.array([s.get_cell() for s in traj])
472                    h5out['positions'] = np.array([s.positions for s in traj])
473                    h5out['stresses'] = np.array([s.get_stress() for s in traj])
474                    h5out['forces'] = np.array([s.get_forces() for s in traj])
475            except:
476                self.logger.warning(f'Cannot parse output from {traj_file} file.')
477
478        # Parse other properties
479        if self._generic_input['calc_mode'] in ['static', 'minimize']:            
480            df = self._parse_calc_static()
481            with self.project_hdf5.open('output/generic') as h5out:
482                h5out['energy_pot'] = df['Energy'].values
483                h5out['energy_tot'] = df['Energy'].values
484                h5out['max_force'] = df['fmax'].values
485                h5out['steps'] = df['Step'].values
486        if self._generic_input['calc_mode'] == 'md':
487            df = self._parse_calc_md(self.input['logfile'])
488            with self.project_hdf5.open('output/generic') as h5out:
489                h5out['energy_pot'] = df['Epot'].values
490                h5out['energy_tot'] = df['Etot'].values                
491                h5out['energy_kin'] = df['Ekin'].values
492                h5out['temperature'] = df['T'].values
493                h5out['time'] = df['Time'].values
494                h5out['steps'] = df.index.values
495
496        self.status.finished = True

Collect and store the results of the calculation.

This function parses the final structure, trajectory, and other output properties, and stores them in the HDF5 format. The structure is updated, and various properties such as energies, forces, and magnetization are saved to the output group.

Returns
  • None
def get_structure(self, frame: int = -1) -> ase.atoms.Atoms:
499    def get_structure(self, frame: int = -1) -> Atoms:
500        """
501        Retrieve the structure at a given frame from the calculation output.
502
503        This function returns the atomic structure (positions, cell, species) at the 
504        specified frame index. The frame index is adjusted to ensure it is valid.
505
506        Parameters
507        ----------
508        frame : int, optional
509            The frame index to retrieve. If negative, it is counted from the last frame, 
510            by default -1.
511
512        Returns
513        -------
514        Atoms
515            The ASE Atoms object representing the structure at the given frame.
516
517        Raises
518        ------
519        ValueError
520            If no structures are found in the job output.
521        IndexError
522            If the frame index is out of range.
523        """
524        num_structures = self.number_of_structures
525        if num_structures == 0:
526            raise ValueError("No structures found in the job output.")
527
528        # Ensure frame is valid
529        if frame < 0:
530            frame += num_structures
531        if frame >= num_structures:
532            raise IndexError(
533                f"Frame {frame} is out of range for {num_structures} structures.")
534
535        with self.project_hdf5.open('output/generic') as h5out:
536            positions = h5out["positions"][frame]
537            cell = h5out["cells"][frame]            
538        # Read positions and cell
539        with self.project_hdf5.open('output/structure') as h5out:
540            species = h5out["species"]
541            indices = h5out["indices"]
542
543        # Generate symbols list
544        symbols = [species[i] for i in indices]
545        return ase_to_pyiron(Atoms(symbols=symbols, positions=positions, cell=cell, pbc=True))

Retrieve the structure at a given frame from the calculation output.

This function returns the atomic structure (positions, cell, species) at the specified frame index. The frame index is adjusted to ensure it is valid.

Parameters
  • frame (int, optional): The frame index to retrieve. If negative, it is counted from the last frame, by default -1.
Returns
  • Atoms: The ASE Atoms object representing the structure at the given frame.
Raises
  • ValueError: If no structures are found in the job output.
  • IndexError: If the frame index is out of range.
class CalcParams(pyiron_base.storage.parameters.GenericParameters):
549class CalcParams(GenericParameters):
550    """
551    Class to manage the calculation parameters for CHGNet calculations.
552
553    This class extends from `GenericParameters` and is used to define and manage 
554    parameters specific to CHGNet calculations. It allows for initialization of 
555    parameters and stores them in a table, with a default table name of "chgnet". 
556    The class can be used to load and store various CHGNet calculation settings and 
557    other related configurations.
558
559    Parameters
560    ----------
561    table_name : str, optional
562        The name of the table where the parameters are stored. By default, it is set 
563        to "chgnet", but it can be customized if necessary.
564    
565    Methods
566    -------
567    __init__(self, table_name="chgnet")
568        Initializes the parameters for the calculation.
569    """
570    
571    def __init__(self, table_name: str = "chgnet") -> None:
572        """
573        Initialize the CalcParams object with the given table name.
574
575        Parameters
576        ----------
577        table_name : str, optional
578            The name of the table where the parameters are stored. By default, 
579            it is set to "chgnet".
580        """
581        super(CalcParams, self).__init__(
582            table_name=table_name,
583        )

Class to manage the calculation parameters for CHGNet calculations.

This class extends from GenericParameters and is used to define and manage parameters specific to CHGNet calculations. It allows for initialization of parameters and stores them in a table, with a default table name of "chgnet". The class can be used to load and store various CHGNet calculation settings and other related configurations.

Parameters
  • table_name (str, optional): The name of the table where the parameters are stored. By default, it is set to "chgnet", but it can be customized if necessary.
Methods

__init__(self, table_name="chgnet") Initializes the parameters for the calculation.

CalcParams(table_name: str = 'chgnet')
571    def __init__(self, table_name: str = "chgnet") -> None:
572        """
573        Initialize the CalcParams object with the given table name.
574
575        Parameters
576        ----------
577        table_name : str, optional
578            The name of the table where the parameters are stored. By default, 
579            it is set to "chgnet".
580        """
581        super(CalcParams, self).__init__(
582            table_name=table_name,
583        )

Initialize the CalcParams object with the given table name.

Parameters
  • table_name (str, optional): The name of the table where the parameters are stored. By default, it is set to "chgnet".