def test_energy_forces_stress(): """ To test that the calculator can produce correct energy and forces. This is done by comparing the energy for an FCC argon lattice with an example model to the known value; the forces/stress returned by the model are compared to numerical estimates via finite difference. """ import numpy as np from pytest import importorskip importorskip('kimpy') from ase.calculators.kim import KIM from ase.lattice.cubic import FaceCenteredCubic # Create an FCC atoms crystal atoms = FaceCenteredCubic( directions=[[1, 0, 0], [0, 1, 0], [0, 0, 1]], size=(1, 1, 1), symbol="Ar", pbc=(1, 0, 0), latticeconstant=3.0, ) # Perturb the x coordinate of the first atom by less than the cutoff distance atoms.positions[0, 0] += 0.01 calc = KIM("ex_model_Ar_P_Morse_07C") atoms.set_calculator(calc) # Get energy and analytical forces/stress from KIM model energy = atoms.get_potential_energy() forces = atoms.get_forces() stress = atoms.get_stress() # Previously computed energy for this configuration for this model energy_ref = 19.7196709065 # eV # Compute forces and virial stress numerically forces_numer = calc.calculate_numerical_forces(atoms, d=0.0001) stress_numer = calc.calculate_numerical_stress(atoms, d=0.0001, voigt=True) tol = 1e-6 assert np.isclose(energy, energy_ref, tol) assert np.allclose(forces, forces_numer, tol) assert np.allclose(stress, stress_numer, tol) # This has been known to segfault atoms.set_pbc(True) atoms.get_potential_energy()
def test_relax(): """ Test that a static relaxation that requires multiple neighbor list rebuilds can be carried out successfully. This is verified by relaxing an icosahedral cluster of atoms and checking that the relaxed energy matches a known precomputed value for an example model. """ import numpy as np from ase.cluster import Icosahedron from pytest import importorskip importorskip('kimpy') from ase.calculators.kim import KIM from ase.optimize import BFGS energy_ref = -0.5420939378624228 # eV # Create structure and calculator atoms = Icosahedron("Ar", latticeconstant=3.0, noshells=2) calc = KIM("ex_model_Ar_P_Morse_07C") atoms.calc = calc opt = BFGS(atoms, logfile=None) opt.run(fmax=0.05) assert np.isclose(atoms.get_potential_energy(), energy_ref)
def test_multi_neighlist(): """ To test that the correct energy/forces/stress can be computed using a model that implements multiple cutoffs. This is done by construct a 10 Angstrom x 10 Angstrom x 10 Angstrom non-periodic cell filled with 15 randomly positioned atoms and requesting tha tthe model compute the energy, forces, and virial stress. The energy returned by the model is compared to a known precomputed value, while the forces and stress returned are compared to numerical estimates via finite difference. """ import numpy as np from ase import Atoms from pytest import importorskip importorskip('kimpy') from ase.calculators.kim import KIM # Create random cluster of atoms positions = np.random.RandomState(34).rand(15, 3) * 10 atoms = Atoms( "Ar" * 15, positions=positions, pbc=False, cell=[[10, 0, 0], [0, 10, 0], [0, 0, 10]] ) calc = KIM("ex_model_Ar_P_Morse_MultiCutoff") atoms.set_calculator(calc) # Get energy and analytical forces/stress from KIM Model energy = atoms.get_potential_energy() forces = atoms.get_forces() stress = atoms.get_stress() # Previously computed energy for this configuration for this model energy_ref = 34.69963483186903 # eV # Compute forces and virial stress numerically forces_numer = calc.calculate_numerical_forces(atoms, d=0.0001) stress_numer = calc.calculate_numerical_stress(atoms, d=0.0001, voigt=True) tol = 1e-6 assert np.isclose(energy, energy_ref, tol) assert np.allclose(forces, forces_numer, tol) assert np.allclose(stress, stress_numer, tol)
def test_cutoff_skin(): """ To test that the calculator handles skin and cutoffs correctly. Specifically, note that the neighbor skin distance must be added onto both the model influence distance *and* each of the model cutoffs. If the skin is not added onto the cutoffs, then an atom lying in between the largest cutoff and the skinned influence distance will not register as a neighbor if it hasn't already. The cutoff (and influence distance) for the model ex_model_Ar_P_Morse_07C is 8.15 Angstroms and the default skin distance when using the kimpy neighbor list library (which is the default when using a KIM portable model with this calculator) is 0.2 times the cutoff distance (1.63 Angstroms for this model). Here, we construct a dimer with a separation falling just beyond the model cutoff but inside of the skinned influence distance. We then compute the energy, which we expect to be zero in any case. Next, we reduce the dimer separation by less than the skin distance so that the atoms fall within the cutoff of one another but without triggering a neighbor list rebuild. If the atom had properly registered as a neighbor when it was outside of the cutoff but still inside of the skinned influence distance, then the energy in this case should be significantly far off from zero. However, if the atom had failed to ever register as a neighbor, then we'll get zero once again. """ import numpy as np from pytest import importorskip importorskip('kimpy') from ase.calculators.kim import KIM from ase import Atoms # Create calculator calc = KIM("ex_model_Ar_P_Morse_07C") # Create dimer with separation just beyond cutoff distance. We *want* # these atoms to register as neighbors of one another since they fall # within the skinned influence distance of 9.78 Angstroms. model_cutoff = 8.15 skin_distance = 0.2 * model_cutoff distance_orig = model_cutoff + 0.1 * skin_distance atoms = Atoms("Ar2", positions=[[0, 0, 0], [distance_orig, 0, 0]]) atoms.set_calculator(calc) # Get energy -- should be zero e_outside_cutoff = atoms.get_potential_energy() # Now reduce the separation distance to something well within the model # cutoff -- should get something significantly non-zero atoms.positions[1, 0] -= 0.5 * skin_distance # Get new energy e_inside_cutoff = atoms.get_potential_energy() assert not np.isclose(e_outside_cutoff, e_inside_cutoff)
model = "ex_model_Ar_P_Morse_07C" model_cutoff = 8.15 # Angstroms # Create a dimer centered in a small box that's small enough so that there will be # padding atoms when we make it periodic box_len = 0.5 * model_cutoff dimer_separation = model_cutoff * 0.3 atoms = Atoms("Ar" * 2, cell=[[box_len, 0, 0], [0, box_len, 0], [0, 0, box_len]]) # Create calculator. Either the kimpy neighbor list library or ASE's native neighbor # lists should suffice to check this since update_kim_coords() belongs to their parent # class, NeighborList. Here, we'll use the default mode (kimpy neighbor list). neigh_skin_ratio = 0.2 skin = neigh_skin_ratio * model_cutoff calc = KIM(model, options={"neigh_skin_ratio": neigh_skin_ratio}) atoms.set_calculator(calc) squeezed_energies_ref = { False: 5.784620078721877, # finite True: 6.766293119162073, # periodic } for pbc in [False, True]: # Reset dimer positions to original configuration set_positions_to_orig(atoms, box_len, dimer_separation) atoms.set_pbc(pbc) # Get potential energy so that it will get rid of "pbc" being in the system_changes.
""" Test that a static relaxation that requires multiple neighbor list rebuilds can be carried out successfully. This is verified by relaxing an icosahedral cluster of atoms and checking that the relaxed energy matches a known precomputed value for an example model. """ import numpy as np from ase.cluster import Icosahedron from ase.calculators.kim import KIM from ase.optimize import BFGS energy_ref = -0.5420939378624228 # eV # Create structure and calculator atoms = Icosahedron("Ar", latticeconstant=3.0, noshells=2) calc = KIM("ex_model_Ar_P_Morse_07C") atoms.set_calculator(calc) opt = BFGS(atoms, logfile=None) opt.run(fmax=0.05) assert np.isclose(atoms.get_potential_energy(), energy_ref)
def test_update_coords(): """ Check that the coordinates registered with the KIM API are updated appropriately when the atomic positions are updated. This can go awry if the 'coords' attribute of the relevant NeighborList subclass is reassigned to a new memory location -- a problem which was indeed occurring at one point (see https://gitlab.com/ase/ase/merge_requests/1442)! """ import numpy as np from ase import Atoms from pytest import importorskip importorskip('kimpy') from ase.calculators.kim import KIM def squeeze_dimer(atoms, d): """Squeeze the atoms together by the absolute distance ``d`` (Angstroms) """ pos = atoms.get_positions() pos[0] += np.asarray([d, 0, 0]) atoms.set_positions(pos) def set_positions_to_orig(atoms, box_len, dimer_separation): pos1 = np.asarray([box_len / 2.0, box_len / 2.0, box_len / 2.0 ]) - np.asarray([dimer_separation / 2.0, 0, 0]) pos2 = np.asarray([box_len / 2.0, box_len / 2.0, box_len / 2.0 ]) + np.asarray([dimer_separation / 2.0, 0, 0]) atoms.set_positions([pos1, pos2]) # We know that ex_model_Ar_P_Morse_07C has a cutoff of 8.15 Angstroms model = "ex_model_Ar_P_Morse_07C" model_cutoff = 8.15 # Angstroms # Create a dimer centered in a small box that's small enough so that there will be # padding atoms when we make it periodic box_len = 0.5 * model_cutoff dimer_separation = model_cutoff * 0.3 atoms = Atoms("Ar" * 2, cell=[[box_len, 0, 0], [0, box_len, 0], [0, 0, box_len]]) # Create calculator. Either the kimpy neighbor list library or ASE's native neighbor # lists should suffice to check this since update_kim_coords() belongs to their parent # class, NeighborList. Here, we'll use the default mode (kimpy neighbor list). neigh_skin_ratio = 0.2 skin = neigh_skin_ratio * model_cutoff calc = KIM(model, options={"neigh_skin_ratio": neigh_skin_ratio}) atoms.calc = calc squeezed_energies_ref = { False: 5.784620078721877, # finite True: 6.766293119162073, # periodic } for pbc in [False, True]: # Reset dimer positions to original configuration set_positions_to_orig(atoms, box_len, dimer_separation) atoms.set_pbc(pbc) # Get potential energy so that it will get rid of "pbc" being in the system_changes. # The only way that update_kim_coords is called is when system_changes # only contains "positions", as otherwise a neighbor list rebuild is triggered. atoms.get_potential_energy() # First squeeze the dimer together by a distance less than the skin and compute the # energy. This doesn't trigger a neighbor list rebuild (which we avoid here because # we're only trying to test update_kim_coords, which is not called in the event that # a neighbor list rebuild is necessary). # # This will update the coordinate values in the ``coords`` attribute of the relevant # NeighborList subclass instance (see ase/calculators/kim/neighborlist.py) and, # since ``coords`` is pointing at the same memory the KIM API is reading the # coordinates from, the KIM Model will see the updated coordinates with no problem. # *However*, it's possible that after updating these values, the ``coords`` # attribute is bound to a *new* object in memory if update_kim_coords is broken # somehow! squeeze_dimer(atoms, 0.2 * skin) atoms.get_potential_energy() # Now squeeze the dimer together by the same amount again, where the sum of this # squeeze and the last is still less than the skin distance so that we still avoid a # neighbor list rebuild. If all is well, the `coords` attribute of the relevant # NeighborList subclass still points at the same location as before we did the # previous squeeze. Thus, when we update the coordinates again, it should still be # updating the same coordinates being read by the KIM API, giving us the expected # value of the energy. If update_kim_coords is broken, ``coords`` will already be # bound to a new object in memory while the KIM API is still looking at its previous # address to read coordinates. This means that when we update the coordinates in # the ``coords`` value, the KIM API never sees them. In fact, it's not even # guaranteed that the KIM API will read the same coordinates as before since the # memory where it's looking may have since been overwritten by some other python # objects. squeeze_dimer(atoms, 0.2 * skin) squeezed_energy = atoms.get_potential_energy() assert np.isclose(squeezed_energy, squeezed_energies_ref[pbc])
from ase.calculators.kim import KIM from ase.lattice.cubic import FaceCenteredCubic # Create an FCC atoms crystal atoms = FaceCenteredCubic( directions=[[1, 0, 0], [0, 1, 0], [0, 0, 1]], size=(1, 1, 1), symbol="Ar", pbc=(1, 0, 0), latticeconstant=3.0, ) # Perturb the x coordinate of the first atom by less than the cutoff distance atoms.positions[0, 0] += 0.01 calc = KIM("ex_model_Ar_P_Morse_07C") atoms.set_calculator(calc) # Get energy and analytical forces/stress from KIM model energy = atoms.get_potential_energy() forces = atoms.get_forces() stress = atoms.get_stress() # Previously computed energy for this configuration for this model energy_ref = 19.7196709065 # eV # Compute forces and virial stress numerically forces_numer = calc.calculate_numerical_forces(atoms, d=0.0001) stress_numer = calc.calculate_numerical_stress(atoms, d=0.0001, voigt=True) tol = 1e-6