def test_set_masks_from_region(at0, qm_calc, mm_calc): """ Test setting masks from region array """ qmmm = at0.calc region = qmmm.get_region_from_masks(at0) # initialise another qmmm with different masks r = at0.get_distances(0, np.arange(len(at0)), mic=True) R_QM = 1.0e-3 qm_mask = r < R_QM test_qmmm = ForceQMMM(at0, qm_mask, qm_calc, mm_calc, buffer_width=3.61) # assert that number of qm atoms is different assert not (np.count_nonzero(qmmm.qm_selection_mask) == np.count_nonzero( test_qmmm.qm_selection_mask)) test_qmmm.set_masks_from_region(region) assert all(test_qmmm.qm_selection_mask == qmmm.qm_selection_mask) assert all(test_qmmm.qm_buffer_mask == qmmm.qm_buffer_mask) test_region = test_qmmm.get_region_from_masks(at0) assert all(region == test_region)
def at0(qm_calc, mm_calc, bulk_at): alat = bulk_at.cell[0, 0] at0 = bulk_at * 5 r = at0.get_distances(0, np.arange(len(at0)), mic=True) # should give 12 nearest neighbours + atom in the center R_QM = alat / np.sqrt(2.0) + 1.0e-3 qm_mask = r < R_QM qmmm = ForceQMMM(at0, qm_mask, qm_calc, mm_calc, buffer_width=3.61) qmmm.initialize_qm_buffer_mask(at0) at0.calc = qmmm return at0
def compare_qm_cell_and_pbc(qm_calc, mm_calc, bulk_at, test_size=4, expected_pbc=np.array([True, True, True]), buffer_width=5 * 3.61): """ test qm cell shape and choice of pbc: make a non-periodic pdc in a direction if qm_radius + buffer is larger than the original cell keep the periodic cell otherwise i. e. if cell[i, i] > qm_radius + buffer the scenario is controlled by the test_size used to create at0 as well as buffer_width. If the size of the at0 is larger than the r_qm + buffer + vacuum the cell stays periodic and the size is the same is original otherwise cell is non-periodic and size is different. """ alat = bulk_at.cell[0, 0] at0 = bulk_at * test_size r = at0.get_distances(0, np.arange(len(at0)), mic=True) # should give 12 nearest neighbours + atom in the center R_QM = alat / np.sqrt(2.0) + 1.0e-3 qm_mask = r < R_QM qmmm = ForceQMMM(at0, qm_mask, qm_calc, mm_calc, buffer_width=buffer_width) # equal to 1 alat # build qm_buffer_mask to build the cell qmmm.initialize_qm_buffer_mask(at0) qm_cluster = qmmm.get_qm_cluster(at0) # test if qm pbc match expected in qmmm.get_cluster() assert all(qm_cluster.pbc == expected_pbc) # test the cell size for qmmm.get_qm_cluster() if not all(expected_pbc): # at least one F. avoid comparing empty arrays assert not all(qm_cluster.cell.lengths()[~expected_pbc] == at0.cell.lengths()[~expected_pbc]) if any(expected_pbc): # at least one T. avoid comparing empty arrays np.testing.assert_allclose(qm_cluster.cell.lengths()[expected_pbc], at0.cell.lengths()[expected_pbc])
def test_import_xyz(at0, qm_calc, mm_calc, testdir): """ test the import_extxyz function and checks the mapping """ filename = "qmmm_export_test.xyz" qmmm = at0.calc qmmm.export_extxyz(filename=filename, atoms=at0) imported_qmmm = ForceQMMM.import_extxyz(filename, qm_calc, mm_calc) assert all(imported_qmmm.qm_selection_mask == qmmm.qm_selection_mask) assert all(imported_qmmm.qm_buffer_mask == qmmm.qm_buffer_mask)
def test_qm_buffer_mask(qm_calc, mm_calc, bulk_at): """ test number of atoms in qm_buffer_mask for spherical region in a fully periodic cell also tests that "region" array returns the same mapping """ alat = bulk_at.cell[0, 0] N_cell_geom = 10 at0 = bulk_at * N_cell_geom r = at0.get_distances(0, np.arange(len(at0)), mic=True) print("N_cell", N_cell_geom, 'N_MM', len(at0), "Size", N_cell_geom * alat) qm_rc = 5.37 # cutoff for EMC() for R_QM in [ 1.0e-3, # one atom in the center alat / np.sqrt(2.0) + 1.0e-3, # should give 12 nearest # neighbours + central atom alat + 1.0e-3 ]: # should give 18 neighbours + central atom at = at0.copy() qm_mask = r < R_QM qm_buffer_mask_ref = r < 2 * qm_rc + R_QM # exclude atoms that are too far (in case of non spherical region) # this is the old way to do it _, r_qm_buffer = get_distances(at.positions[qm_buffer_mask_ref], at.positions[qm_mask], at.cell, at.pbc) updated_qm_buffer_mask = np.ones_like(at[qm_buffer_mask_ref]) for i, r_qm in enumerate(r_qm_buffer): if r_qm.min() > 2 * qm_rc: updated_qm_buffer_mask[i] = False qm_buffer_mask_ref[qm_buffer_mask_ref] = updated_qm_buffer_mask ''' print(f'R_QM {R_QM} N_QM {qm_mask.sum()}') print(f'R_QM + buffer: {2 * qm_rc + R_QM:.2f}' f' N_QM_buffer {qm_buffer_mask_ref.sum()}') print(f' N_total: {len(at)}') ''' qmmm = ForceQMMM(at, qm_mask, qm_calc, mm_calc, buffer_width=2 * qm_rc) # build qm_buffer_mask and test it qmmm.initialize_qm_buffer_mask(at) # print(f' Calculator N_QM_buffer:' # f' {qmmm.qm_buffer_mask.sum().sum()}') assert qmmm.qm_buffer_mask.sum() == qm_buffer_mask_ref.sum() # same test for qmmm.get_cluster() qm_cluster = qmmm.get_qm_cluster(at) assert len(qm_cluster) == qm_buffer_mask_ref.sum() # test region mappings region = qmmm.get_region_from_masks(at) qm_mask_region = region == "QM" assert qm_mask_region.sum() == qm_mask.sum() buffer_mask_region = region == "buffer" assert qm_mask_region.sum() + \ buffer_mask_region.sum() == qm_buffer_mask_ref.sum()
from ase import Atom, Atoms from ase.build import bulk, fcc100, add_adsorbate, add_vacuum from ase.calculators.vasp import Vasp from ase.calculators.kim.kim import KIM from ase.calculators.qmmm import ForceQMMM, RescaledCalculator from ase.constraints import StrainFilter from ase.optimize import LBFGS from ase.visualize import view atoms = bulk("Pd", "fcc", a=3.5, cubic=True) atoms.calc = KIM("MEAM_LAMMPS_JeongParkDo_2018_PdMo__MO_356501945107_000") opt = LBFGS(StrainFilter(atoms), logfile=None) opt.run(0.03, steps=30) length = atoms.cell.cellpar()[0] atoms = fcc100("Pd", (2,2,5), a=length, vacuum=10, periodic=True) add_adsorbate(atoms, Atoms([Atom("Mo")]), 1.2) qm_mask = [len(atoms)-1, len(atoms)-2] qm_calc = Vasp(directory="./qmmm") mm_calc = KIM("MEAM_LAMMPS_JeongParkDo_2018_PdMo__MO_356501945107_000") mm_calc = RescaledCalculator(mm_calc, 1, 1, 1, 1) qmmm = ForceQMMM(atoms, qm_mask, qm_calc, mm_calc, buffer_width=3) qmmm.initialize_qm_buffer_mask(atoms) atoms.pbc=True atoms.calc = qmmm print(atoms.get_forces())
print(len(r)) del at0[0] # introduce a vacancy print("N_cell", N_cell, 'N_MM', len(at0)) ref_at = at0.copy() ref_at.set_calculator(qm) opt = FIRE(ref_at) opt.run(fmax=1e-3) u_ref = ref_at.positions - at0.positions us = [] for R_QM in R_QMs: at = at0.copy() mask = r < R_QM print('R_QM', R_QM, 'N_QM', mask.sum(), 'N_total', len(at)) qmmm = ForceQMMM(at, mask, qm, mm, buffer_width=2 * qm.rc) at.set_calculator(qmmm) opt = FIRE(at) opt.run(fmax=1e-3) us.append(at.positions - at0.positions) # compute error in energy norm |\nabla u - \nabla u_ref| def strain_error(at0, u_ref, u, cutoff, mask): I, J = neighbor_list('ij', at0, cutoff) I, J = np.array([(i, j) for i, j in zip(I, J) if mask[i]]).T v = u_ref - u dv = np.linalg.norm(v[I, :] - v[J, :], axis=1) return np.linalg.norm(dv)
def test_forceqmmm(): # parameters N_cell = 2 R_QMs = np.array([3, 7]) # setup bulk and MM region bulk_at = bulk("Cu", cubic=True) sigma = (bulk_at * 2).get_distance(0, 1) * (2.**(-1. / 6)) mm = LennardJones(sigma=sigma, epsilon=0.05) qm = EMT() # compute MM and QM equations of state def strain(at, e, calc): at = at.copy() at.set_cell((1.0 + e) * at.cell, scale_atoms=True) at.calc = calc v = at.get_volume() e = at.get_potential_energy() return v, e eps = np.linspace(-0.01, 0.01, 13) v_qm, E_qm = zip(*[strain(bulk_at, e, qm) for e in eps]) v_mm, E_mm = zip(*[strain(bulk_at, e, mm) for e in eps]) eos_qm = EquationOfState(v_qm, E_qm) v0_qm, E0_qm, B_qm = eos_qm.fit() a0_qm = v0_qm**(1.0 / 3.0) eos_mm = EquationOfState(v_mm, E_mm) v0_mm, E0_mm, B_mm = eos_mm.fit() a0_mm = v0_mm**(1.0 / 3.0) mm_r = RescaledCalculator(mm, a0_qm, B_qm, a0_mm, B_mm) v_mm_r, E_mm_r = zip(*[strain(bulk_at, e, mm_r) for e in eps]) eos_mm_r = EquationOfState(v_mm_r, E_mm_r) v0_mm_r, E0_mm_r, B_mm_r = eos_mm_r.fit() a0_mm_r = v0_mm_r**(1.0 / 3) # check match of a0 and B after rescaling is adequete assert abs( (a0_mm_r - a0_qm) / a0_qm) < 1e-3 # 0.1% error in lattice constant assert abs((B_mm_r - B_qm) / B_qm) < 0.05 # 5% error in bulk modulus # plt.plot(v_mm, E_mm - np.min(E_mm), 'o-', label='MM') # plt.plot(v_qm, E_qm - np.min(E_qm), 'o-', label='QM') # plt.plot(v_mm_r, E_mm_r - np.min(E_mm_r), 'o-', label='MM rescaled') # plt.legend() at0 = bulk_at * N_cell r = at0.get_distances(0, np.arange(1, len(at0)), mic=True) print(len(r)) del at0[0] # introduce a vacancy print("N_cell", N_cell, 'N_MM', len(at0)) ref_at = at0.copy() ref_at.calc = qm opt = FIRE(ref_at) opt.run(fmax=1e-3) u_ref = ref_at.positions - at0.positions us = [] for R_QM in R_QMs: at = at0.copy() mask = r < R_QM print('R_QM', R_QM, 'N_QM', mask.sum(), 'N_total', len(at)) qmmm = ForceQMMM(at, mask, qm, mm, buffer_width=2 * qm.rc) at.calc = qmmm opt = FIRE(at) opt.run(fmax=1e-3) us.append(at.positions - at0.positions) # compute error in energy norm |\nabla u - \nabla u_ref| def strain_error(at0, u_ref, u, cutoff, mask): I, J = neighbor_list('ij', at0, cutoff) I, J = np.array([(i, j) for i, j in zip(I, J) if mask[i]]).T v = u_ref - u dv = np.linalg.norm(v[I, :] - v[J, :], axis=1) return np.linalg.norm(dv) du_global = [ strain_error(at0, u_ref, u, 1.5 * sigma, np.ones(len(r))) for u in us ] du_local = [strain_error(at0, u_ref, u, 1.5 * sigma, r < 3.0) for u in us] print('du_local', du_local) print('du_global', du_global) # check local errors are monotonically decreasing assert np.all(np.diff(du_local) < 0) # check global errors are monotonically converging assert np.all(np.diff(du_global) < 0) # biggest QM/MM should match QM result assert du_local[-1] < 1e-10 assert du_global[-1] < 1e-10
def test_forceqmmm(qm_calc, mm_calc, bulk_at): # parameters N_cell = 2 R_QMs = np.array([3, 7]) sigma = (bulk_at * 2).get_distance(0, 1) * (2.**(-1. / 6)) at0 = bulk_at * N_cell r = at0.get_distances(0, np.arange(1, len(at0)), mic=True) print(len(r)) del at0[0] # introduce a vacancy print("N_cell", N_cell, 'N_MM', len(at0), "Size", N_cell * bulk_at.cell[0, 0]) ref_at = at0.copy() ref_at.calc = qm_calc opt = FIRE(ref_at) opt.run(fmax=1e-3) u_ref = ref_at.positions - at0.positions us = [] for R_QM in R_QMs: at = at0.copy() qm_mask = r < R_QM qm_buffer_mask_ref = r < 2 * qm_calc.rc + R_QM print(f'R_QM {R_QM} N_QM {qm_mask.sum()}') print(f'R_QM + buffer: {2 * qm_calc.rc + R_QM:.2f}' f' N_QM_buffer {qm_buffer_mask_ref.sum()}') print(f' N_total: {len(at)}') # Warning: Small size of the cell and large size of the buffer # lead to the qm calculation performed on the whole cell. qmmm = ForceQMMM(at, qm_mask, qm_calc, mm_calc, buffer_width=2 * qm_calc.rc) qmmm.initialize_qm_buffer_mask(at) at.calc = qmmm opt = FIRE(at) opt.run(fmax=1e-3) us.append(at.positions - at0.positions) # compute error in energy norm |\nabla u - \nabla u_ref| def strain_error(at0, u_ref, u, cutoff, mask): I, J = neighbor_list('ij', at0, cutoff) I, J = np.array([(i, j) for i, j in zip(I, J) if mask[i]]).T v = u_ref - u dv = np.linalg.norm(v[I, :] - v[J, :], axis=1) return np.linalg.norm(dv) du_global = [ strain_error(at0, u_ref, u, 1.5 * sigma, np.ones(len(r))) for u in us ] du_local = [strain_error(at0, u_ref, u, 1.5 * sigma, r < 3.0) for u in us] print('du_local', du_local) print('du_global', du_global) # check local errors are monotonically decreasing assert np.all(np.diff(du_local) < 0) # check global errors are monotonically converging assert np.all(np.diff(du_global) < 0) # biggest QM/MM should match QM result assert du_local[-1] < 1e-10 assert du_global[-1] < 1e-10