1+ """Functions to run ASE calculators."""
2+
3+ from ase import Atoms
4+ from ase import units
5+ from ase .optimize import BFGS
6+ # from sella import Sella
7+
8+ def get_calculator (calculator , family = None ):
9+ """Get a new instance of the appropriate ASE calculator.
10+
11+ Args:
12+ program: The program name ('psi4', 'mace_mp', etc.)
13+ Returns:
14+ The calculator class
15+ Raises:
16+ ValueError: If the program is not supported
17+ """
18+ # Lazy import the appropriate calculator
19+ if calculator == 'psi4' :
20+ import psi4
21+ from ase .calculators .psi4 import Psi4
22+ return Psi4
23+ elif calculator == 'mace' :
24+ if family == 'mace_mp' :
25+ from mace .calculators import mace_mp
26+ return mace_mp
27+ elif family == 'mace_off' :
28+ from mace .calculators import mace_off
29+ return mace_off
30+ elif family == 'mace_anicc' :
31+ from mace .calculators import mace_anicc
32+ return mace_anicc
33+ else :
34+ raise ValueError (f"Unsupported MACE family: { family } " )
35+ # elif calculator == 'uma':
36+ # elif calculator == 'nwx':
37+ # from ase.calculators.nwchem import NWChem
38+ # return NWChem
39+
40+
41+ def from_calc_dictionary (input_dct , script_str ):
42+ """ Run an ASE calculation from an input dictionary and return results
43+
44+ Args:
45+ input_dct: Dictionary containing:
46+ - calculator: Name of ASE calculator to use ('psi4' or 'nwx')
47+ - atom_parameters:
48+ - symbols: List of atomic symbols
49+ - positions: List of [x,y,z] coordinates
50+ - basis: Basis set name
51+ - method: Quantum chemistry method
52+ - charge: Molecular charge
53+ - multiplicity: Spin multiplicity
54+ - reference: Reference type ('rhf', 'uhf', etc.)
55+ script_str: String of script that contains ase_<calculator>
56+ Returns:
57+ dict: Results containing:
58+ - energy: Total energy in eV
59+ - forces: Forces on atoms in eV/Å
60+ - dipole: Dipole moment in e⋅Å
61+ - charges: Atomic charges
62+ - parameters: Input parameters used
63+ - version: Version string of the program used
64+ """
65+ # Set up Atoms object
66+ atom_parameters = input_dct .get ('atom_parameters' , {})
67+ job = input_dct .get ('job' , 'energy' )
68+ atoms = Atoms (
69+ symbols = atom_parameters .get ('symbols' , []),
70+ positions = atom_parameters .get ('positions' , []),
71+ )
72+ # Set up calculator
73+ calculator = None
74+ if len (script_str .split ('ase_' )) > 1 :
75+ calculator = script_str .split ('ase_' )[- 1 ].strip ()
76+ if 'method' in input_dct .keys ():
77+ kwargs = {
78+ 'atoms' : atoms ,
79+ 'basis' : input_dct .get ('basis' ),
80+ 'method' : input_dct .get ('method' ),
81+ 'charge' : input_dct .get ('charge' , 0 ),
82+ 'multiplicity' : input_dct .get ('multiplicity' , 1 ),
83+ 'reference' : input_dct .get ('reference' )
84+ }
85+ family = None
86+ elif 'family' in input_dct .keys ():
87+ family = input_dct .get ('family' )
88+ if 'mlip' in input_dct .keys ():
89+ kwargs = {
90+ 'model' : input_dct .get ('mlip' ),
91+ 'device' : 'cpu'
92+ }
93+ else :
94+ kwargs = {}
95+ calc = get_calculator (calculator , family )(** kwargs )
96+
97+ # Attach calculator to atoms and run
98+ atoms .calc = calc
99+
100+ # Run calculation and get basic properties that all calculators support
101+ if job == 'energy' :
102+ energy = atoms .get_potential_energy () / units .Hartree
103+ positions = atoms .get_positions ()
104+ elif job == 'optimize' :
105+ if not input_dct .get ('saddle' , False ):
106+ dyn = BFGS (atoms )
107+ # else:
108+ # dyn = Sella(atoms)
109+ dyn .run (
110+ fmax = input_dct .get ('gconv' , 0.05 ),
111+ steps = input_dct .get ('maxcyc' , 100 ),
112+ opt_type = input_dct .get ('opt_type' , 'standard' )
113+ )
114+ energy = atoms .get_potential_energy ()
115+ positions = atoms .get_positions ()
116+
117+ # Initialize results with guaranteed properties
118+ results = {
119+ 'energy' : energy ,
120+ 'positions' : positions ,
121+ # 'forces': forces.tolist(),
122+ 'parameters' : dict (calc .parameters ) # Convert to regular dict for JSON serialization
123+ }
124+
125+ # Add calculator-specific properties based on calculator type
126+ if calculator == 'psi4' :
127+ results ['version' ] = f'ase-psi4'
128+ results ['normal_termination' ] = True
129+ # Psi4 supports both dipole moments and charges
130+ # if 'properties' not in calc.parameters or 'dipole' in calc.parameters.get('properties', []):
131+ # results['dipole'] = atoms.get_dipole_moment().tolist()
132+ # if 'properties' not in calc.parameters or 'mulliken' in calc.parameters.get('properties', []):
133+ # results['charges'] = atoms.get_charges().tolist()
134+ elif 'mace' in calculator :
135+ # MACE specific properties
136+ results ['version' ] = f'ase-mace'
137+ results ['normal_termination' ] = True
138+ # MACE does not provide dipole moments or charges
139+ # if 'properties' not in calc.parameters or 'dipole' in calc.parameters.get('properties', []):
140+ # results['dipole'] = atoms.get_dipole_moment().tolist()
141+ # if 'properties' not in calc.parameters or 'mulliken' in calc.parameters.get('properties', []):
142+ # results['charges'] = atoms.get_charges().tolist()
143+ elif calculator == 'nwx' :
144+ # NWChem specific properties
145+ pass
146+ # if 'dipole_moment' in calc.get_implemented_properties():
147+ # results['dipole'] = atoms.get_dipole_moment().tolist()
148+ # if 'charges' in calc.get_implemented_properties():
149+ # results['charges'] = atoms.get_charges().tolist()
150+
151+ return results
152+
0 commit comments