Skip to content

Commit 5346f27

Browse files
committed
Merge branch 'mkphuthi/develop' of https://github.com/BattModels/asimtools into mkphuthi/develop
2 parents 3afdbb1 + c198886 commit 5346f27

File tree

8 files changed

+150
-9
lines changed

8 files changed

+150
-9
lines changed

asimtools/asimmodules/ase_md/ase_md.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,8 @@ def ase_md(
290290
)
291291

292292

293+
if plot_args is None:
294+
plot_args = {}
293295
if plot:
294296
plot_thermo(
295297
images={'image_file': 'output.traj'},

asimtools/asimmodules/data/collect_images.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def collect_images(
6161
:rtype: Dict
6262
6363
"""
64+
6465
write_kwargs = {}
6566
if fnames == (1):
6667
fnames = [f'{fnames}-{i:03d}' for i in range(len(splits))]
@@ -125,6 +126,7 @@ def collect_images(
125126
selected_atoms = [selected_atoms[i] for i in selected_inds]
126127

127128
if shuffle:
129+
assert not sort_by_energy_per_atom, 'Either sort or shuffle, not both'
128130
np.random.shuffle(selected_atoms)
129131
elif sort_by_energy_per_atom:
130132
assert not shuffle, 'Either sort or shuffle, not both'

asimtools/asimmodules/lammps/lammps.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
Author: mkphuthi@github.com
66
'''
7-
from typing import Dict, Optional
7+
from typing import Dict, Optional, Sequence
88
import sys
99
from pathlib import Path
1010
from numpy.random import randint
@@ -24,6 +24,8 @@ def lammps(
2424
masses: bool = True,
2525
velocities: bool = False,
2626
seed: Optional[int] = None,
27+
restart_template: Optional[str] = None,
28+
specorder: Sequence[str] = None,
2729
) -> Dict:
2830
"""Runs a lammps script based on a specified template, variables can be
2931
specified as arguments to be defined in the final LAMMPS input file if
@@ -55,6 +57,12 @@ def lammps(
5557
seed to be placed, if seed=None, a random one is generated,
5658
defaults to None
5759
:type seed: int, optional
60+
:param restart_template: Optional lammps input template to be used to
61+
generate a restart.lammps file, defaults to None
62+
:type restart_template: str, optional
63+
:param specorder: Optional list of atomic species in the order they
64+
should appear in the LAMMPS data input file, defaults to None
65+
:type specorder: Sequence[str], optional
5866
:return: LAMMPS out file names
5967
:rtype: Dict
6068
"""
@@ -73,6 +81,7 @@ def lammps(
7381
atom_style=atom_style,
7482
masses=masses,
7583
velocities=velocities,
84+
specorder=specorder,
7685
)
7786
except ValueError as te:
7887
err_txt = 'Need ASE version >=3.23 to support writing '
@@ -92,6 +101,7 @@ def lammps(
92101
variables['IMAGE_FILE'] = 'image_input.lmpdat'
93102

94103
lmp_txt = ''
104+
95105
for variable, value in variables.items():
96106
lmp_txt += f'variable {variable} equal {value}\n'
97107

@@ -103,6 +113,18 @@ def lammps(
103113
if placeholders is None:
104114
placeholders = {}
105115

116+
if restart_template is not None:
117+
restart_txt = lmp_txt
118+
with open(restart_template, 'r', encoding='utf-8') as f:
119+
restart_lines = f.readlines()
120+
for rline in restart_lines:
121+
if placeholders is not None:
122+
for placeholder in placeholders:
123+
rline = rline.replace(placeholder, str(placeholders[placeholder]))
124+
restart_txt += rline
125+
with open('restart.lammps', 'w', encoding='utf-8') as f:
126+
f.write(restart_txt)
127+
106128
for line in lines:
107129
if 'SEED' in line and 'SEED' not in placeholders:
108130
if seed is None:

asimtools/asimmodules/vasp/vasp.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,24 @@ def vasp(
7373
:param image: Initial image for VASP calculation. Image specification,
7474
see :func:`asimtools.utils.get_atoms`
7575
:type image: Dict
76-
:param vaspinput_args: Dictionary of pymatgen's VaspInput arguments.
76+
:param user_incar_settings: Dictionary of INCAR settings to override
77+
defaults or MP settings, defaults to None
78+
:type user_incar_settings: Dict, optional
79+
:param user_kpoints_settings: Dictionary of KPOINTS settings to override
80+
defaults or MP settings, defaults to None
81+
:type user_kpoints_settings: Dict, optional
82+
:param user_potcar_functional: Potcar functional to use in case of MP
83+
settings, defaults to 'PBE_64'
84+
:type user_potcar_functional: str, optional
85+
:param potcar: Dictionary specifying Potcar settings, see
86+
:class:`pymatgen.io.vasp.inputs.Potcar`, defaults to None
87+
:type potcar: Dict, optional
88+
:param prev_calc: Path to previous VASP calculation to use as starting
89+
point for MP settings, defaults to None
90+
:type prev_calc: os.PathLike, optional
91+
:param vaspinput_kwargs: Dictionary of pymatgen's VaspInput arguments.
7792
See :class:`pymatgen.io.vasp.inputs.VaspInput`
78-
:type vaspinput_args: Dict
93+
:type vaspinput_kwargs: Dict
7994
:param command: Command with which to run VASP, defaults to 'vasp_std'
8095
:type command: str, optional
8196
:param mpset: Materials Project VASP set to use see
@@ -84,6 +99,9 @@ def vasp(
8499
:param write_image_output: Whether to write output image in standard
85100
asimtools format to file, defaults to False
86101
:type write_image_output: bool, optional
102+
:param run_vasp: Whether to run VASP after writing input files,
103+
defaults to True
104+
:type run_vasp: bool, optional
87105
"""
88106

89107
if vaspinput_kwargs is None:

asimtools/asimmodules/workflows/image_array.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414

1515
def image_array(
1616
images: Dict,
17-
subsim_input: Dict,
17+
subsim_input: Optional[Dict] = None,
18+
template_sim_input: Optional[Dict] = None,
1819
calc_input: Optional[Dict] = None,
1920
env_input: Optional[Dict] = None,
2021
array_max: Optional[int] = None,
@@ -33,8 +34,14 @@ def image_array(
3334
3435
:param images: Images specification, see :func:`asimtools.utils.get_images`
3536
:type images: Dict
36-
:param subsim_input: sim_input of asimmodule to be run
37-
:type subsim_input: Dict
37+
:param subsim_input: sim_input of asimmodule to be run, included for backward
38+
compatibility, please use template_sim_input instead,
39+
defaults to None
40+
:type subsim_input: Optional[Dict], optional
41+
:param template_sim_input: sim_input of asimmodule to be run, defaults to None
42+
:type template_sim_input: Optional[Dict], optional
43+
:param str_btn_args: args to pass to :func:`asimtools.utils.get_str_btn`
44+
:type str_btn_args: Optional[Sequence], optional
3845
:param calc_input: calc_input to override global file, defaults to None
3946
:type calc_input: Optional[Dict], optional
4047
:param env_input: env_input to override global file, defaults to None
@@ -83,6 +90,8 @@ def image_array(
8390
secondary_array_values=secondary_array_values,
8491
)
8592

93+
if template_sim_input is not None:
94+
subsim_input = template_sim_input
8695
if key_sequence is None:
8796
key_sequence = ['args', 'image']
8897
# For backwards compatibility where we don't have to specify image

asimtools/calculators.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def load_calc(
3838
calc_params = calc_input[calc_id]
3939
except KeyError as exc:
4040
msg = f'Calculator with calc_id: {calc_id} not found in'
41-
msg += f'calc_input {calc_input}'
41+
msg += f'calc_input {[c for c in calc_input]}'
4242
raise KeyError(msg) from exc
4343
except AttributeError as exc:
4444
raise AttributeError('No calc_input found') from exc
@@ -400,6 +400,28 @@ def load_ase_dftd3(calc_params):
400400

401401
return calc
402402

403+
def load_aqcat(calc_params):
404+
"""Load AQCat Calculator
405+
:param calc_params: args to pass to loader, including checkpoint_path
406+
:type calc_params: Dict
407+
:return: AQCat calculator
408+
:rtype: :class:`fairchem.core.common.relaxation.ase_utils.patched_calc`
409+
"""
410+
from fairchem.core.common.relaxation.ase_utils import patched_calc
411+
412+
CHECKPOINT_PATH = "aqcat25/checkpoints_aqcat_ev2/ev2-in+midFiLM-AQCat25+OC20-20M_20251008_223220.pt"
413+
414+
try:
415+
calc = patched_calc(**calc_params)
416+
except Exception:
417+
logging.error(
418+
"Failed to load AQCat FAIRChemCalculator with parameters:\n %s", \
419+
calc_params
420+
)
421+
raise
422+
423+
return calc
424+
403425
external_calcs = {
404426
'NequIP': load_nequip,
405427
'Allegro': load_nequip,
@@ -415,4 +437,5 @@ def load_ase_dftd3(calc_params):
415437
'fairchemV2': load_fairchemV2,
416438
'fairchem': load_fairchemV2,
417439
'ASEDFTD3': load_ase_dftd3,
440+
'AQCat': load_aqcat
418441
}

asimtools/utils.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ def get_atoms(
245245
builder: Optional[str] = 'bulk',
246246
atoms: Optional[Atoms] = None,
247247
repeat: Optional[Tuple[int, int, int]] = None,
248+
repeat_to_N_args: Optional[Dict] = None,
248249
rattle_stdev: Optional[float] = None,
249250
mp_id: Optional[str] = None,
250251
user_api_key: Optional[str] = None,
@@ -329,14 +330,16 @@ def get_atoms(
329330
>>> get_atoms(image_file='molecules.xyz', index=0) # Pick out one structure using indexing
330331
Atoms(symbols='OH2', pbc=False)
331332
332-
You can also make supercells and rattle the atoms
333+
You can also make supercells and rattle the atoms or repeat to a target
333334
334335
>>> li_bulk = get_atoms(name='Li')
335336
>>> li_bulk.write('POSCAR', format='vasp')
336337
>>> get_atoms(image_file='POSCAR', repeat=[3,3,3])
337338
Atoms(symbols='Li27', pbc=True, cell=[[-5.235, 5.235, 5.235], [5.235, -5.235, 5.235], [5.235, 5.235, -5.235]])
338339
>>> get_atoms(builder='bulk', name='Li', repeat=[2,2,2], rattle_stdev=0.01)
339340
Atoms(symbols='Li8', pbc=True, cell=[[-3.49, 3.49, 3.49], [3.49, -3.49, 3.49], [3.49, 3.49, -3.49]])
341+
>>> get_atoms(builder='bulk', name='Li', repeat_to_N_args={'N': 16, 'max_dim': 10.0})
342+
Atoms(symbols='Li16', pbc=True, cell=[[-6.98, 6.98, 6.98], [6.98, -6.98, 6.98], [6.98, 6.98, -6.98]])
340343
341344
Mostly for internal use and use in asimmodules, one can specify atoms
342345
directly
@@ -439,6 +442,13 @@ def get_atoms(
439442
elif rattle_stdev is not None and interface == 'pymatgen':
440443
struct.perturb(distance=rattle_stdev, min_distance=0)
441444

445+
if repeat_to_N_args is not None and interface == 'ase':
446+
atoms = repeat_to_N(atoms, **repeat_to_N_args)
447+
elif repeat_to_N_args is not None and interface == 'pymatgen':
448+
raise NotImplementedError(
449+
'repeat_to_N_args is only implemented for ASE interface'
450+
)
451+
442452
if constraints is not None and interface == 'ase':
443453
consts = []
444454
for constraint_args in constraints:
@@ -491,6 +501,47 @@ def parse_slice(value: str, bash: bool = False) -> slice:
491501
parts.append('1')
492502
return f'$(seq {parts[0]} {parts[2]} {parts[1]})'
493503

504+
def repeat_to_N(
505+
atoms: Atoms,
506+
N: int,
507+
max_dim: float = 50.0,
508+
) -> Atoms:
509+
"""Scale a structure to have approximately N atoms in the unit cell.
510+
The function repeats the shortest axis of the unit cell until the
511+
number of atoms >= N or the longest axis of the unit cell is > max_dim.
512+
513+
:param atoms: Input atoms object
514+
:type atoms: Atoms
515+
:param N: Target number of atoms
516+
:type N: int
517+
:param max_dim: Maximum length of the longest axis of the unit cell,
518+
defaults to 50.0
519+
:type max_dim: float, optional
520+
:raises ValueError: If it fails to scale the structure to N atoms
521+
:return: Scaled atoms object
522+
:rtype: Atoms
523+
"""
524+
cell = atoms.get_cell()
525+
lengths = [np.linalg.norm(vec) for vec in cell]
526+
num_atoms = len(atoms)
527+
new_atoms = atoms.copy()
528+
529+
while num_atoms < N and np.max(lengths) < max_dim:
530+
shortest_axis = np.argmin(lengths)
531+
repeat_vec = [1, 1, 1]
532+
repeat_vec[shortest_axis] += 1
533+
new_atoms = new_atoms.repeat(repeat_vec)
534+
cell = new_atoms.get_cell()
535+
lengths = [np.linalg.norm(vec) for vec in cell]
536+
num_atoms = len(new_atoms)
537+
538+
if num_atoms < N:
539+
raise ValueError(
540+
f'Failed to scale structure to {N} atoms without exceeding \
541+
max_dim of {max_dim} Angstroms'
542+
)
543+
return new_atoms
544+
494545
def get_images(
495546
image_file: str = None,
496547
pattern: str = None,

tests/unit/test_utils.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
get_str_btn,
2525
expand_wildcards,
2626
write_atoms,
27+
repeat_to_N,
2728
)
2829
import ase.build
2930

@@ -132,6 +133,7 @@ def test_join_names(test_input, expected):
132133
])
133134
def test_get_atoms(test_input, expected):
134135
''' Test getting atoms from different inputs '''
136+
print('e', expected)
135137
assert get_atoms(**test_input) == expected
136138

137139
def test_get_atoms_constraints(tmp_path):
@@ -373,4 +375,16 @@ def test_expand_wildcards(test_input, expected, tmp_path):
373375
f.write('')
374376
print(f'Found paths in {os.getcwd()}: {[f for f in Path(tmp_path).glob("*")]}')
375377

376-
assert expand_wildcards(test_input, root_path=tmp_path) == expected
378+
assert expand_wildcards(test_input, root_path=tmp_path) == expected
379+
380+
def test_repeat_to_N():
381+
''' Test repeating unit cell to at least N atoms '''
382+
atoms = ase.build.bulk('Cu', crystalstructure='fcc', cubic=True, a=2.0)
383+
repeated_atoms = repeat_to_N(atoms, 16)
384+
assert len(repeated_atoms) == 16
385+
assert np.abs(repeated_atoms.get_cell()[0][0] - 2*2.0) < 1e-6
386+
assert np.abs(repeated_atoms.get_cell()[1][1] - 2*2.0) < 1e-6
387+
assert np.abs(repeated_atoms.get_cell()[2][2] - 1*2.0) < 1e-6
388+
assert len(repeat_to_N(atoms, 15)) == 16
389+
with pytest.raises(ValueError):
390+
repeat_to_N(atoms, 16, max_dim=4)

0 commit comments

Comments
 (0)