def test_double_swaps_ternary( self ): if ( not has_ase_with_ce ): # Disable this test self.skipTest("ASE version has not cluster expansion") return db_name = "test_db_ternary.db" conc_args = { "conc_ratio_min_1":[[4,0,0]], "conc_ratio_max_1":[[0,4,0]], "conc_ratio_min_1":[[2,2,0]], "conc_ratio_max_2":[[1,1,2]] } max_dia = get_max_cluster_dia_name() size_arg = {max_dia:4.05} ceBulk = CEBulk( crystalstructure="fcc", a=4.05, size=[4,4,4], basis_elements=[["Al","Mg","Si"]], \ conc_args=conc_args, db_name=db_name, max_cluster_size=3, **size_arg) ceBulk.reconfigure_settings() corr_func = CorrFunction( ceBulk ) cf = corr_func.get_cf( ceBulk.atoms ) #prefixes = [name.rpartition("_")[0] for name in cf.keys()] #prefixes.remove("") eci = {name:1.0 for name in cf.keys()} calc = CE( ceBulk, eci ) n_tests = 10 # Insert 25 Mg atoms and 25 Si atoms n = 18 for i in range(n): calc.calculate( ceBulk.atoms, ["energy"], [(i,"Al","Mg")]) calc.calculate( ceBulk.atoms, ["energy"], [(i+n,"Al","Si")]) updated_cf = calc.get_cf() brute_force = corr_func.get_cf_by_cluster_names( ceBulk.atoms, updated_cf.keys() ) for key in updated_cf.keys(): self.assertAlmostEqual( brute_force[key], updated_cf[key]) # Swap atoms for i in range(n_tests): indx1 = np.random.randint(low=0,high=len(ceBulk.atoms)) symb1 = ceBulk.atoms[indx1].symbol indx2 = indx1 symb2 = symb1 while( symb2 == symb1 ): indx2 = np.random.randint( low=0, high=len(ceBulk.atoms) ) symb2 = ceBulk.atoms[indx2].symbol calc.calculate( ceBulk.atoms, ["energy"], [(indx1,symb1,symb2),(indx2,symb2,symb1)]) update_cf = calc.get_cf() brute_force = corr_func.get_cf_by_cluster_names( ceBulk.atoms, update_cf.keys() ) for key,value in brute_force.items(): self.assertAlmostEqual( value, update_cf[key])
def test_double_swaps(self): if not has_ase_with_ce: self.skipTest("ASE version does not have CE") return for lat in self.lattices: calc, ceBulk, eci = self.get_calc(lat) corr_func = CorrFunction(ceBulk) cf = corr_func.get_cf(ceBulk.atoms) n_tests = 10 # Insert 25 Mg atoms for i in range(25): calc.calculate( ceBulk.atoms, ["energy"], [(i,"Al","Mg")] ) # Swap Al and Mg atoms for i in range(n_tests): indx1 = np.random.randint(low=0, high=len(ceBulk.atoms)) symb1 = ceBulk.atoms[indx1].symbol indx2 = indx1 symb2 = symb1 while symb2 == symb1: indx2 = np.random.randint(low=0, high=len(ceBulk.atoms)) symb2 = ceBulk.atoms[indx2].symbol calc.calculate(ceBulk.atoms, ["energy"], [(indx1,symb1,symb2),(indx2,symb2,symb1)]) updated_cf = calc.get_cf() brute_force = corr_func.get_cf_by_cluster_names( ceBulk.atoms, updated_cf.keys() ) for key,value in brute_force.items(): self.assertAlmostEqual( value, updated_cf[key] )
def test_sequence_of_swap_moves(self): if (not has_ase_with_ce): self.skipTest("ASE does not have CE") return calc,ceBulk,eci = self.get_calc("fcc") corr_func = CorrFunction(ceBulk) cf = corr_func.get_cf(ceBulk.atoms) n_tests = 10 # Insert 10 Mg atoms for i in range(n_tests): calc.calculate( ceBulk.atoms, ["energy"], [(i,"Al","Mg")] ) # Swap Al and Mg atoms changes = [] for i in range(n_tests): indx1 = i indx2 = len(ceBulk.atoms)-i-1 symb1 = "Mg" symb2 = "Al" changes += [(indx1,symb1,symb2),(indx2,symb2,symb1)] calc.calculate( ceBulk.atoms, ["energy"], changes ) updated_cf = calc.get_cf() brute_force = corr_func.get_cf_by_cluster_names( ceBulk.atoms, updated_cf.keys() ) for key,value in brute_force.items(): self.assertAlmostEqual( value, updated_cf[key] )
def test_double_swaps(self): if not has_ase_with_ce: self.skipTest("ASE version does not have CE") return for lat in self.lattices: atoms, ceBulk, _ = self.get_calc(lat) calc = atoms.get_calculator() corr_func = CorrFunction(ceBulk) n_tests = 10 symbs = [atom.symbol for atom in atoms] # Insert 10 Mg atoms for i in range(10): symbs[i] = "Mg" calc.set_symbols(symbs) # Swap Al and Mg atoms for i in range(n_tests): indx1 = np.random.randint(low=0, high=len(ceBulk.atoms)) symb1 = atoms[indx1].symbol indx2 = indx1 symb2 = symb1 while symb2 == symb1: indx2 = np.random.randint(low=0, high=len(atoms)) symb2 = atoms[indx2].symbol calc.calculate(atoms, ["energy"], [(indx1, symb1, symb2),(indx2, symb2, symb1)]) updated_cf = calc.get_cf() brute_force = corr_func.get_cf_by_cluster_names(atoms, updated_cf.keys()) for key,value in brute_force.items(): self.assertAlmostEqual(value, updated_cf[key])
def test_supercell( self ): if ( not has_ase_with_ce ): self.skipTest("ASE version does not have CE") return calc, ceBulk, eci = self.get_calc("fcc") db_name = "test_db_fcc_super.db" conc_args = { "conc_ratio_min_1":[[1,0]], "conc_ratio_max_1":[[0,1]], } kwargs = { "crystalstructure": "fcc", "a": 4.05, "size":[3, 3, 3], "basis_elements":[["Al","Mg"]], "conc_args": conc_args, "db_name": db_name, "max_cluster_size": 3, "max_cluster_dia": 4.05 } kwargs_template = copy.deepcopy(kwargs) kwargs_template["size"] = [4, 4, 4] kwargs_template["db_name"] = "template_bc.db" template_supercell_bc = CEBulk(**kwargs_template) template_supercell_bc.reconfigure_settings() ceBulk = CEBulk(**kwargs) ceBulk.reconfigure_settings() calc = get_ce_calc(ceBulk, kwargs, eci, size=[4, 4, 4], db_name="sc4x4x.db") ceBulk = calc.BC ceBulk.atoms.set_calculator(calc) corr_func = CorrFunction(template_supercell_bc) for i in range(10): calc.calculate(ceBulk.atoms, ["energy"], [(i, "Al", "Mg")]) updated_cf = calc.get_cf() brute_force = corr_func.get_cf_by_cluster_names( ceBulk.atoms, list(updated_cf.keys())) for key, value in brute_force.items(): self.assertAlmostEqual(value, updated_cf[key]) os.remove("sc4x4x.db")
class CEcalc(Calculator): implemented_properties = ['energy'] def __init__(self, ecis, BC): Calculator.__init__(self) self.corrFunc = CorrFunction(BC) self.eci = ecis def calculate(self, atoms, properties, system_changes): Calculator.calculate(self, atoms) cf_by_name = self.corrFunc.get_cf_by_cluster_names( atoms, self.eci.keys()) energy = 0.0 for key, value in self.eci.items(): energy += value * cf_by_name[key] self.results["energy"] = energy * len( atoms) # Should not return energy per atom!
def test_binary_spacegroup( self ): if ( not has_ase_with_ce ): self.skipTest("ASE version does not have CE") return bs, db_name = get_bulkspacegroup_binary() cf = CorrFunction( bs ) cf_vals = cf.get_cf( bs.atoms ) ecis = {name:1.0 for name in cf_vals.keys()} calc = CE( bs, ecis ) bs.atoms.set_calculator( calc ) for i in range(25): if ( bs.atoms[i].symbol == "Al" ): new_symb = "Mg" old_symb = "Al" else: new_symb = "Al" old_symb = "Mg" calc.calculate( bs.atoms, ["energy"], [(i,old_symb,new_symb)] ) updated_cf = calc.get_cf() brute_force = cf.get_cf_by_cluster_names( bs.atoms, updated_cf.keys() ) for key,value in brute_force.items(): self.assertAlmostEqual( value, updated_cf[key] )
def test_random_swaps(self): if not has_ase_with_ce: self.skipTest("ASE version does not have CE") return for lat in self.lattices: msg = "Failed for lattice {}".format(lat) calc, ceBulk, eci = self.get_calc(lat) n_tests = 10 corr_func = CorrFunction(ceBulk) cf = corr_func.get_cf(ceBulk.atoms) for i in range(n_tests): indx = np.random.randint(low=0, high=len(ceBulk.atoms)) old_symb = ceBulk.atoms[indx].symbol if old_symb == "Al": new_symb = "Mg" else: new_symb = "Al" calc.calculate(ceBulk.atoms, ["energy"], [(indx, old_symb, new_symb)]) updated_cf = calc.get_cf() brute_force = corr_func.get_cf_by_cluster_names(ceBulk.atoms, updated_cf.keys()) for key, value in updated_cf.items(): self.assertAlmostEqual(value, brute_force[key], msg=msg)
class CE(Calculator): """ Class for updating the CE when symbols change :param ClusterExpansionSetting BC: Settings from ASE :param dict eci: Effective cluster interactions :param initial_cf: Initial correlation functions, if None all the required correlation functions are calculated from scratch :type initial_cf: dict or None """ implemented_properties = ["energy"] def __init__(self, atoms, BC, eci=None, initial_cf=None): Calculator.__init__(self) self.BC = BC if self._has_self_interaction(BC.cluster_info): raise SelfInteractionError( 'The simulation cell is so small that the same site ' 'is present multiple times within one cluster. ' 'Increase the size of the simulation cell.') # Make sure there is an ECI corresponding to # the emptry cluster if 'c0' not in eci.keys(): eci['c0'] = 0.0 if initial_cf is not None: initial_cf['c0'] = 1.0 # NOTE: This should be handled in the CE code self.BC._info_entries_to_list() self.corrFunc = CorrFunction(self.BC) cf_names = list(eci.keys()) if initial_cf is None: msg = "Calculating {} correlation ".format(len(cf_names)) msg += "functions from scratch" print(msg) self.cf = self.corrFunc.get_cf_by_cluster_names(atoms, cf_names) else: self.cf = initial_cf print("Correlation functions initialized...") self.eci = eci self.atoms = atoms # Attach the calculator to the atoms object self.atoms.set_calculator(self) # Keep a copy of the original symbols symbols = [atom.symbol for atom in self.atoms] self._check_trans_mat_dimensions() self.ctype = {} # self.convert_cluster_indx_to_list() if isinstance(self.BC.trans_matrix, np.ndarray): self.BC.trans_matrix = np.array(self.BC.trans_matrix).astype( np.int32) self.updater = None if use_cpp: print("Initializing C++ calculator...") self.updater = PyCEUpdater(self.atoms, self.BC, self.cf, self.eci) print("C++ module initialized...") if use_cpp: self.clear_history = self.updater.clear_history self.undo_changes = self.updater.undo_changes self.update_cf = self.updater.update_cf else: raise ImportError("Could not find C++ backend for the updater!") # Set the symbols back to their original value self.set_symbols(symbols) self._linear_vib_correction = None def copy(self): """Create a copy of the calculator. :return: New CE instance :rtype: CE """ from copy import deepcopy self.atoms.set_calculator(None) new_bc = deepcopy(self.BC) self.atoms.set_calculator(self) atoms = self.atoms.copy() new_calc = CE(atoms, new_bc, eci=self.eci, initial_cf=self.get_cf()) #new_calc.atoms.set_calculator(new_calc) return new_calc def _check_trans_mat_dimensions(self): """ Check that dimension of the trans matrix matches the number of atoms """ if isinstance(self.BC.trans_matrix, list): n_sites = len(self.BC.trans_matrix) elif isinstance(self.BC.trans_matrix, np.ndarray): n_sites = self.BC.trans_matrix.shape[0] # Make sure that the database information fits if len(self.atoms) != n_sites: msg = "The number of atoms and the dimension of the translation " msg += "matrix is inconsistent\n" msg += "Num atoms: {}. ".format(len(self.atoms)) msg += "Num row trans mat: {}".format(n_sites) raise ValueError(msg) def get_full_cluster_names(self, cnames): """ Returns the full cluster names with decoration info in the end :param list cnames: List of the current cluster names """ full_names = self.cf.keys() only_prefix = [name.rpartition("_")[0] for name in full_names] full_cluster_names = [] # First insert the one body names, nothing to be done for them for name in cnames: if name.startswith("c1"): full_cluster_names.append(name) for name in cnames: if name.startswith("c1"): continue indx = only_prefix.index(name) full_cluster_names.append(full_names[indx]) return full_cluster_names @property def linear_vib_correction(self): return self._linear_vib_correction @linear_vib_correction.setter def linear_vib_correction(self, linvib): if not isinstance(linvib, lvc.LinearVibCorrection): raise TypeError( "Linear vib correction has to be of type LinearVibCorrection!") if (self.linear_vib_correction is not None): orig_eci = self.linear_vib_correction.reset(self.eci) if (orig_eci is not None): self.eci = orig_eci self.update_ecis(self.eci) self._linear_vib_correction = linvib if self.updater is not None: # This just initialize a LinearVibCorrection object, it does not # change the ECIs self.updater.add_linear_vib_correction(linvib.eci_per_kbT) def include_linvib_in_ecis(self, T): """ Includes the effect of linear vibration correction in the ECIs :param float T: Temperature in Kelvin """ if (self.linear_vib_correction is None): return # Reset the ECIs to the original self.update_ecis(self.eci) self.ecis = self.linear_vib_correction.include(self.eci, T) self.update_ecis(self.eci) def vib_energy(self, T): """ Returns the vibration energy per atom :param float T: Temperature in kelving :return: Vibration energy :rtype: float """ if (self.updater is not None): return self.updater.vib_energy(T) if (self.linear_vib_correction is not None): return self.linear_vib_correction.energy(T, self.cf) return 0.0 def get_energy(self): """ Returns the energy of the system :return: Energy of the system :rtype: float """ energy = 0.0 if (self.updater is None): for key, value in self.eci.items(): energy += value * self.cf[key] energy *= len(self.atoms) else: energy = self.updater.get_energy() return energy def create_ctype_lookup(self): """ Creates a lookup table for cluster types based on the prefix """ for n in range(2, len(self.BC.cluster_names)): for ctype in range(len(self.BC.cluster_names[n])): name = self.BC.cluster_names[n][ctype] prefix = name # name.rpartition('_')[0] self.ctype[prefix] = (n, ctype) def calculate(self, atoms, properties, system_changes): """ Calculates the energy. The system_changes is assumed to be a list of tuples of the form (indx,old_symb,new_symb) :param Atoms atoms: This is not used to fit the signature of this function in ASE. The energy returned, is the one of the internal atoms object *after* system_changes is applied :param list properties: Has to be ["energy"] :param list system_changes: Updates to the system. Same signature as :py:meth:`cemc.mcmc.MCObserver.__call__` :return: Energy of the system :rtype: float """ energy = self.updater.calculate(system_changes) self.cf = self.updater.get_cf() self.results["energy"] = energy return self.results["energy"] def get_cf(self): """ Returns the correlation functions :return: Correlation functions :rtype: dict """ if (self.updater is None): return self.cf else: return self.updater.get_cf() def update_ecis(self, new_ecis): """ Updates the ecis :param dict new_ecis: New ECI values """ self.eci = new_ecis if (self.updater is not None): self.updater.set_ecis(self.eci) def get_singlets(self): """ Return the singlets :return: Correlation functions corresponding to singlets :rtype: dict """ return self.updater.get_singlets() def set_composition(self, comp): """ Change composition of an object. :param dict comp: New composition. If you want to set the composition to for instance 20%% Mg and 80 %% Al, this argument should be {"Mg":0.2, "Al":0.8} """ # Verify that the sum of the compositions is one tot_conc = 0.0 max_element = None max_conc = 0.0 for key, conc in comp.items(): tot_conc += conc if (conc > max_conc): max_element = key max_conc = conc if (np.abs(tot_conc - 1.0) > 1E-6): raise ValueError("The specified concentration does not sum to 1!") # Change all atoms to the one with the highest concentration init_elm = max_element for i in range(len(self.atoms)): # self.update_cf( (i,self.atoms[i].symbol,init_elm) ) # Set all # atoms to init element self.calculate(self.atoms, ["energy"], [(i, self.atoms[i].symbol, init_elm)]) start = 0 for elm, conc in comp.items(): if (elm == init_elm): continue n_at = int(round(conc * len(self.atoms))) for i in range(start, start + n_at): self.update_cf((i, init_elm, elm)) start += n_at self.clear_history() def set_symbols(self, symbs): """ Change the symbols of the entire atoms object :param list symbs: List of new symbols """ if (len(symbs) != len(self.atoms)): raise ValueError("Length of the symbols array has to match" "the length of the atoms object.!") for i, symb in enumerate(symbs): self.update_cf((i, self.atoms[i].symbol, symb)) self.clear_history() def singlet2comp(self, singlets): """ Convert singlet to compositions :param dict singlets: Singlet values :return: Concentrations corresponding to the singlets :rtype: dict """ bfs = self.BC.basis_functions if (len(singlets.keys()) != len(bfs)): msg = "The number singlet terms specified is different " msg += "from the number of basis functions\n" msg += "Given singlet terms: {}\n".format(singlets) msg += "Basis functions: {}\n".format(bfs) raise ValueError(msg) # Generate system of equations rhs = np.zeros(len(bfs)) # Concentration of this element is implicitly determined via the others spec_element = list(bfs[0].keys())[0] for key, value in singlets.items(): dec = int(key[-1]) rhs[dec] = value - bfs[dec][spec_element] matrix = np.zeros((len(bfs), len(bfs))) for key in singlets.keys(): row = int(key[-1]) col = 0 for element in bfs[0].keys(): if (element == spec_element): continue matrix[row, col] = bfs[row][element] - bfs[row][spec_element] col += 1 concs = np.linalg.solve(matrix, rhs) eps = 1E-6 # Allow some uncertainty in the solution for i in range(len(concs)): if (concs[i] < 0.0 and concs[i] > -eps): concs[i] = 0.0 conc_spec_element = 1.0 - np.sum(concs) if (conc_spec_element < 0.0 and conc_spec_element > -eps): conc_spec_element = 0.0 # Some trivial checks if (conc_spec_element > 1.0 or conc_spec_element < 0.0): msg = "Something strange happened when converting singlets to " msg += "composition\n" msg += "Concentration of one of the implicitly " msg += "determined element is {}".format(conc_spec_element) raise RuntimeError(msg) if (np.any(concs > 1.0) or np.any(concs < 0.0)): msg = "Something went wrong when the linear system of " msg += "equations were solved.\n" msg += "Final concentration is {}".format(concs) raise RuntimeError(msg) conc_dict = {} counter = 0 for element in bfs[0].keys(): if (element == spec_element): conc_dict[element] = conc_spec_element else: conc_dict[element] = concs[counter] counter += 1 return conc_dict def set_singlets(self, singlets): """ Brings the system into a certain configuration such that the singlet has a certain value. Note that depending on the size of atoms, it is not all singlet value that are possible. So the composition obtained in the end, may differ slightly from the intended value. :param dict singlets: Singlet values """ conc = self.singlet2comp(singlets) self.set_composition(conc) def backup_dict(self): """Return a dictionary containing all arguments for backup. :return: All nessecary arguments to reconstruct the calculator in the current state :rtype: dict """ backup_data = {} backup_data["cf"] = self.get_cf() backup_data["symbols"] = [atom.symbol for atom in self.atoms] backup_data["setting_kwargs"] = self.BC.kwargs backup_data["setting_kwargs"]["classtype"] = type(self.BC).__name__ backup_data["eci"] = self.eci return backup_data def save(self, fname): """ Store all nessecary information required to restart the calculation. :param str fname: Filename """ import json with open(fname, 'w') as outfile: json.dump(self.backup_dict(), outfile, indent=2, separators=(",", ": ")) def __reduce__(self): return (CE.load_from_dict, (self.backup_dict(), )) @staticmethod def load(fname): """Rebuild the calculator. :param str fname: Filename """ import json with open(fname, 'r') as infile: backup_data = json.load(infile) return CE.load_from_dict(backup_data) @staticmethod def load_from_dict(backup_data): """Rebuild the calculator from dictionary :param dict backup_data: All nessecary arguments """ from ase.clease import CEBulk, CECrystal classtype = backup_data["setting_kwargs"].pop("classtype") if classtype == "CEBulk": bc = CEBulk(**backup_data["setting_kwargs"]) elif classtype == "CECrystal": bc = CECrystal(**backup_data["setting_kwargs"]) else: raise ValueError("Unknown setting classtype: {}" "".format(backup_data["setting_kwargs"])) atoms = bc.atoms.copy() for symb in backup_data["symbols"]: atoms.symbol = symb return CE(atoms, bc, eci=backup_data["eci"], initial_cf=backup_data["cf"]) def _has_self_interaction(self, cluster_info): """Check if self interactions exists :param list: Cluster info. Cluster information from CLEASE """ for info in cluster_info: for k, cluster in info.items(): for sub in cluster['indices']: if cluster['ref_indx'] in sub: return True if len(set(sub)) != len(sub): return True return False def set_num_threads(self, num_threads): """ Set the number of threads to use when updating the number of threads :param int num_threads: New number of threads """ self.updater.set_num_threads(num_threads)